Banana Tree Success!

I think Banana Tree is complete, for now. Software projects can never be completed. Little UI elements and responses are still missing. No copy-paste yet, no undo-redo. But for its purpose and intended use, it does it very well. I was able to replicate the behaviour of ground and flying crawlers using the tool. By doing this, I also uncovered and fixed some major implementation holes.

Ground enemy behaviour using Banana Tree
Ground enemy behaviour using Banana Tree

The biggest stumbling block for me is when I found out that Unity can’t serialize a nested field that has depth of seven or more. It’s so disappointing. My node tree data was serialized like this:

[Serializable]
public class NodeData {

    [SerializeField]
    private string label;

    ... // more data

    // this is a nested serialized field
    // Unity can't handle this if it's too deep
    [SerializeField]
    private List<NodeData> children;

}

public class BananaTreeBehaviour : MonoBehaviour {

    // I thought I'll never have problems with this
    [SerializeField]
    private NodeData rootNodeData;

    ...

}

I went to the forums and found out that others are complaining about it, too. I found this out last Monday night. I was so excited that I could finally see Banana Tree in action in an actual game, then this happened. Tuesday morning was spent on fixing it. It’s not an easy fix, either. Fortunately, my habits worked for me. Layers of indirection made refactoring easier.

My fix is to have a master list of all node data. I added an extra property named masterId which works as a primary key for the node. Child nodes are then kept as a list of integers where each entry represents the masterId to the actual node instance. I made a custom container class to manage the list of all nodes.

[Serializable]
public class NodeData {

    [SerializeField]
    private int masterId;

    [SerializeField]
    private string label;

    ... // more data

    // no longer nested to circumvent the limitation
    [SerializeField]
    private List<int> children;

}

[Serializable]
public class NodeDataMaster {

    // all nodes of the behaviour tree are collected here
    [SerializeField]
    private List<NodeData> nodeList;

    /**
     * Searches for the node with the specified master id
     */
    public NodeData FindByMasterId(int masterId) {
        for(int i = 0; i < nodeList.Count; ++i) {
            if(nodeList[i].MasterId == masterId) {
                return nodeList[i];
            }
        }

        Assertion.Assert(false, "Can't find node with master id = " + masterId);
        return null;
    }

    public NodeData AddNode() {
        ...
    }

    public void RemoveNode() {
        ...
    }

    ...
}

public class BananaTreeBehaviour : MonoBehaviour {

    // all node manipulation now goes through here
    [SerializeField]
    private NodeDataMaster dataMaster;

    ...

}

I designed the tool to be flexible and extensible without editing any of its source code. This is done through reflection. Custom actions should be implemented as classes that inherits a certain class. By doing so, the editor identifies these classes and adds UI controls for it. Thus, replicating the existing behaviour means porting a lot of function calls to their action class equivalents. My action browser looks like this:

Custom actions
Custom actions

Custom action classes look like this:

    [ActionGroup("Game.Crawler")]
    public class CrawlerHasBlocker : CrawlerAction {

        #region implemented abstract members of BntNodeAdapter
        public override BntResult Start() {
            return Evaluate();
        }

        public override BntResult Update() {
            return Evaluate();
        }

        public override BntResult FixedUpdate() {
            return Evaluate();
        }
        #endregion

        private BntResult Evaluate() {
            if(GetCrawler().HasBlocker()) {
                return BntResult.SUCCESS;
            }

            return BntResult.FAILED;
        }

    }

    [ActionGroup("Game.Path")]
    public class GetTargetPathPosition : BntCachedComponentAction<PathCrawler> {

        [UseVariable]
        public BntVector3 Result {get; set;}

        #region implemented abstract members of BntNodeAdapter
        public override BntResult Start() {
            Result.Value = GetCachedComponent().GetTargetPathPosition();
            return BntResult.SUCCESS;
        }

        public override BntResult Update() {
            return BntResult.SUCCESS;
        }

        public override BntResult FixedUpdate() {
            return BntResult.SUCCESS;
        }
        #endregion

    }

It sounds silly that a single function call need to be promoted to a class to be used in an editor. But I like this better. I like the idea that you can extend the functionality of an editor by just adding classes. This adheres to the Open-Close principle of software engineering.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s