An Entity Action runs State Designer behavior through ECS systems and jobs. Use one when the project already uses ECS or when profiling shows that an Action must scale across many entities. For ordinary scene-object behavior, a GameObject Action is usually the simpler starting point.

In the editor

An Entity Action appears as its own state node rather than as a GameObject Action inside an Action State. After the example class compiles, open Create State, choose Move To Point from the Documentation/ECS category, and place it in the graph.

Select the node to configure Target, Speed, and Arrive Distance in the Element Inspector. These fields come from the serialized values on the authoring class; State Designer converts them to ECS data when it initializes the graph.

Authoring workflow

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

  1. An authoring Action class that appears in the editor.
  2. A runtime buffer component containing the per-entity values.
  3. An enableable flag identifying when this Action is active.
  4. A system, with an optional job, that performs the runtime work.

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

This is the runtime data your system reads/writes.

using Unity.Entities;
using Unity.Mathematics;

/// <summary>
/// Runtime ECS data for MoveToPoint.
/// </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 execution status (Queued, Running, Finished, and so on). Index is the lookup key.

Active flag

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

using Unity.Entities;

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

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

System and job

The system executes runtime logic for entities matching your query. Usually this means StateComponent plus the Action buffer, any additional data, the Action flag, and 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="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 distance = math.length(toTarget);
                var dir = math.normalizesafe(toTarget);
                var step = math.min(math.max(0f, action.Speed * DeltaTime), distance);
                var t = transform.ValueRW;
                t.Position += dir * step;
                transform.ValueRW = t;
            }
        }
    }
}

How it runs

When State Designer initializes the graph, GetBufferElement() copies the editor values into a MoveToPointComponent in the entity’s dynamic buffer. RuntimeIndex links that component to the corresponding StateComponent, which stores the Action’s execution status.

State Designer enables MoveToPointFlag when the node is active. The system query also requires EvaluateFlag, the state-status buffer, the Action buffer, and LocalTransform, so the job runs only for entities with the required State Designer data.

The job changes a queued Action to Running and moves the entity toward its target. Each step is clamped to the remaining distance so it cannot overshoot and oscillate around the target. On the next update within the arrival distance, the status changes to Finished. Use a positive Speed to move toward a target outside the arrival distance. State Designer can then follow an outgoing transition from the completed node.

Verify the result

After the scripts compile, confirm that:

  • Move To Point is available in Create State under Documentation/ECS.
  • Selecting the node shows Target, Speed, and Arrive Distance in the Element Inspector.
  • In Play mode, an entity enters Running, moves toward the configured target, and reaches Finished within the arrival distance.

If the node appears but does not run, confirm that the system query includes MoveToPointFlag, EvaluateFlag, StateComponent, MoveToPointComponent, and LocalTransform, and that the job writes status changes back to the StateComponent buffer.