Entity Condition
ECS conditions are the high-performance way to author transition checks in State Designer. They run in DOTS systems/jobs and participate in transition evaluation by updating TransitionComponent.ValidConditionCount. Like ECS actions, ECS conditions use a consistent structure with multiple parts. An ECS condition has four core parts:
- Authoring condition class.
- Runtime buffer component struct.
- Active flag struct.
- System logic (with optional job).
One key difference from GameObject conditions: you do not return bool from IsValid(). Instead, your ECS system increments ValidConditionCount when the condition is true.
Authoring Condition Class
This class will capture condition parameters and build runtime condition data for a specific transition index.
using Opsive.StateDesigner.Runtime.Conditions;
using Unity.Entities;
using UnityEngine;
/// <summary>
/// Returns valid when the origin is within distance of the target.
/// </summary>
[Opsive.Shared.Utility.Category("Documentation/ECS")]
[Opsive.Shared.Utility.Description("Returns valid when origin is within distance of target.")]
public class WithinDistanceECSCondition : ECSCondition<WithinDistanceECSConditionSystem, WithinDistanceECSConditionComponent, WithinDistanceECSConditionFlag>
{
[Tooltip("Origin position.")]
[SerializeField] protected Vector3 m_Origin;
[Tooltip("Target position.")]
[SerializeField] protected Vector3 m_Target;
[Tooltip("Distance threshold.")]
[SerializeField] protected float m_Distance = 5f;
/// <summary>
/// Creates runtime ECS data for this condition and transition.
/// </summary>
/// <param name="transitionIndex">The transition buffer index this condition belongs to.</param>
/// <returns>The initialized condition buffer data.</returns>
public override WithinDistanceECSConditionComponent GetBufferElement(ushort transitionIndex)
{
return new WithinDistanceECSConditionComponent
{
TransitionIndex = transitionIndex,
Origin = m_Origin,
Target = m_Target,
Distance = m_Distance
};
}
/// <summary>
/// Resets inspector values to defaults.
/// </summary>
public override void Reset()
{
base.Reset();
m_Origin = Vector3.zero;
m_Target = Vector3.zero;
m_Distance = 5f;
}
}
This is the class added in the graph inspector. This class only prepares data. It does not evaluate the condition itself. The transitionIndex is critical because the system uses it to update the right transition entry.
Runtime Buffer Component Struct
This struct is the runtime data your condition system reads.
using Unity.Entities;
using Unity.Mathematics;
/// <summary>
/// Runtime ECS data for WithinDistanceECSCondition.
/// </summary>
public struct WithinDistanceECSConditionComponent : IBufferElementData
{
public ushort TransitionIndex;
public float3 Origin;
public float3 Target;
public float Distance;
}
TransitionIndex maps this condition entry to the correct TransitionComponent in the entity’s transition buffer.
Active Flag Struct
This enableable tag controls when the condition system should process the entity.
/// <summary>
/// ECS enableable tag indicating WithinDistanceECSCondition is active.
/// </summary>
public struct WithinDistanceECSConditionFlag : IComponentData, IEnableableComponent { }
System Logic (with Job)
The condition system runs in ConditionSystemGroup. When condition data is valid, increment transition.ValidConditionCount.
using Opsive.StateDesigner.Runtime.Components;
using Opsive.StateDesigner.Runtime.Groups;
using Opsive.StateDesigner.Runtime.States;
using Opsive.StateDesigner.Runtime.Transitions;
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
/// <summary>
/// Runtime system for WithinDistanceECSCondition.
/// </summary>
[DisableAutoCreation]
[UpdateInGroup(typeof(ConditionSystemGroup))]
public partial struct WithinDistanceECSConditionSystem : ISystem
{
private EntityQuery m_Query;
/// <summary>
/// Builds and caches the entity query used by this condition.
/// </summary>
/// <param name="state">The current ECS system state.</param>
[BurstCompile]
private void OnCreate(ref SystemState state)
{
m_Query = SystemAPI.QueryBuilder().WithAllRW<TransitionComponent>().WithAll<StateComponent, WithinDistanceECSConditionComponent, WithinDistanceECSConditionFlag, EvaluateFlag>().Build();
}
/// <summary>
/// Schedules the condition job for matching entities.
/// </summary>
/// <param name="state">The current ECS system state.</param>
[BurstCompile]
private void OnUpdate(ref SystemState state)
{
state.Dependency = new WithinDistanceECSConditionJob().ScheduleParallel(m_Query, state.Dependency);
}
/// <summary>
/// Job that evaluates the condition and updates transition validity counts.
/// </summary>
[BurstCompile]
private partial struct WithinDistanceECSConditionJob : IJobEntity
{
/// <summary>
/// Executes condition logic for a single entity's buffers.
/// </summary>
/// <param name="transitionComponents">Transition buffer.</param>
/// <param name="stateComponents">State buffer.</param>
/// <param name="conditionComponents">Condition runtime data buffer.</param>
[BurstCompile]
public void Execute(ref DynamicBuffer<TransitionComponent> transitionComponents, in DynamicBuffer<StateComponent> stateComponents, in DynamicBuffer<WithinDistanceECSConditionComponent> conditionComponents)
{
for (int i = 0; i < conditionComponents.Length; ++i) {
var condition = conditionComponents[i];
if (condition.TransitionIndex >= transitionComponents.Length) {
continue;
}
var transition = transitionComponents[condition.TransitionIndex];
if (transition.SourceIndex >= stateComponents.Length) {
continue;
}
var sourceState = stateComponents[transition.SourceIndex];
if (sourceState.Status == StateStatus.Inactive) {
continue;
}
var shouldEvaluate = transition.EvaluationMode switch
{
EvaluationMode.Continuous => true,
EvaluationMode.OnStateFinished => sourceState.Status == StateStatus.Finished,
EvaluationMode.OnIterationComplete => sourceState.IterationComplete,
_ => true
};
if (!shouldEvaluate) {
continue;
}
if (math.distance(condition.Origin, condition.Target) <= condition.Distance) {
transition.ValidConditionCount++;
transitionComponents[condition.TransitionIndex] = transition;
}
}
}
}
}
This system checks each condition entry, skips inactive/untimed evaluations, and increments ValidConditionCount if valid. It does not transition directly. Transition triggering happens later in TransitionEvaluationSystem.
How It Connects to Transition Flow
For ECS conditions, the flow is:
- Condition system checks runtime data.
- True conditions increment ValidConditionCount.
- TransitionEvaluationSystem compares counts (All vs Any) and triggers transitions.
- Cleanup systems reset ValidConditionCount each tick for the next evaluation cycle.
So your ECS condition should only report validity. It should not directly perform transition switching.