Entities BT by quabug - 1

FrameworksAI

Behavior Tree for Unity ECS (DOTS) framework

Unknown VersionOtherUpdated 2 days agoCreated on December 3rd, 2019
Go to source

EntitiesBT

openupm

Table of contents

Behavior Tree framework based on and used for Unity Entities (DOTS)

Why another Behavior Tree framework?

While developing my new game by using Unity Entities, I found that the existing BT frameworks are not support Entities out of box and also lack of compatibility with plugins like odin, so I decide to write my own.

Features

  • Actions are easy to read/write data from/to entity.
  • Use Component of Unity directly instead of own editor window to maximize compatibility of other plugins.
  • Data-oriented design, save all nodes data into a continuous data blob (NodeBlob.cs)
  • Node has no internal states.
  • Separate runtime nodes and editor nodes.
  • Easy to extend.
  • Also compatible with Unity GameObject without any entity.
  • Able to serialize behavior tree into binary file.
  • Flexible thread control: force on main thread, force on job thread, controlled by behavior tree.
  • Runtime debug window to show the states of nodes.

Disadvantages

  • Incompatible with burst (Won’t support this in the foreseen future)
  • Lack of action nodes. (Will add some actions as extension if I personally need them)
  • Not easy to modify tree structure at runtime.
  • Node data must be compatible with Blob and created by BlobBuilder
  • GC allocate for each frames. (should be optimized soon)

HowTo

Installation

Requirement: Unity >= 2019.3 and entities package >= 0.6.0-preview.24

Install the package either by

UMP: https://github.com/quabug/EntitiesBT.git

or

OpenUMP: openupm add entities-bt

Usage

Create behavior tree

create

Attach behavior tree onto Entity

attach

Serialization

save-to-file

Thread control

thread-control
  • Force Run on Main Thread: running on main thread only, will not use job to tick behavior tree. Safe to call UnityEngine method.
  • Force Run on Job: running on job threads only, will not use main thread to tick behavior tree. Not safe to call UnityEngine method.
  • Controlled by Behavior Tree: Running on job threads by default, but will switch to main thread once meet decorator of RunOnMainThread

Variable Property

Fetch data from different sources.

  • ComponentVariableProperty: fetch data from Component on Entity
    • Component Value Name: which value should be access from component
  • NodeVariableProperty: fetch data from blob of another node
    • Node Object: another node should be access by this variable, must be in the same behavior tree.
    • Value Field Name: the name of data field in another node.
    • Access Runtime Data:
      • false: will copy data to local blob node while building, value change of Node Object won’t effect variable once build.
      • true: will access data field of Node Object at runtime, something like reference value of Node Object.
Code Example
    public class BTVariableNode : BTNode<VariableNode>
    {
#if ODIN_INSPECTOR // use Odin to make it possible to serialize generic type of `VariableProperty`

        [OdinSerialize, NonSerialized] // make this variable serialized by odin serializer instead of unity
        public VariableProperty<int> IntVariable; // an `int` variable property

#else // without Odin, have to generate an interface of `Int32Property` to make it possible to serialize and display by Unity.
      // see "Generate specific types` below

        [SerializeReference, SerializeReferenceButton] // neccessary for editor
        public Int32Property IntVariable; // an `int` variable property

#endif

        protected override void Build(ref VariableNode data, BlobBuilder builder, ITreeNode<INodeDataBuilder>[] tree)
        {
            // save `Int32Property` as `BlobVariable<int>` of `VariableNode`
            IntVariable.Allocate(ref builder, ref data.Variable, this, tree);
        }
    }
    
    [BehaviorNode("867BFC14-4293-4D4E-B3F0-280AD4BAA403")]
    public struct VariableNode : INodeData
    {
        public BlobVariable<int> IntBlobVariable;

        public NodeState Tick(int index, INodeBlob blob, IBlackboard blackboard)
        {
            var intVariable = IntBlobVariable.GetData(index, blob, blackboard); // get variable value
            ref var intVariable = ref IntBlobVariable.GetDataRef(index, blob, blackboard); // get variable ref value
            return NodeState.Success;
        }
        
        public void Reset(int index, INodeBlob blob, IBlackboard bb) {}
    }
Generate specific types of VariableProperty<T>

Generic VariableProperty<T> cannot be serialized in Unity, since [SerializeReference] is not allowed for a generic type. A specific type of VariableProperty<T> must be declared before use.

  • First create a Scriptable Object of VariableGeneratorSetting Snipaste_2020-03-18_18-57-30

  • Fill which Types you want to use as variable property.

  • Fill Filename, Namespace, etc.

  • Create script from this setting and save it in Assets Snipaste_2020-03-18_18-57-36

  • And now you are free to use specific type properties, like float2Property etc.

Debug

debug

Custom behavior node

Action

// most important part of node, actual logic on runtime.
[Serializable] // for debug view only
[BehaviorNode("F5C2EE7E-690A-4B5C-9489-FB362C949192")] // must add this attribute to indicate a class is a `BehaviorNode`
public struct EntityMoveNode : INodeData
{
    public float3 Velocity; // node data saved in `INodeBlob`
    
    [EntitiesBT.Core.ReadWrite(typeof(Translation))] // declare readwrite access of `Translation` component
    [EntitiesBT.Core.ReadOnly(typeof(BehaviorTreeTickDeltaTime))] // declare readonly access of `BehaviorTreeTickDeltaTime` component
    public NodeState Tick(int index, INodeBlob blob, IBlackboard bb)
    { // access and modify node data
        ref var translation = ref bb.GetDataRef<Translation>(); // get blackboard data by ref (read/write)
        var deltaTime = bb.GetData<BehaviorTreeTickDeltaTime>(); // get blackboard data by value (readonly)
        translation.Value += Velocity * deltaTime.Value;
        return NodeState.Running;
    }

    public void Reset(int index, INodeBlob blob, IBlackboard bb) {}
}

// builder and editor part of node
public class EntityMove : BTNode<EntityMoveNode>
{
    public Vector3 Velocity;

    protected override void Build(ref EntityMoveNode data, BlobBuilder _, ITreeNode<INodeDataBuilder>[] __)
    {
        // set `NodeData` here
        data.Velocity = Velocity;
    }
}

// debug view (optional)
public class EntityMoveDebugView : BTDebugView<EntityMoveNode> {}

Decorator

// runtime behavior
[Serializable] // for debug view only
[BehaviorNode("A13666BD-48E3-414A-BD13-5C696F2EA87E", BehaviorNodeType.Decorate/*decorator must explicit declared*/)]
public struct RepeatForeverNode : INodeData
{
    public NodeState BreakStates;
    
    public NodeState Tick(int index, INodeBlob blob, IBlackboard blackboard)
    {
        // short-cut to tick first only children
        var childState = blob.TickChildren(index, blackboard).FirstOrDefault();
        if (childState == 0) // 0 means no child was ticked
                             // tick a already completed `Sequence` or `Selector` will return 0
        {
            blob.ResetChildren(index, blackboard);
            childState = blob.TickChildren(index, blackboard).FirstOrDefault();
        }
        if (BreakStates.HasFlag(childState)) return childState;
        
        return NodeState.Running;
    }

    public void Reset(int index, INodeBlob blob, IBlackboard bb) {}
}

// builder and editor
public class BTRepeat : BTNode<RepeatForeverNode>
{
    public NodeState BreakStates;
    
    public override void Build(ref RepeatForeverNode data, BlobBuilder _, ITreeNode<INodeDataBuilder>[] __)
    {
        data.BreakStates = BreakStates;
    }
}

// debug view (optional)
public class BTDebugRepeatForever : BTDebugView<RepeatForeverNode> {}

Composite

// runtime behavior
[StructLayout(LayoutKind.Explicit)] // sizeof(SelectorNode) == 0
[BehaviorNode("BD4C1D8F-BA8E-4D74-9039-7D1E6010B058", BehaviorNodeType.Composite/*composite must explicit declared*/)]
public struct SelectorNode : INodeData
{
    public NodeState Tick(int index, INodeBlob blob, IBlackboard blackboard)
    {
        // tick children and break if child state is running or success.
        return blob.TickChildren(index, blackboard, breakCheck: state => state.IsRunningOrSuccess()).LastOrDefault();
    }

    public void Reset(int index, INodeBlob blob, IBlackboard bb) {}
}

// builder and editor
public class BTSelector : BTNode<SelectorNode> {}

// avoid debug view since there's nothing need to be debug for `Selector`

EntityQuery

Behavior tree need some extra information for generating EntityQuery.

public struct SomeNode : INodeData
{
    // add an `optional`, `readonly` or `readwrite` attribute to mark an access type of `BlobVariable`
    // `readwrite` by default.
    [EntitiesBT.Core.Optional] BlobVariable<int> IntVariable;
    [EntitiesBT.Core.ReadOnly] BlobVariable<float> FloatVariable;
    [EntitiesBT.Core.ReadWrite] BlobVariable<double> FloatVariable;

    // declare right access types for `Tick` method
    [EntitiesBT.Core.ReadWrite(typeof(ReadWriteComponentData))]
    [EntitiesBT.Core.ReadOnly(typeof(ReadOnlyComponentData))]
    public NodeState Tick(int index, INodeBlob blob, IBlackboard blackboard)
    {
        // access `ComponentData`s by variables or directly from `blackboard`
        // ...
        return NodeState.Success;
    }

    // also declare right access types for `Reset` method if there's any
    [EntitiesBT.Core.ReadWrite(typeof(ReadWriteComponentData))]
    public void Reset(int index, INodeBlob blob, IBlackboard bb)
    {
        // access `ComponentData`s by variables or directly from `blackboard`
        // ...
    }
}

Advanced: customize debug view

Advanced: access other node data

NodeBlob store all internal data of behavior tree, and it can be access from any node. To access specific node data, just store its index and access by INodeData.GetNodeData<T>(index).

Advanced: behavior tree component

[BehaviorTreeComponent] // mark a component data as `BehaviorTreeComponent`
public struct BehaviorTreeTickDeltaTime : IComponentData
{
    public float Value;
}

[UpdateBefore(typeof(VirtualMachineSystem))]
public class BehaviorTreeDeltaTimeSystem : ComponentSystem
{
    protected override void OnUpdate()
    {
        Entities.ForEach((ref BehaviorTreeTickDeltaTime deltaTime) => deltaTime.Value = Time.DeltaTime);
    }
}

The components of behavior will add into Entity automatically on the stage of convert GameObject to Entity, if AutoAddBehaviorTreeComponents is enabled.

Advanced: virtual node builder

A single builder node is able to product multiple behavior nodes while building.

public class BTSequence : BTNode<SequenceNode>
{
    [Tooltip("Enable this will re-evaluate node state from first child until running node instead of skip to running node directly.")]
    [SerializeField] private bool _recursiveResetStatesBeforeTick;

    public override INodeDataBuilder Self => _recursiveResetStatesBeforeTick
        // add `RecursiveResetStateNode` as parent of `this` node
        ? new BTVirtualDecorator<RecursiveResetStateNode>(this)
        : base.Self
    ;
}

Data Structure

public struct NodeBlob
{
    // default data (serializable data)
    public BlobArray<int> Types; // type id of behavior node, generated from `Guid` of `BehaviorNodeAttribute`
    public BlobArray<int> EndIndices; // range of node branch must be in [nodeIndex, nodeEndIndex)
    public BlobArray<int> Offsets; // data offset of `DefaultDataBlob` of this node
    public BlobArray<byte> DefaultDataBlob; // nodes data
    
    // runtime only data (only exist on runtime)
    public BlobArray<NodeState> States; // nodes states
    // initialize from `DefaultDataBlob`
    public BlobArray<byte> RuntimeDataBlob; // same as `DefaultNodeData` but only available at runtime and will reset to `DefaultNodeData` once reset.
}
data-structure
Show all projects by quabug