Entity Action

ECS actions are the “high structure, high performance” option in State Designer. They’re great when you want Burst/job-friendly logic and you’re running lots of agents. Compared to a GameObject action, an ECS action has more moving pieces, but once the pattern clicks it’s very repeatable.

An ECS action in State Designer is made of four parts:

  1. Authoring action class.
  2. Runtime buffer component struct.
  3. Active flag struct.
  4. System logic (with optional job)

One important note up front: in State Designer, ECSAction<…> is a logic/state node style action, not a stacked GameObject action inside an Action State.

Authoring Action Class

This is the class you add in the graph editor. It stores inspector values and converts them into runtime ECS data. You derive from:

ECSAction<TSystem, TBufferElement, TComponentFlag>

and implement GetBufferElement().

using Opsive.StateDesigner.Runtime.States;
using Unity.Entities;
using UnityEngine;

[Opsive.Shared.Utility.Category("Documentation/ECS")]
[Opsive.Shared.Utility.Description("Moves the entity to a target point.")]
public class MoveToPoint : ECSAction<MoveToPointSystem, MoveToPointComponent, MoveToPointFlag>
{
    [SerializeField] protected Vector3 m_Target = new Vector3(0, 0, 10);
    [SerializeField] protected float m_Speed = 5f;
    [SerializeField] protected float m_ArriveDistance = 0.25f;

    /// <summary>
    /// Creates the runtime ECS buffer element used by the action system.
    /// </summary>
    /// <returns>The initialized action buffer data.</returns>
    public override MoveToPointComponent GetBufferElement()
    {
        return new MoveToPointComponent {
            // RuntimeIndex maps this action to its StateComponent.
            Index = RuntimeIndex,
            Target = m_Target,
            Speed = m_Speed,
            ArriveDistance = m_ArriveDistance
        };
    }
}

Runtime Buffer Component Struct

This is the runtime data your system reads/writes.

using Unity.Entities;
using Unity.Mathematics;

/// <summary>
/// Runtime ECS data for MoveTowardsRandomPointECS.
/// </summary>
public struct MoveToPointComponent : IBufferElementData
{
    public ushort Index;
    public float3 Target;
    public float Speed;
    public float ArriveDistance;
}

The Index field maps to a StateComponent entry, and that entry holds the action/state execution status (QueuedRunningFinished, etc.). Index is the lookup key.

Active Flag Struct

This flag is how State Designer enables/disables your action system per entity/state activity.

using Unity.Entities;

/// <summary>
/// ECS enableable tag indicating MoveTowardsRandomPointECS is active.
/// </summary>
public struct MoveToPointFlag : IComponentData, IEnableableComponent { }

Your action runs only when this flag and EvaluateFlag are both active for the entity.

System Logic (and Job)

The system executes runtime logic for entities matching your query. Usually this means StateComponent + YourBuffer + AnyDataYouNeed + Flag + EvaluateFlag.

using Opsive.StateDesigner.Runtime.Components;
using Opsive.StateDesigner.Runtime.States;
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

[DisableAutoCreation]
public partial struct MoveToPointSystem : ISystem
{
    private EntityQuery m_Query;

    /// <summary>
    /// Builds and caches the entity query used by this action.
    /// </summary>
    /// <param name="state">The current ECS system state.</param>
    [BurstCompile]
    private void OnCreate(ref SystemState state)
    {
        m_Query = SystemAPI.QueryBuilder().WithAll<MoveToPointFlag, EvaluateFlag>().WithAll<StateComponent, MoveToPointComponent, LocalTransform>().Build();
    }

    /// <summary>
    /// Schedules the action job for matching entities.
    /// </summary>
    /// <param name="state">The current ECS system state.</param>
    [BurstCompile]
    private void OnUpdate(ref SystemState state)
    {
        state.Dependency = new MoveToPointJob {
            DeltaTime = SystemAPI.Time.DeltaTime
        }.ScheduleParallel(m_Query, state.Dependency);
    }

    /// <summary>
    /// Job that executes movement and state status transitions.
    /// </summary>
    [BurstCompile]
    private partial struct MoveToPointJob : IJobEntity
    {
        public float DeltaTime;

        /// <summary>
        /// Executes action logic for a single entity's buffers.
        /// </summary>
        /// <param name="entity">The ECS entity.</param>
        /// <param name="stateComponents">State status buffer for the graph.</param>
        /// <param name="actionComponents">Action runtime data buffer.</param>
        /// <param name="transform">Entity transform.</param>
        [BurstCompile]
        public void Execute(ref DynamicBuffer<StateComponent> stateComponents, ref DynamicBuffer<MoveToPointComponent> actionComponents, RefRW<LocalTransform> transform)
        {
            for (int i = 0; i < actionComponents.Length; ++i) {
                var action = actionComponents[i];
                var stateComponent = stateComponents[action.Index];

                if (stateComponent.Status == StateStatus.Inactive || stateComponent.Status == StateStatus.Finished) {
                    continue;
                }

                if (stateComponent.Status == StateStatus.Queued) {
                    stateComponent.Status = StateStatus.Running;
                    stateComponents[action.Index] = stateComponent;
                }

                if (stateComponent.Status != StateStatus.Running) {
                    continue;
                }

                var currentPos = transform.ValueRO.Position;
                var toTarget = action.Target - currentPos;
                var arriveDistSq = action.ArriveDistance * action.ArriveDistance;

                if (math.lengthsq(toTarget) <= arriveDistSq) {
                    stateComponent.Status = StateStatus.Finished;
                    stateComponents[action.Index] = stateComponent;
                    continue;
                }

                var dir = math.normalizesafe(toTarget);
                var t = transform.ValueRW;
                t.Position += dir * action.Speed * DeltaTime;
                transform.ValueRW = t;
            }
        }
    }
}