Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

https://github.com/quabug/EntitiesBT

Behavior Tree for Unity ECS (DOTS) framework
https://github.com/quabug/EntitiesBT

behavior-tree unity3d

Last synced: about 1 month ago
JSON representation

Behavior Tree for Unity ECS (DOTS) framework

Lists

README

        

```
_____ _ _ _ _ ______ _____
| ___| | | (_) | (_) | ___ \_ _|
| |__ _ __ | |_ _| |_ _ ___ ___ | |_/ / | |
| __| '_ \| __| | __| |/ _ \/ __|| ___ \ | |
| |__| | | | |_| | |_| | __/\__ \| |_/ / | |
\____/_| |_|\__|_|\__|_|\___||___/\____/ \_/

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

## Release Notes
- [1.2.0](https://github.com/quabug/EntitiesBT/pull/160#issue-696822887)

## Why another Behavior Tree framework?
Existing BT frameworks do not support Entities out of the box.

## 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](Packages/essential/Runtime/Entities/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`](Packages/essential/Runtime/Entities/VirtualMachineSystem.cs#L59).

## Disadvantages
- 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 by [`BlobBuilder`](https://docs.unity3d.com/Packages/[email protected]/api/Unity.Entities.BlobBuilder.html)

## Packages
- [essential](Packages/essential): essential part of entities behavior tree, any extension should depend on this package.
- [codegen](Packages/codegen): auto-generate [entity query accesors](#entityquery) on the methods of nodes.
- [builder.component](Packages/builder.component): build behavior tree data from unity components.
- [builder.graphview](Packages/com.quabug.entities-bt.builder.graphview): build behavior tree data by graph with components.
- [builder.odin](Packages/builder.odin): advanced hierarchy builder based on Odin and its serializer.
- [builder.visual](Packages/builder.visual): build and use behavior tree by graph of DOTS visual scripting (suspended).
- [debug.component-viewer](Packages/debug.component-viewer): show selected entity with behavior tree as components in inspector of unity while running.
- [variable.scriptable-object](Packages/variable.scriptable-object): extension for using scriptable object data as a variable source of behavior tree node.

## HowTo
### Installation
Requirement: Unity >= 2020.2 and entities package >= 0.14.0-preview.19

Install the packages either by

[UPM](https://docs.unity3d.com/Manual/upm-ui-giturl.html):
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](https://openupm.com/docs/getting-started.html#installing-an-upm-package):
```
openupm add com.quabug.entities-bt.builder.graphview
```

### Usage
#### GraphView Builder
##### Create behavior tree graph
create graph

##### Attach graph of behavior tree onto _Entity_
attach graph

#### Component Builder
##### 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 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`](Packages/essential/Runtime/Nodes/Decorators/RunOnMainThreadNode.cs)

#### Variant
##### Variant Types
- `BlobVariantReader`: read-only variant
- `BlobVariantWriter`: write-only variant
- `BlobVariantReaderAndWriter`: read-write variant, able to link to the same source.

##### Variant Sources
- `LocalVariant`: regular variable, custom value will save into `NodeData`.

- `ComponentVariant`: fetch data from `Component` on `Entity`
- _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.

##### Code Example
``` c#
[BehaviorNode("867BFC14-4293-4D4E-B3F0-280AD4BAA403")]
public struct VariantNode : INodeData
{
public BlobVariantReader IntVariant;
public BlobVariantReaderAndWriter FloatVariant;

public NodeState Tick(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(int index, ref TNodeBlob blob, ref TBlackboard bb)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{}
}
```

#### Multiple Trees
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`.

### Debug
debug

### Custom behavior node

#### Action
``` c#
// 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(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(); // get blackboard data by ref (read/write)
var deltaTime = bb.GetData(); // get blackboard data by value (readonly)
translation.Value += Velocity * deltaTime.Value;
return NodeState.Running;
}

public void Reset(int index, ref TNodeBlob blob, ref TBlackboard bb)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{}
}

// debug view (optional)
public class EntityMoveDebugView : BTDebugView {}
```

#### Decorator
``` c#
// 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, 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(int index, ref TNodeBlob blob, ref TBlackboard bb)
where TNodeBlob : struct, INodeBlob
where TBlackboard : struct, IBlackboard
{}
}

// debug view (optional)
public class BTDebugRepeatForever : BTDebugView {}
```

#### Composite
``` c#
// 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, 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(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`
```

#### `EntityQuery`
The behavior tree needs some extra information for generating `EntityQuery`.

``` c#
public struct SomeNode : INodeData
{
// read-only access
BlobVariantReader IntVariable;

// read-write access (there's no write-only access)
BlobVariantWriter FloatVariable;

// read-write access
BlobVariantReaderAndWriter FloatVariable;

// leave method attribute to be empty and will generate right access of this method
public NodeState Tick(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();

// generate `[ReadWrite(typeof(ReadWriteComponent)]` on `Tick` method
bb.GetDataRef();

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(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();

// generate `[ReadWrite(typeof(ReadWriteComponent)]` on `Reset` method
bb.GetDataRef();

// ...
}
}
```

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

```c#

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(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(typeof(long));
return NodeState.Success;
}
}
```

#### Advanced: customize debug view
- Behavior Node example: [PrioritySelectorNode.cs](Packages/essential/Runtime/Nodes/Composites/PrioritySelectorNode.cs)
- Debug View example: [BTDebugPrioritySelector.cs](Packages/debug.component-viewer/Runtime/BTDebugPrioritySelector.cs)

#### Advanced: access other node data
`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(index)`.
- Behavior Node example: [ModifyPriorityNode.cs](Packages/essential/Runtime/Nodes/Actions/ModifyPriorityNode.cs)
- Editor/Builder example: [BTModifyPriority.cs](Packages/builder.component/Runtime/Components/BTModifyPriority.cs)

#### Advanced: behavior tree component
``` c#
[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.

#### Advanced: virtual node builder
A single builder node can produce multiple behavior nodes while building.
``` C#
public class BTSequence : BTNode
{
[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(this)
: base.Self
;
}
```

## Data Structure
``` c#
public struct NodeBlob
{
// default data (serializable data)
public BlobArray Types; // type id of behavior node, generated from `Guid` of `BehaviorNodeAttribute`
public BlobArray EndIndices; // range of node branch must be in [nodeIndex, nodeEndIndex)
public BlobArray Offsets; // data offset of `DefaultDataBlob` of this node
public BlobArray DefaultDataBlob; // nodes data

// runtime only data (only exist on runtime)
public BlobArray States; // nodes states
// initialize from `DefaultDataBlob`
public BlobArray RuntimeDataBlob; // same as `DefaultNodeData` but only available at runtime and will reset to `DefaultNodeData` once reset.
}
```
data-structure