During the creation of this sample game, I required a DOTS state machine solution for handling my “character movement states”, and the state machine had to have the following characteristics:
Characteristics
everything must be very easy to use & super versatile
states must be able to have several different functions
state functions must be able to be called at different points in the frame
multiple different state functions must be callable at different points during the same job (so changes must be instant, and no “1 job per state” approach)
individual states must be able to store data, and data must be stored in ECS components & be editable in inspector
must not involve structural changes (they can make things complicated and cause all sorts of constraints)
must detect state enter/exit events
state updates must be able to receive & modify any kind of data (ex: get passed a PhysicsWorld so you can raycast from your state update, read/write from ComponentDataFromEntity, etc…)
must be capable of supporting nested state machines
state updates must be parallelizable
I ended up with the solution in the links at the top of this post. All useful info for getting started should be on the Sample project page.
In short, this is how it works:
1. Create the interface that defines your state machine
you create an interface with a special attribute on it: this defines a state machine and all the functions that its states can have
2. Create the structs that define your states
you create several structs that implement the interface from point 1: this defines all the states that your specific state machine can have, as well as their implementation logic
3. Generate the state machine code
you press a button to codegen a statemachine component based on the interface you created in point 1, and the states you defined in point 2. The generated component keeps track of a “CurrentState”, and has ways of calling each of the functions from your interface on any of the states
This sounds great, I probably should use something like this I currently have an unholy mess of If Else statements to control my states so maybe this would help.
use union of states inside StateMachine component so all state will consume the same space in memory and there will be no big memory waste in case of tens of states. Common states data can lie in separate struct. All this will do better memory packing of state machine more entities will be packed into one DOTS chunk, better cache/performance…
there will be good to create HFSM (Hierarchical state machine) e.g. We use this for Attack state in our game, top hierarchy Attack state have substates: shoot, reload, heal, aiming (between each shoot) and only go out of Attack state when lost target
for future ECS where we will have component enabled bit generator can create tag components foreach state, add all of them to entity and only enable tag that correspond to current state (so no structural changes on change state) and that create queries for exact states like if each state is separate IComponentData.
In project late phase this approach can help optimise code. i.e. if you notice that 50% of mobs stay in some roam state and another 50% in all other states you potentially can make that state (only one state) with adding/removing RoamStateTagComponent instead of enable/disable and thus group all mobs in roam state into one set of chunks potentially increasing processing performance (but there will be structrual changes)
Base Idea to generate state machine component really Liked to me, looking forward to future ECS with enable/disable components and C# Source Generators to make all this with common tooling. Hope all this will land with Unity 2021
Wouldn’t this make it impossible to store data in each individual state though? In other words; being able to store data in a state, switching to another state, coming back to the original state and the data is still there
But maybe I’m misunderstanding
I’m almost done creating some kind of “Polymorphic Component Generator” that essentially generates a common union struct for all structs that implement a given interface. It sounds similar to what you’re describing, and it could be used to make a different kind of state machine implementation where individual states are each stored on their own entity, and the state machine simply has a dynamicBuffer
public interface IMyPolyComp
{
void Update(float deltaTime, ref Translation translation, ref Rotation rotation);
}
[Serializable]
[StructLayout(LayoutKind.Explicit, Size = (16 + 4))]
public struct MyPolyComponent : IComponentData
{
public enum CompType
{
CompA,
CompB,
}
[FieldOffset(0)]
public CompA CompA;
[FieldOffset(0)]
public CompB CompB;
[FieldOffset(16)]
public CompType CompTypeId;
public void Update(float deltaTime, ref Translation translation, ref Rotation rotation)
{
switch (CompTypeId)
{
case CompType.CompA:
CompA.Update(deltaTime, ref translation, ref rotation);
break;
case CompType.CompB:
CompB.Update(deltaTime, ref translation, ref rotation);
break;
}
}
}
[Serializable]
public struct CompA : IMyPolyComp
{
public float MoveSpeed;
public float MoveAmplitude;
[HideInInspector]
public float TimeCounter;
public void Update(float deltaTime, ref Translation translation, ref Rotation rotation)
{
TimeCounter += deltaTime;
translation.Value.y = math.sin(TimeCounter * MoveSpeed) * MoveAmplitude;
}
}
[Serializable]
public struct CompB : IMyPolyComp
{
public float RotationSpeed;
public float3 RotationAxis;
public void Update(float deltaTime, ref Translation translation, ref Rotation rotation)
{
rotation.Value = math.mul(rotation.Value, quaternion.AxisAngle(math.normalizesafe(RotationAxis), RotationSpeed * deltaTime));
}
}
public class TestPolyCompSystem : SystemBase
{
protected override void OnUpdate()
{
float deltaTime = Time.DeltaTime;
Entities.ForEach((Entity entity, ref MyPolyComponent polyComp, ref Translation translation, ref Rotation rotation) =>
{
polyComp.Update(deltaTime, ref translation, ref rotation);
}).Schedule();
}
}
You right it is good only in situation when you dont want to persist old state.
In our case we never want to persist state data, end even more, we want to pass specific data into state on transition e.g.
TransitionToAttack( target );
TransitionToRun( point );
etc.
Transition method will fire StateExitForPrevState, clear union data, and setup new state with transition data so new state could operate correctly in new situation.
Behaviour depends on situation and game logic but in our case entering state in future will always consider new world state and initialize FSM state from scratch