Table of contents
Behavior Tree framework based on and used for Unity Entities (DOTS)
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.
- 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.
- 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.
Requirement: Unity >= 2020.1 and entities package >= 0.4.0-preview.10
Install the package either by
UMP: https://github.com/quabug/EntitiesBT.git
or
OpenUMP: openupm add entities-bt
- 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
// 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`
// declare access of each component data.
public static readonly ComponentType[] Types = {
ComponentType.ReadWrite<Translation>()
, ComponentType.ReadOnly<BehaviorTreeTickDeltaTime>()
};
// access and modify node data
public static NodeState Tick(int index, INodeBlob blob, IBlackboard bb)
{
ref var data = ref blob.GetNodeData<EntityMoveNode>(index);
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 += data.Velocity * deltaTime.Value;
return NodeState.Running;
}
}
// 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> {}
// 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 static 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();
}
ref var data = ref blob.GetNodeData<RepeatForeverNode>(index);
if (data.BreakStates.HasFlag(childState)) return childState;
return NodeState.Running;
}
}
// 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> {}
// 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 static 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();
}
}
// builder and editor
public class BTSelector : BTNode<SelectorNode> {}
// avoid debug view since there's nothing need to be debug for `Selector`
- Behavior Node example: PrioritySelectorNode.cs
- Debug View example: BTDebugPrioritySelector.cs
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)
.
- Behavior Node example: ModifyPriorityNode.cs
- Editor/Builder example: BTModifyPriority.cs
[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.
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
;
}
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.
}