_____ _ _ _ _ ______ _____
| ___| | | (_) | (_) | ___ \_ _|
| |__ _ __ | |_ _| |_ _ ___ ___ | |_/ / | |
| __| '_ \| __| | __| |/ _ \/ __|| ___ \ | |
| |__| | | | |_| | |_| | __/\__ \| |_/ / | |
\____/_| |_|\__|_|\__|_|\___||___/\____/ \_/
Behavior Tree framework based on and used for Unity Entities (DOTS)
Existing BT frameworks do not support Entities out of the box.
- 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 entity.
- Able to serialize the behavior tree into a binary file.
- Flexible thread control: force on the main thread, force on job thread, controlled by the behavior tree.
- Runtime debug window to show the states of nodes.
- Optimized. 0 GC allocated by behavior tree itself after initializing, only 64Byte GC allocated every tick by
CreateArchetypeChunkArrayAsync
.
- Incompatible with burst.
- Incompatible with il2cpp.
- Lack of action nodes. (Will add some actions as extensions if I need them)
- Difficult to change tree structure at runtime.
- Node data must be compatible with
Blob
and created byBlobBuilder
- essential: essential part of entities behavior tree, any extension should depend on this package.
- codegen: auto-generate entity query accesors on the methods of nodes.
- builder.component: build behavior tree data from unity components.
- builder.graphview: build behavior tree data by graph with components.
- builder.odin: advanced hierarchy builder based on Odin and its serializer.
- builder.visual: build and use behavior tree by graph of DOTS visual scripting (suspended).
- debug.component-viewer: show selected entity with behavior tree as components in inspector of unity while running.
- variable.scriptable-object: extension for using scriptable object data as a variable source of behavior tree node.
Requirement: Unity >= 2020.2 and entities package >= 0.14.0-preview.19
Install the packages either by
UPM:
modify Packages/manifest.json
as below:
{
"dependencies": {
...
"com.quabug.entities-bt.builder.graphview": "1.4.0",
},
"scopedRegistries": [
{
"name": "package.openupm.com",
"url": "https://package.openupm.com",
"scopes": [
"com.quabug"
]
}
]
}
or
openupm add com.quabug.entities-bt.builder.graphview
- Force Run on Main Thread: running on the 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 the main thread to tick the 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
BlobVariantReader
: read-only variantBlobVariantWriter
: write-only variantBlobVariantReaderAndWriter
: read-write variant, able to link to the same source.
-
LocalVariant
: regular variable, custom value will save intoNodeData
. -
ComponentVariant
: fetch data fromComponent
onEntity
- Component Value Name: which value should be accessed from the component
- Copy To Local Node: Will read component data into a local node and never write back into component data. (Force
ReadOnly
access)
-
NodeVariant
: fetch data from the blob of another node- Node Object: another node should be accessed by this variable, and must be in the same behavior tree.
- Value Field Name: the name of the 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 affect variable once build.
- true: will access data field of Node Object at runtime, something like reference value of Node Object.
-
ScriptableObjectVariant
- Scriptable Object: target SO.
- Scriptable Object Value: target field.
[BehaviorNode("867BFC14-4293-4D4E-B3F0-280AD4BAA403")]
public struct VariantNode : INodeData
{
public BlobVariantReader<int> IntVariant;
public BlobVariantReaderAndWriter<float> FloatVariant;
public NodeState Tick<TNodeBlob, TBlackboard>(int index, ref TNodeBlob blob, ref TBlackboard blackboard)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{
var intVariant = IntVariant.Read(index, ref blob, ref blackboard); // get variable value
var floatVariant = FloatVariant.Read(index, ref blob, ref blackboard);
FloatVariant.Write(index, ref blob, ref blackboard, floatVariant + 1);
return NodeState.Success;
}
public void Reset<TNodeBlob, TBlackboard>(int index, ref TNodeBlob blob, ref TBlackboard bb)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{}
}
Adding multiple BehaviorTreeRoot
onto a single entity gameobject will create numerous behavior trees to control this single entity.
Behavior tree sorted by Order
of BehaviorTreeRoot
.
// 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`
public NodeState Tick<TNodeBlob, TBlackboard>(int index, ref TNodeBlob blob, ref TBlackboard bb)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{ // 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<TNodeBlob, TBlackboard>(int index, ref TNodeBlob blob, ref TBlackboard bb)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{}
}
// 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 NodeState Tick<TNodeBlob, TBlackboard>(int index, ref TNodeBlob blob, ref TBlackboard blackboard)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{
// short-cut to tick first only children
var childState = blob.TickChildrenReturnFirstOrDefault(index, blackboard);
if (childState == 0) // 0 means no child was ticked
// tick an already completed `Sequence` or `Selector` will return 0
{
blob.ResetChildren(index, blackboard);
childState = blob.TickChildrenReturnFirstOrDefault(index, blackboard);
}
if (BreakStates.HasFlag(childState)) return childState;
return NodeState.Running;
}
public void Reset<TNodeBlob, TBlackboard>(int index, ref TNodeBlob blob, ref TBlackboard bb)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{}
}
// 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 NodeState Tick<TNodeBlob, TBlackboard>(int index, ref TNodeBlob blob, ref TBlackboard blackboard)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{
// tick children and break if the child state is running or success.
return blob.TickChildrenReturnLastOrDefault(index, blackboard, breakCheck: state => state.IsRunningOrSuccess());
}
public void Reset<TNodeBlob, TBlackboard>(int index, ref TNodeBlob blob, ref TBlackboard bb)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{}
}
// avoid debugging view since there's nothing that needs to debug for `Selector`
The behavior tree needs some extra information for generating EntityQuery
.
public struct SomeNode : INodeData
{
// read-only access
BlobVariantReader<int> IntVariable;
// read-write access (there's no write-only access)
BlobVariantWriter<float> FloatVariable;
// read-write access
BlobVariantReaderAndWriter<double> FloatVariable;
// leave method attribute to be empty and will generate right access of this method
public NodeState Tick<TNodeBlob, TBlackboard>(int index, ref TNodeBlob blob, ref TBlackboard blackboard)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{
// generate `[ReadOnly(typeof(ReadOnlyComponent)]` on `Tick` method
bb.GetData<ReadOnlyComponent>();
// generate `[ReadWrite(typeof(ReadWriteComponent)]` on `Tick` method
bb.GetDataRef<ReadWriteComponent>();
return NodeState.Success;
}
// or manually declare right access types for this method
[EntitiesBT.Core.ReadWrite(typeof(ReadWriteComponentData))]
[EntitiesBT.Core.ReadOnly(typeof(ReadOnlyComponentData))]
public void Reset<TNodeBlob, TBlackboard>(int index, ref TNodeBlob blob, ref TBlackboard bb)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{
// generate `[ReadOnly(typeof(ReadOnlyComponent)]` on `Reset` method
bb.GetData<ReadOnlyComponent>();
// generate `[ReadWrite(typeof(ReadWriteComponent)]` on `Reset` method
bb.GetDataRef<ReadWriteComponent>();
// ...
}
}
make sure to mark the outside method call with the proper access attributes to generate the appropriate access type on Tick
or Reset
method of the node
public static class Extension
{
[ReadOnly(typeof(FooComponent)), ReadWrite(typeof(BarComponent))]
public static void Call<[ReadWrite] T, [ReadOnly] U>([ReadOnly] Type type) { /* ... */ }
}
public struct SomeNode : INodeData
{
// leave method attribute to be empty to generate automatically
public NodeState Tick<TNodeBlob, TBlackboard>(int index, ref TNodeBlob blob, ref TBlackboard blackboard)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{
// the following call will generate access attributes on `Tick` as below:
// [ReadOnly(typeof(FooComponent))]
// [ReadWrite(typeof(BarComponent))]
// [ReadWrite(typeof(int))]
// [ReadOnly(typeof(float))]
// [ReadOnly(typeof(long))]
Extension.Call<int, float>(typeof(long));
return NodeState.Success;
}
}
- Behavior Node example: PrioritySelectorNode.cs
- Debug View example: BTDebugPrioritySelector.cs
NodeBlob
stores all the behavioral tree's internal data, which can be accessed from any node.
To access specific node data, just store its index and access it 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 automatically add to Entity
on the stage of converting GameObject
to Entity
, if AutoAddBehaviorTreeComponents
is enabled.
A single builder node can produce multiple behavior nodes while building.
public class BTSequence : BTNode<SequenceNode>
{
[Tooltip("Enable this will re-evaluate node state from the first child until running node instead of skip to the running node directly.")]
[SerializeField] private bool _recursiveResetStatesBeforeTick;
public override INodeDataBuilder Self => _recursiveResetStatesBeforeTick
// add `RecursiveResetStateNode` as the 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.
}