Skip to content

Instantly share code, notes, and snippets.

@nicloay
Last active June 1, 2023 19:35
Show Gist options
  • Save nicloay/048f5970e20d870860bd6928c522004d to your computer and use it in GitHub Desktop.
Save nicloay/048f5970e20d870860bd6928c522004d to your computer and use it in GitHub Desktop.
Unity ECS stateMachine

Unity FSM (finite state machine) implemented on ECS

image

This state machine represent the idea

  1. There are few guardians which allow to jump from source state to target state. Please see Template file, they are defined ast GUARD_** constants
  2. Attack consist of few phases (preparation timer -> apply damage -> post timer to finish animations)
  3. Cooldown is weapon property, next attack is possible only when timer put the tag that cooldown is complete
  4. All jobs run sequentially one by one with parallelism (one job can utilise several threads)

cs file show the result of the template generation. the main logic, e.g. timer, or proximity check, is part of the external scope, basically another system would be responsible to run the tests and put appropriate tags, or calculate targets.

json representation looks like this (I struggled to connect json and custom classes to the template, so decided for the first iteration to omit this functionality), but for information json can be also descriptive as the screenshot above

using CyberSnake.ECS.AI.EnemyProperties;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
namespace CyberSnake.ECS.AI.Armadillo.FSM
{
// --------states-------
public struct IdleState : IComponentData, IEnableableComponent {}
public struct MoveTowardsPlayerState : IComponentData, IEnableableComponent {}
public struct BeginAttackState : IComponentData, IEnableableComponent {}
public struct ApplyDamageState : IComponentData, IEnableableComponent {}
public struct EndAttackState : IComponentData, IEnableableComponent {}
// ---------guards------
public struct AttackOnCooldownGuard : IComponentData{}
public struct AttackPreparationDoneGuard : IComponentData{}
public struct AttackFinishedGuard : IComponentData{}
// --------main system-----
public partial struct ArmadilloFSMSystem : ISystem
{
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<EndSimulationEntityCommandBufferSystem.Singleton>();
}
public void OnUpdate(ref SystemState state)
{
var lookupIdleState = SystemAPI.GetComponentLookup<IdleState>();
var lookupMoveTowardsPlayerState = SystemAPI.GetComponentLookup<MoveTowardsPlayerState>();
var lookupBeginAttackState = SystemAPI.GetComponentLookup<BeginAttackState>();
var lookupApplyDamageState = SystemAPI.GetComponentLookup<ApplyDamageState>();
var lookupEndAttackState = SystemAPI.GetComponentLookup<EndAttackState>();
var IdleState_to_MoveTowardsPlayerState = new IdleStateToMoveTowardsPlayerState()
{
LookupIdleState = lookupIdleState,
LookupMoveTowardsPlayerState = lookupMoveTowardsPlayerState
}.ScheduleParallel(default(JobHandle));
var IdleState_to_BeginAttackState = new IdleStateToBeginAttackState()
{
LookupIdleState = lookupIdleState,
LookupBeginAttackState = lookupBeginAttackState
}.ScheduleParallel(IdleState_to_MoveTowardsPlayerState);
var MoveTowardsPlayerState_to_IdleState = new MoveTowardsPlayerStateToIdleState()
{
LookupMoveTowardsPlayerState = lookupMoveTowardsPlayerState,
LookupIdleState = lookupIdleState
}.ScheduleParallel(IdleState_to_BeginAttackState);
var MoveTowardsPlayerState_to_BeginAttackState = new MoveTowardsPlayerStateToBeginAttackState()
{
LookupMoveTowardsPlayerState = lookupMoveTowardsPlayerState,
LookupBeginAttackState = lookupBeginAttackState
}.ScheduleParallel(MoveTowardsPlayerState_to_IdleState);
var BeginAttackState_to_ApplyDamageState = new BeginAttackStateToApplyDamageState()
{
LookupBeginAttackState = lookupBeginAttackState,
LookupApplyDamageState = lookupApplyDamageState
}.ScheduleParallel(MoveTowardsPlayerState_to_BeginAttackState);
var ApplyDamageState_to_EndAttackState = new ApplyDamageStateToEndAttackState()
{
LookupApplyDamageState = lookupApplyDamageState,
LookupEndAttackState = lookupEndAttackState
}.ScheduleParallel(BeginAttackState_to_ApplyDamageState);
var EndAttackState_to_IdleState = new EndAttackStateToIdleState()
{
LookupEndAttackState = lookupEndAttackState,
LookupIdleState = lookupIdleState
}.ScheduleParallel(ApplyDamageState_to_EndAttackState);
var EndAttackState_to_MoveTowardsPlayerState = new EndAttackStateToMoveTowardsPlayerState()
{
LookupEndAttackState = lookupEndAttackState,
LookupMoveTowardsPlayerState = lookupMoveTowardsPlayerState
}.ScheduleParallel(EndAttackState_to_IdleState);
var EndAttackState_to_BeginAttackState = new EndAttackStateToBeginAttackState()
{
LookupEndAttackState = lookupEndAttackState,
LookupBeginAttackState = lookupBeginAttackState
}.ScheduleParallel(EndAttackState_to_MoveTowardsPlayerState);
EndAttackState_to_BeginAttackState.Complete();
}
// -------jobs---------
[BurstCompile]
[WithAll(typeof(IdleState))]
[WithNone(typeof(TargetInAttackRange))]
public partial struct IdleStateToMoveTowardsPlayerState : IJobEntity
{
[NativeDisableParallelForRestriction] public ComponentLookup<IdleState> LookupIdleState;
[NativeDisableParallelForRestriction] public ComponentLookup<MoveTowardsPlayerState> LookupMoveTowardsPlayerState;
void Execute(Entity entity)
{
UnityEngine.Debug.Log("new state: IdleStateToMoveTowardsPlayerState");
LookupIdleState.SetComponentEnabled(entity, false);
LookupMoveTowardsPlayerState.SetComponentEnabled(entity, true);
}
}
[BurstCompile]
[WithAll(typeof(IdleState),typeof(TargetInAttackRange))]
[WithNone(typeof(AttackOnCooldownGuard))]
public partial struct IdleStateToBeginAttackState : IJobEntity
{
[NativeDisableParallelForRestriction] public ComponentLookup<IdleState> LookupIdleState;
[NativeDisableParallelForRestriction] public ComponentLookup<BeginAttackState> LookupBeginAttackState;
void Execute(Entity entity)
{
UnityEngine.Debug.Log("new state: IdleStateToBeginAttackState");
LookupIdleState.SetComponentEnabled(entity, false);
LookupBeginAttackState.SetComponentEnabled(entity, true);
}
}
[BurstCompile]
[WithAll(typeof(MoveTowardsPlayerState),typeof(TargetInAttackRange),typeof(AttackOnCooldownGuard))]
public partial struct MoveTowardsPlayerStateToIdleState : IJobEntity
{
[NativeDisableParallelForRestriction] public ComponentLookup<MoveTowardsPlayerState> LookupMoveTowardsPlayerState;
[NativeDisableParallelForRestriction] public ComponentLookup<IdleState> LookupIdleState;
void Execute(Entity entity)
{
UnityEngine.Debug.Log("new state: MoveTowardsPlayerStateToIdleState");
LookupMoveTowardsPlayerState.SetComponentEnabled(entity, false);
LookupIdleState.SetComponentEnabled(entity, true);
}
}
[BurstCompile]
[WithAll(typeof(MoveTowardsPlayerState),typeof(TargetInAttackRange))]
[WithNone(typeof(AttackOnCooldownGuard))]
public partial struct MoveTowardsPlayerStateToBeginAttackState : IJobEntity
{
[NativeDisableParallelForRestriction] public ComponentLookup<MoveTowardsPlayerState> LookupMoveTowardsPlayerState;
[NativeDisableParallelForRestriction] public ComponentLookup<BeginAttackState> LookupBeginAttackState;
void Execute(Entity entity)
{
UnityEngine.Debug.Log("new state: MoveTowardsPlayerStateToBeginAttackState");
LookupMoveTowardsPlayerState.SetComponentEnabled(entity, false);
LookupBeginAttackState.SetComponentEnabled(entity, true);
}
}
[BurstCompile]
[WithAll(typeof(BeginAttackState),typeof(AttackPreparationDoneGuard))]
public partial struct BeginAttackStateToApplyDamageState : IJobEntity
{
[NativeDisableParallelForRestriction] public ComponentLookup<BeginAttackState> LookupBeginAttackState;
[NativeDisableParallelForRestriction] public ComponentLookup<ApplyDamageState> LookupApplyDamageState;
void Execute(Entity entity)
{
UnityEngine.Debug.Log("new state: BeginAttackStateToApplyDamageState");
LookupBeginAttackState.SetComponentEnabled(entity, false);
LookupApplyDamageState.SetComponentEnabled(entity, true);
}
}
[BurstCompile]
[WithAll(typeof(ApplyDamageState))]
public partial struct ApplyDamageStateToEndAttackState : IJobEntity
{
[NativeDisableParallelForRestriction] public ComponentLookup<ApplyDamageState> LookupApplyDamageState;
[NativeDisableParallelForRestriction] public ComponentLookup<EndAttackState> LookupEndAttackState;
void Execute(Entity entity)
{
UnityEngine.Debug.Log("new state: ApplyDamageStateToEndAttackState");
LookupApplyDamageState.SetComponentEnabled(entity, false);
LookupEndAttackState.SetComponentEnabled(entity, true);
}
}
[BurstCompile]
[WithAll(typeof(EndAttackState),typeof(AttackFinishedGuard),typeof(TargetInAttackRange),typeof(AttackOnCooldownGuard))]
public partial struct EndAttackStateToIdleState : IJobEntity
{
[NativeDisableParallelForRestriction] public ComponentLookup<EndAttackState> LookupEndAttackState;
[NativeDisableParallelForRestriction] public ComponentLookup<IdleState> LookupIdleState;
void Execute(Entity entity)
{
UnityEngine.Debug.Log("new state: EndAttackStateToIdleState");
LookupEndAttackState.SetComponentEnabled(entity, false);
LookupIdleState.SetComponentEnabled(entity, true);
}
}
[BurstCompile]
[WithAll(typeof(EndAttackState),typeof(AttackFinishedGuard))]
[WithNone(typeof(TargetInAttackRange))]
public partial struct EndAttackStateToMoveTowardsPlayerState : IJobEntity
{
[NativeDisableParallelForRestriction] public ComponentLookup<EndAttackState> LookupEndAttackState;
[NativeDisableParallelForRestriction] public ComponentLookup<MoveTowardsPlayerState> LookupMoveTowardsPlayerState;
void Execute(Entity entity)
{
UnityEngine.Debug.Log("new state: EndAttackStateToMoveTowardsPlayerState");
LookupEndAttackState.SetComponentEnabled(entity, false);
LookupMoveTowardsPlayerState.SetComponentEnabled(entity, true);
}
}
[BurstCompile]
[WithAll(typeof(EndAttackState),typeof(AttackFinishedGuard),typeof(TargetInAttackRange))]
[WithNone(typeof(AttackOnCooldownGuard))]
public partial struct EndAttackStateToBeginAttackState : IJobEntity
{
[NativeDisableParallelForRestriction] public ComponentLookup<EndAttackState> LookupEndAttackState;
[NativeDisableParallelForRestriction] public ComponentLookup<BeginAttackState> LookupBeginAttackState;
void Execute(Entity entity)
{
UnityEngine.Debug.Log("new state: EndAttackStateToBeginAttackState");
LookupEndAttackState.SetComponentEnabled(entity, false);
LookupBeginAttackState.SetComponentEnabled(entity, true);
}
}
// --------authroing util-------
// add all states to the entity in disabled state except first one
public static void AddStatesTo(IBaker baker, Entity entity)
{
baker.AddComponent<IdleState>(entity);
baker.SetComponentEnabled<IdleState>(entity, true);
baker.AddComponent<MoveTowardsPlayerState>(entity);
baker.SetComponentEnabled<MoveTowardsPlayerState>(entity, false);
baker.AddComponent<BeginAttackState>(entity);
baker.SetComponentEnabled<BeginAttackState>(entity, false);
baker.AddComponent<ApplyDamageState>(entity);
baker.SetComponentEnabled<ApplyDamageState>(entity, false);
baker.AddComponent<EndAttackState>(entity);
baker.SetComponentEnabled<EndAttackState>(entity, false);
}
}
}
<#@ template language="C#" debug="true"#>
<#@ output extension=".cs" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Linq" #>
<#
const bool DEBUG_LOG = true;
const string STATE_IDLE = "IdleState";
const string STATE_MOVE_TOWARDS_PLAYER = "MoveTowardsPlayerState";
const string STATE_BEGIN_ATTACK = "BeginAttackState";
const string STATE_APPLY_DAMAGE = "ApplyDamageState";
const string STATE_END_ATTACK = "EndAttackState";
var states = new[]
{ STATE_IDLE, STATE_MOVE_TOWARDS_PLAYER, STATE_BEGIN_ATTACK, STATE_APPLY_DAMAGE, STATE_END_ATTACK };
const string GUARD_TARGET_IN_ATTACK_RANGE = "TargetInAttackRange";
const string GUARD_ATTACK_ON_COOLDOWN = "AttackOnCooldownGuard";
const string GUARD_ATTACK_PREPARATION_DONE = "AttackPreparationDoneGuard";
const string GUARD_ATTACK_FINISHED = "AttackFinishedGuard";
var guards = new[] // list of autogenerated components
{ /*GUARD_TARGET_IN_ATTACK_RANGE,*/ GUARD_ATTACK_ON_COOLDOWN, GUARD_ATTACK_PREPARATION_DONE, GUARD_ATTACK_FINISHED };
const string NOT = "!";
var transitions = new Dictionary<string, Dictionary<string, string[]>>
{
{
STATE_IDLE, new Dictionary<string, string[]>
{
{ STATE_MOVE_TOWARDS_PLAYER, new[] { NOT + GUARD_TARGET_IN_ATTACK_RANGE } },
{ STATE_BEGIN_ATTACK, new[] { GUARD_TARGET_IN_ATTACK_RANGE, NOT + GUARD_ATTACK_ON_COOLDOWN } }
}
},
{
STATE_MOVE_TOWARDS_PLAYER, new Dictionary<string, string[]>
{
{ STATE_IDLE, new[] { GUARD_TARGET_IN_ATTACK_RANGE, GUARD_ATTACK_ON_COOLDOWN } },
{ STATE_BEGIN_ATTACK, new[] { GUARD_TARGET_IN_ATTACK_RANGE, NOT + GUARD_ATTACK_ON_COOLDOWN } }
}
},
{
STATE_BEGIN_ATTACK, new Dictionary<string, string[]>
{
{ STATE_APPLY_DAMAGE, new[] { GUARD_ATTACK_PREPARATION_DONE } }
}
},
{
STATE_APPLY_DAMAGE, new Dictionary<string, string[]>
{
{ STATE_END_ATTACK, Array.Empty<string>() }
}
},
{
STATE_END_ATTACK, new Dictionary<string, string[]>
{
{ STATE_IDLE, new[] { GUARD_ATTACK_FINISHED, GUARD_TARGET_IN_ATTACK_RANGE, GUARD_ATTACK_ON_COOLDOWN } },
{ STATE_MOVE_TOWARDS_PLAYER, new[] { GUARD_ATTACK_FINISHED, NOT + GUARD_TARGET_IN_ATTACK_RANGE } },
{ STATE_BEGIN_ATTACK, new[] { GUARD_ATTACK_FINISHED, GUARD_TARGET_IN_ATTACK_RANGE, NOT + GUARD_ATTACK_ON_COOLDOWN }
}
}
}
};
#>
using CyberSnake.ECS.AI.EnemyProperties;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
namespace CyberSnake.ECS.AI.Armadillo.FSM
{
// --------states-------
<# foreach (var state in states) { #>
public struct <#= state #> : IComponentData, IEnableableComponent {}
<# } #>
// ---------guards------
<# foreach (var guard in guards) { #>
public struct <#= guard #> : IComponentData{}
<# } #>
// --------main system-----
public partial struct ArmadilloFSMSystem : ISystem
{
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<EndSimulationEntityCommandBufferSystem.Singleton>();
}
public void OnUpdate(ref SystemState state)
{
<# foreach (var state in states) { #>
var lookup<#=state #> = SystemAPI.GetComponentLookup<<#=state #>>();
<# } #>
<#
var previousJobHandleName = "default(JobHandle)";
foreach (var outer in transitions)
{
var transitionFrom = outer.Key;
foreach (var inner in outer.Value)
{
var transitionTo = inner.Key;
var handleName = transitionFrom +"_to_"+ transitionTo;
var jobName = transitionFrom +"To"+ transitionTo;
#>
var <#=handleName #> = new <#=jobName#>()
{
Lookup<#=transitionFrom #> = lookup<#=transitionFrom#>,
Lookup<#=transitionTo #> = lookup<#=transitionTo#>
}.ScheduleParallel(<#=previousJobHandleName #>);
<#
previousJobHandleName = handleName;
}
}
#>
<#=previousJobHandleName#>.Complete();
}
// -------jobs---------
<#
foreach (var outer in transitions)
{
var transitionFrom = outer.Key;
foreach (var inner in outer.Value)
{
var transitionTo = inner.Key;
var jobName = transitionFrom +"To"+ transitionTo;
var activeGuards = inner.Value;
var conditionsOn = new []{transitionFrom}.Concat(activeGuards.Where(value => !value.StartsWith(NOT))).Select(value => $"typeof({value})").ToArray();
var withAll = string.Join(",", conditionsOn);
var conditionsOff = activeGuards.Where(value => value.StartsWith(NOT)).Select(value => $"typeof({value.Substring(1)})").ToArray();
#>
[BurstCompile]
[WithAll(<#=withAll #>)]
<# if (conditionsOff.Any()) {
var withNone = string.Join(",", conditionsOff);
#>
[WithNone(<#=withNone #>)]
<#}#>
public partial struct <#=jobName #> : IJobEntity
{
[NativeDisableParallelForRestriction] public ComponentLookup<<#=transitionFrom#>> Lookup<#=transitionFrom#>;
[NativeDisableParallelForRestriction] public ComponentLookup<<#=transitionTo#>> Lookup<#=transitionTo#>;
void Execute(Entity entity)
{
<# if (DEBUG_LOG) {#>
UnityEngine.Debug.Log("new state: <#=jobName #>");
<#}#>
Lookup<#=transitionFrom#>.SetComponentEnabled(entity, false);
Lookup<#=transitionTo#>.SetComponentEnabled(entity, true);
}
}
<#
}
}
#>
// --------authroing util-------
// add all states to the entity in disabled state except first one
public static void AddStatesTo(IBaker baker, Entity entity)
{
<# for (var i = 0; i < states.Length; i++)
{
var stateName = states[i];
var isEnabled = i == 0 ? "true" : "false";
#>
baker.AddComponent<<#=stateName#>>(entity);
baker.SetComponentEnabled<<#=stateName#>>(entity, <#=isEnabled #>);
<#}#>
}
}
}
{
"states" : ["Idle", "MoveTowardsPlayer", "BeginAttack", "ApplyDamage", "EndAttack"],
"guards" : ["TargetInAttackRange", "AttackOnCoolDown", "AttackPreparationDone", "AttackFinished"],
"transitions": {
"Idle" : {
"MoveTowardsPlayer" : ["!TargetInAttackRange"],
"BeginAttack" : ["TargetInAttackRange", "!AttackOnCoolDown"]
},
"MoveTowardsPlayer": {
"Idle" : ["TargetInAttackRange", "AttackOnCoolDown"],
"BeginAttack" : ["TargetInAttackRange", "!AttackOnCoolDown"]
},
"BeginAttack": {
"ApplyDamage" : ["AttackPreparationDone"]
},
"ApplyDamage": {
"EndAttack" : []
},
"EndAttack": {
"Idle": ["AttackFinished", "TargetInAttackRange", "AttackOnCoolDown"],
"MoveTowardsPlayer" : ["AttackFinished","!TargetInAttackRange"],
"BeginAttack" : ["AttackFinished", "TargetInAttackRange", "!AttackOnCoolDown"]
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment