_____ _ _ _ _ ______ _____
| ___| | | (_) | (_) | ___ \_ _|
| |__ _ __ | |_ _| |_ _ ___ ___ | |_/ / | |
| __| '_ \| __| | __| |/ _ \/ __|| ___ \ | |
| |__| | | | |_| | |_| | __/\__ \| |_/ / | |
\____/_| |_|\__|_|\__|_|\___||___/\____/ \_/
Behavior Tree framework based on and used for Unity Entities (DOTS)
Existing BT frameworks are not support Entities out of 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 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.
- Optimized. 0 GC allocated by behavior tree itself after initialized, only 64Byte GC allocated every tick by
CreateArchetypeChunkArrayAsync
.
- Incompatible with burst.
- Incompatible with il2cpp.
- 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 byBlobBuilder
- essential: essential part of entities behavior tree, any extension should depend on this package.
- codegen: automatically 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 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 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
BlobVariantReader
: read-only variantBlobVariantWriter
: write-only variantBlobVariantReaderAndWriter
: read-write variant, able to link to same source.
-
LocalVariant
: regular variable, custom value will save intoNodeData
. -
ComponentVariant
: fetch data fromComponent
onEntity
- Component Value Name: which value should be access from component
- Copy To Local Node: Will read component data into local node and never write back into component data. (Force
ReadOnly
access)
-
NodeVariant
: 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.
-
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
{}
}
Add multiple BehaviorTreeRoot
onto a single entity gameobject will create multiple behavior tree 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 a 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 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 debug view since there's nothing need to be debug for `Selector`
Behavior tree need 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 outside method call with right access attributes to generate right access type on Tick
or Reset
method of 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` like 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
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.
}