Entity Task

Entity tasks are extremely efficient tasks that can use Burst and the job system. With that said, there is considerably more structure required for Entity tasks as you cannot inherit base classes. Before beginning this example make sure you have an understanding of how the DOTS system works as we will be focusing on the structure specific to Behavior Designer. There are three parts to an Entity task:

  • Authoring Struct: The struct that is created when the task is added to the behavior tree. This struct does not contain any runtime logic and it is purely used to create the necessary components/systems at runtime.
  • Component Struct: The struct that is used at runtime. This struct is traversed by the system logic.
  • System Logic: The runtime logic for the task. Can start a job and run in parallel.
  • Job Logic (optional): Created by the system, the job can optionally execute the runtime logic in parallel.
  • Reevaluation System Logic (optional): Used by conditional tasks, the reevaluation system is responsible for reevaluating the conditional task logic.
  • Reevaluate System Job (optional): Optional job created by the reevaluation system in order to reevaluate in parallel instead of in the system.

For our example we are going to create a new conditional task that checks if the Entity is within distance of a point. While this specific task doesn’t have as much application outside of the documentation it does a good job of focusing on the specific Behavior Designer structure. This is a conditional task so we can demonstrate the reevaluation setup but all of the tasks have a similar structure.

Authoring Struct

The authoring struct must implement a number of interfaces in order for it to be compatible with Behavior Designer:

  • ILogicNode: This interface exposes the parameters required in order for the task to be added to Behavior Designer.
  • ITaskComponentData: This interface contains the properties and methods required in order to add the correct components, systems, and tags at runtime.
  • IConditional: Specifies that this is a conditional task. There are similar IAction, IComposite, and IDecorator interfaces.
  • IReevaluateResponder: Specifies that this conditional task can be reevaluated.

After creating a new struct and implementing the interfaces we have:

public struct WithinDistance : ILogicNode, ITaskComponentData, IConditional, IReevaluateResponder
{
    // Required ILogicNode parameters.
    [Tooltip("The index of the node.")]
    [SerializeField] ushort m_Index;
    [Tooltip("The parent index of the node. ushort.MaxValue indicates no parent.")]
    [SerializeField] ushort m_ParentIndex;
    [Tooltip("The sibling index of the node. ushort.MaxValue indicates no sibling.")]
    [SerializeField] ushort m_SiblingIndex;

    // Required ILogicNode properties.
    public ushort Index { get => m_Index; set => m_Index = value; }
    public ushort ParentIndex { get => m_ParentIndex; set => m_ParentIndex = value; }
    public ushort SiblingIndex { get => m_SiblingIndex; set => m_SiblingIndex = value; }
    public ushort RuntimeIndex { get; set; }

    // Required ITaskComponentData properties.
    public ComponentType Tag { get => typeof(WithinDistanceTag); }
    public System.Type SystemType { get => typeof(WithinDistanceTaskSystem); }

    // Required IReevaluateResponder properties.
    public ComponentType ReevaluateTag { get => typeof(WithinDistanceReevaluateTag); }
    public System.Type ReevaluateSystemType { get => typeof(WithinDistanceReevaluateTaskSystem); }

    /// <summary>
    /// Adds the IBufferElementData to the entity.
    /// </summary>
    /// <param name="world">The world that the entity exists.</param>
    /// <param name="entity">The entity that the IBufferElementData should be assigned to.</param>
    public void AddBufferElement(World world, Entity entity)
    {
        DynamicBuffer<WithinDistanceComponent> buffer;
        if (world.EntityManager.HasBuffer<WithinDistanceComponent>(entity)) {
            buffer = world.EntityManager.GetBuffer<WithinDistanceComponent>(entity);
        } else {
            buffer = world.EntityManager.AddBuffer<WithinDistanceComponent>(entity);
        }

        buffer.Add(new WithinDistanceComponent()
        {
            Index = RuntimeIndex,
        });
    }

    /// <summary>
    /// Clears the IBufferElementData from the entity.
    /// </summary>
    /// <param name="world">The world that the entity exists.</param>
    /// <param name="entity">The entity that the IBufferElementData should be cleared from.</param>
    public void ClearBufferElement(World world, Entity entity)
    {
        DynamicBuffer<WithinDistanceComponent> buffer;
        if (world.EntityManager.HasBuffer<WithinDistanceComponent>(entity)) {
            buffer = world.EntityManager.GetBuffer<WithinDistanceComponent>(entity);
            buffer.Clear();
        }
    }
}

These properties and methods are required for the basic task structure, we haven’t added anything specific to the Within Distance task yet. Multiple tasks of the same type can be added to the behavior tree so each component is stored within a DynamicBuffer. AddBufferElement and ClearBufferElement use the WithinDistanceComponent struct but we haven’t defined that struct yet. This struct is the component struct from the first list on this page. Before defining the struct lets first add the Within Distance parameters:

    // WithinDistance parameters.
    [Tooltip("The target position to determine if the agent is within the distance of.")]
    [SerializeField] Vector3 m_Target;
    [Tooltip("Returns success when the agent is within the specified distance.")]
    [SerializeField] float m_Distance;

These parameters now need to be set on the WithinDistanceComponent. We can do that by modifying the new WithinDistanceComponent block within AddBufferElement:

    buffer.Add(new WithinDistanceComponent()
    {
        Index = RuntimeIndex,
        Target = m_Target,
        Distance = m_Distance
    });

The full task will be added at the end of the page.

Component Struct

The component struct is used at runtime by the system. This contains the actual data that is used within the behavior tree. This component is similar to the authoring component except it doesn’t contain any of the behavior tree structure logic or the setup/cleanup code:

/// <summary>
/// The DOTS data structure for the WithinDistance struct.
/// </summary>
public struct WithinDistanceComponent : IBufferElementData
{
    [Tooltip("The index of the node.")]
    public ushort Index;
    [Tooltip("The target position to determine if the agent is within the distance of.")]
    public float3 Target;
    [Tooltip("Returns success when the agent is within the specified distance.")]
    public float Distance;
}

/// <summary>
/// A DOTS tag indicating when a WithinDistance node is active.
/// </summary>
public struct WithinDistanceTag : IComponentData, IEnableableComponent { }

The WithinDistanceComponent contains the same logic parameters defined as the authoring component with the Index addition. When the behavior tree is initialized a DynamicBuffer of TaskComponents are added to the entity. The TaskComponent is an internal Behavior Designer structure that contains the information about the task, such as its execution status. The Index parameter within the WithinDistanceComponent specifies the index of the TaskComponent that corresponds to the Within Distance task. This is used so the status of the task can be changed.

The WithinDistanceTag specifies when the task is active. This is the same tag that is specified by the Tag property within the WithinDistance authoring component from above.

System Logic

It’s now time to use the components that we created. Within the authoring component you’ll see that there’s a SystemType property that returns the WithinDistanceTaskSystem. This is the system that is responsible for executing the runtime logic of the component. Let’s create that system:

/// <summary>
/// Runs the WithinDistance logic.
/// </summary>
[DisableAutoCreation]
public partial struct WithinDistanceTaskSystem : ISystem
{
    /// <summary>
    /// Creates the job.
    /// </summary>
    /// <param name="state">The current state of the system.</param>
    [BurstCompile]
    private void OnUpdate(ref SystemState state)
    {
        var query = SystemAPI.QueryBuilder().WithAllRW<TaskComponent>().WithAll<WithinDistanceComponent, WithinDistanceTag, EvaluationComponent>().Build();
        state.Dependency = new WithinDistanceJob().ScheduleParallel(query, state.Dependency);
    }
}

This system is extremely simple because the work will be done by the WithinDistanceJob. You can run your logic within the system itself or create a job. The built-in tasks within Behavior Designer will do either depending on the situation. The one thing to notice is that within the query we are using the BranchComponent and EvaluationComponent. These are two Behavior Designer structures:

  • BranchComponent: The execution status of the branch. Remember behavior trees can have multiple branches running at the same time, and this component is responsible for keeping track of the active task index for each branch. It also handles interrupts.
  • EvaluationComponent: Contains information about the behavior tree execution. If the behavior tree is disabled then this component will not exist on the entity allowing the system to skip processing that entity.
Job Logic

For our task the job is responsible for executing the task logic. This job looks like:

/// <summary>
/// Job which executes the task logic.
/// </summary>
[BurstCompile]
private partial struct WithinDistanceJob : IJobEntity
{
    /// <summary>
    /// Executes the idle logic.
    /// </summary>
    /// <param name="taskComponents">An array of TaskComponents.</param>
    /// <param name="withinDistanceComponents">An array of WithinDistanceComponents.</param>
    /// <param name="transform">The entity's transform.</param>
    [BurstCompile]
    public void Execute(ref DynamicBuffer<TaskComponent> taskComponents, ref DynamicBuffer<WithinDistanceComponent> withinDistanceComponents, RefRO<LocalTransform> transform)
    {
        for (int i = 0; i < withinDistanceComponents.Length; ++i) {
            var withinDistanceComponent = withinDistanceComponents[i];
            var taskComponent = taskComponents[withinDistanceComponent.Index];
            if (taskComponent.Status != TaskStatus.Queued) {
                continue;
            }

            taskComponent.Status = math.distance(transform.ValueRO.Position, withinDistanceComponent.Target) < withinDistanceComponent.Distance ? TaskStatus.Success : TaskStatus.Failure;
            taskComponents[withinDistanceComponent.Index] = taskComponent;
        }
    }
}

This is the heart of the task. The job will loop through all of the WithinDistanceComponents and check for the task that should start running. We are checking against a TaskStatus of Queued rather than Running because the task isn’t actually running until the task itself says that it is. This allows the task to perform any initialization logic if it needs to. Since our task is a conditional it should only return success or failure and that’s what we are doing with the distance check:

taskComponent.Status = math.distance(transform.ValueRO.Position, withinDistanceComponent.Target) < withinDistanceComponent.Distance ? TaskStatus.Success : TaskStatus.Failure;

When the task status is set it needs to be stored within the TaskComponents array. Because the task is not continuing to run we then needed to pass execution up to the parent index. This is done with the three BranchComponent lines. If we didn’t include this then the behavior tree would keep trying to execute the Within Distance task, and because that Within Distance task has a status of Success or Failure it would not actually execute.

With these components and systems setup we have a complete DOTS task! The only additional thing that we need to add is the code that will reevaluate the task logic because the task is a conditional task.

Reevaluation System Logic

When the task reevaluates because of a conditional abort a similar but different system should run. The reason for this is because when of the BranchComponent code from the job above. When the task is being reevaluated it doesn’t have control over the branch so we do not want to set the NextIndex of the BranchComponent. The rest of the logic is similar. Instead of creating a new job we are going to do everything within the system to show a different way to accomplish the same thing.

/// <summary>
/// A DOTS tag indicating when an WithinDistance node needs to be reevaluated.
/// </summary>
public struct WithinDistanceReevaluateTag : IComponentData, IEnableableComponent
{
}

/// <summary>
/// Runs the WithinDistance reevaluation logic.
/// </summary>
[DisableAutoCreation]
public partial struct WithinDistanceReevaluateTaskSystem : ISystem
{
    /// <summary>
    /// Updates the reevaluation logic.
    /// </summary>
    /// <param name="state">The current state of the system.</param>
    [BurstCompile]
    private void OnUpdate(ref SystemState state)
    {
        foreach (var (taskComponents, withinDistanceComponents, transform) in
SystemAPI.Query<DynamicBuffer<TaskComponent>, DynamicBuffer<WithinDistanceComponent>, RefRO<LocalTransform>>().WithAll<WithinDistanceReevaluateTag, EvaluationComponent>()) {
        for (int i = 0; i < withinDistanceComponents.Length; ++i) {
        var withinDistanceComponent = withinDistanceComponents[i];
        var taskComponent = taskComponents[withinDistanceComponent.Index];
        if (!taskComponent.Reevaluate) {
            continue;
        }

        var status = math.distance(transform.ValueRO.Position, withinDistanceComponent.Target) < withinDistanceComponent.Distance ? TaskStatus.Success : TaskStatus.Failure;
        if (status != taskComponent.Status) {
            taskComponent.Status = status;
            var buffer = taskComponents;
            buffer[taskComponent.Index] = taskComponent;
        }
    }
}

The WithinDistanceReevaluateTag is similar to the WithinDistanceTag except it is only used to specify when the task is being reevaluated, rather than when it is actually active. The WithinDistanceReevaluateTaskSystem is then similar to the setup for WithinDistanceTaskSystem. There are two key differences with this reevaluation system:

        if (!taskComponent.Reevaluate) {
            continue;
        }

In the original system we compared taskComponent.Status != TaskStatus.Queued, whereas here we are determining if it is being reevaluated. The second difference is in the assignment if the status is different:

        if (status != taskComponent.Status) {
            taskComponent.Status = status;
            var buffer = taskComponents;
            buffer[taskComponent.Index] = taskComponent;
        }

This is done to prevent unnecessary assignment. You could have always assigned theĀ TaskComponent to theĀ TaskComponents but there’s no reason to if there are no changes. A conditional abort system will then compare the TaskComponent status against the original status to determine if the status is different. If the status is different then an abort will be triggered.

With this system in place we now have a fully working DOTS behavior tree task!

Complete Script

The entire script is below with the namespaces and all of the comments.

using Opsive.BehaviorDesigner.Runtime.Components;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.GraphDesigner.Runtime;
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

[NodeDescription("Returns success when the agent is within distance of the specified position.")]
public struct WithinDistance : ILogicNode, ITaskComponentData, IConditional, IReevaluateResponder
{
    // Required ILogicNode parameters.
    [Tooltip("The index of the node.")]
    [SerializeField] ushort m_Index;
    [Tooltip("The parent index of the node. ushort.MaxValue indicates no parent.")]
    [SerializeField] ushort m_ParentIndex;
    [Tooltip("The sibling index of the node. ushort.MaxValue indicates no sibling.")]
    [SerializeField] ushort m_SiblingIndex;

    // WithinDistance parameters.
    [Tooltip("The target position to determine if the agent is within the distance of.")]
    [SerializeField] Vector3 m_Target;
    [Tooltip("Returns success when the agent is within the specified distance.")]
    [SerializeField] float m_Distance;

    // Required ILogicNode properties.
    public ushort Index { get => m_Index; set => m_Index = value; }
    public ushort ParentIndex { get => m_ParentIndex; set => m_ParentIndex = value; }
    public ushort SiblingIndex { get => m_SiblingIndex; set => m_SiblingIndex = value; }
    public ushort RuntimeIndex { get; set; }

    // Required ITaskComponentData properties.
    public ComponentType Tag { get => typeof(WithinDistanceTag); }
    public System.Type SystemType { get => typeof(WithinDistanceTaskSystem); }

    // Required IReevaluateResponder properties.
    public ComponentType ReevaluateTag { get => typeof(WithinDistanceReevaluateTag); }
    public System.Type ReevaluateSystemType { get => typeof(WithinDistanceReevaluateTaskSystem); }

    /// <summary>
    /// Adds the IBufferElementData to the entity.
    /// </summary>
    /// <param name="world">The world that the entity exists.</param>
    /// <param name="entity">The entity that the IBufferElementData should be assigned to.</param>
    public void AddBufferElement(World world, Entity entity)
    {
        DynamicBuffer<WithinDistanceComponent> buffer;
        if (world.EntityManager.HasBuffer<WithinDistanceComponent>(entity)) {
            buffer = world.EntityManager.GetBuffer<WithinDistanceComponent>(entity);
        } else {
            buffer = world.EntityManager.AddBuffer<WithinDistanceComponent>(entity);
        }

        buffer.Add(new WithinDistanceComponent()
        {
            Index = RuntimeIndex,
            Target = m_Target,
            Distance = m_Distance
        });
    }

    /// <summary>
    /// Clears the IBufferElementData from the entity.
    /// </summary>
    /// <param name="world">The world that the entity exists.</param>
    /// <param name="entity">The entity that the IBufferElementData should be cleared from.</param>
    public void ClearBufferElement(World world, Entity entity)
    {
        DynamicBuffer<WithinDistanceComponent> buffer;
        if (world.EntityManager.HasBuffer<WithinDistanceComponent>(entity)) {
            buffer = world.EntityManager.GetBuffer<WithinDistanceComponent>(entity);
            buffer.Clear();
        }
    }
}

/// <summary>
/// The DOTS data structure for the WithinDistance struct.
/// </summary>
public struct WithinDistanceComponent : IBufferElementData
{
    [Tooltip("The index of the node.")]
    public ushort Index;
    [Tooltip("The target position to determine if the agent is within the distance of.")]
    public float3 Target;
    [Tooltip("Returns success when the agent is within the specified distance.")]
    public float Distance;
}

/// <summary>
/// A DOTS tag indicating when a WithinDistance node is active.
/// </summary>
public struct WithinDistanceTag : IComponentData, IEnableableComponent { }

/// <summary>
/// Runs the WithinDistance logic.
/// </summary>
[DisableAutoCreation]
public partial struct WithinDistanceTaskSystem : ISystem
{
    /// <summary>
    /// Creates the job.
    /// </summary>
    /// <param name="state">The current state of the system.</param>
    [BurstCompile]
    private void OnUpdate(ref SystemState state)
    {
        var query = SystemAPI.QueryBuilder().WithAllRW<TaskComponent>().WithAll<WithinDistanceComponent, WithinDistanceTag, EvaluationComponent>().Build();
        state.Dependency = new WithinDistanceJob().ScheduleParallel(query, state.Dependency);
    }

    /// <summary>
    /// Job which executes the task logic.
    /// </summary>
    [BurstCompile]
    private partial struct WithinDistanceJob : IJobEntity
    {
        /// <summary>
        /// Executes the idle logic.
        /// </summary>
        /// <param name="taskComponents">An array of TaskComponents.</param>
        /// <param name="withinDistanceComponents">An array of WithinDistanceComponents.</param>
        /// <param name="transform">The entity's transform.</param>
        [BurstCompile]
        public void Execute(ref DynamicBuffer<TaskComponent> taskComponents, ref DynamicBuffer<WithinDistanceComponent> withinDistanceComponents, RefRO<LocalTransform> transform)
        {
            for (int i = 0; i < withinDistanceComponents.Length; ++i) {
                var withinDistanceComponent = withinDistanceComponents[i];
                var taskComponent = taskComponents[withinDistanceComponent.Index];
                if (taskComponent.Status != TaskStatus.Queued) {
                    continue;
                }

                taskComponent.Status = math.distance(transform.ValueRO.Position, withinDistanceComponent.Target) < withinDistanceComponent.Distance ? TaskStatus.Success : TaskStatus.Failure;
                taskComponents[withinDistanceComponent.Index] = taskComponent;
            }
        }
    }
}

/// <summary>
/// A DOTS tag indicating when an WithinDistance node needs to be reevaluated.
/// </summary>
public struct WithinDistanceReevaluateTag : IComponentData, IEnableableComponent
{
}

/// <summary>
/// Runs the WithinDistance reevaluation logic.
/// </summary>
[DisableAutoCreation]
public partial struct WithinDistanceReevaluateTaskSystem : ISystem
{
    /// <summary>
    /// Updates the reevaluation logic.
    /// </summary>
    /// <param name="state">The current state of the system.</param>
    [BurstCompile]
    private void OnUpdate(ref SystemState state)
    {
        foreach (var (taskComponents, withinDistanceComponents, transform) in
SystemAPI.Query<DynamicBuffer<TaskComponent>, DynamicBuffer<WithinDistanceComponent>, RefRO<LocalTransform>>().WithAll<WithinDistanceReevaluateTag, EvaluationComponent>()) {
            for (int i = 0; i < withinDistanceComponents.Length; ++i) {
                var withinDistanceComponent = withinDistanceComponents[i];
                var taskComponent = taskComponents[withinDistanceComponent.Index];
                if (!taskComponent.Reevaluate) {
                    continue;
                }

                var status = math.distance(transform.ValueRO.Position, withinDistanceComponent.Target) < withinDistanceComponent.Distance ? TaskStatus.Success : TaskStatus.Failure;
                if (status != taskComponent.Status) {
                    taskComponent.Status = status;
                    var buffer = taskComponents;
                    buffer[taskComponent.Index] = taskComponent;
                }
            }
        }
    }
}