Connecting the DOTS: State machines

What is a state machine? Let's start with the Wikipedia definition:

A finite-state machine (FSM) is an abstract machine that can be in exactly one of a finite number of states at any given time. The FSM can change from one state to another in response to some inputs; the change from one state to another is called a transition. An FSM is defined by a list of its states, its initial state, and the inputs that trigger each transition.

When it comes to game programming, a state machine is a convenient abstraction that has been traditionally used for managing the artificial intelligence of NPCs (even though the past few years have seen a rise in the use of behaviour trees, specially for more complex mechanics). Other common usages include things like managing the flow of menu screens or the transitions between the animations of a 3D model. A state machine provides structure and strong typing to the naive, hard-coded alternative of long switch statements based on an enumerated value. It is only natural to take this abstraction to the next level by providing a visual frontend to the machine that allows both programmers and non-programmers to create states and transitions without typing any code. In the context of Unity, this is used to great effect in the Animator window or a PlayMaker/Bolt script.

How can one create a data-oriented state machine? Traditional implementations of state machines in object-oriented languages generally rely on an abstract base class with virtual OnEnter, OnExit, OnUpdate methods; therefore lying at the very opposite of what data-orientation is all about: that sweet, cache-friendly linear memory access. In practical terms, though, the main benefit that we can obtain with a data-oriented state machine is in the bulk processing of many entities (so, that OnUpdate method); transitions should generally not prove to be much of a bottleneck to begin with unless your game is doing frequent state changes.

For the purposes of this article, we will consider a fictitious game featuring enemies that can be in one of two states: idle and patrol. So the first thing we need to do is to define these states. It is only natural to think of states as components that will get added to (and removed from) our Enemy entities.

using Unity.Entities;

public struct IdleState : IComponentData
{
}
using Unity.Entities;

public struct PatrolState : IComponentData
{
}

In a similar fashion, we can represent transitions as components:

using Unity.Entities;

public struct EnterIdle : IComponentData
{
}
using Unity.Entities;

public struct ExitIdle : IComponentData
{
}
using Unity.Entities;

public struct EnterPatrol : IComponentData
{
}
using Unity.Entities;

public struct ExitPatrol : IComponentData
{
}

Two important aspects about this design are:

  • Changing states means adding and removing components to our entities. This comes at a cost (changing the entity's archetype and moving it to a different chunk) that is worth noting if you are planning on changing states very frequently. I am personally a big fan of tag components because they make existential processing code very elegant and, in most of the code I write, the change of states does not happen so often and the batch processing of the entities without any additional conditional logic within the systems is preferable.
  • An entity can potentially be in several states (which, technically speaking, means this is not a finite state machine anymore) unless strictly enforced. This can be useful to get closer to hierarchical state machines but, if this is an issue for your game's design, it may be preferable to have a State component with a CurrentState enum field. The inconvenient of this approach is that it forces you into a switch inside your system to differentiate between states, whereas tag components naturally provide independent systems for each state.

Thanks to Sigurdur for his feedback on this!

What is left now is creating the systems that will run the state's logic, by filtering over entities with the appropriate combinations of the aforementioned components. The first one will take care of performing the transition into the idle state. As we try to do with many of our systems, we use ScheduleParallel to get as maximum parallelism as possible out of the Entities.ForEach loop, which requires any structural changes to entities to happen in a delayed fashion through a barrier system. The EndSimulation barrier system is a good default, as it happens at the end of the frame.

using Unity.Entities;

[UpdateInGroup(typeof(EnterStateSystemGroup))]
public class EnterIdleStateSystem : SystemBase
{
    private EndSimulationEntityCommandBufferSystem ecbSystem;

    protected override void OnCreate()
    {
        ecbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    protected override void OnUpdate()
    {
        var ecb = ecbSystem.CreateCommandBuffer().ToConcurrent();

        Entities
            .WithAll<EnterIdle>()
            .WithNone<IdleState>()
            .ForEach((Entity entity, int entityInQueryIndex) =>
            {
                // Insert your game logic here.
                ecb.RemoveComponent<EnterIdle>(entityInQueryIndex, entity);
                ecb.AddComponent<IdleState>(entityInQueryIndex, entity);
            }).ScheduleParallel();

        ecbSystem.AddJobHandleForProducer(Dependency);
    }
}

The second system will take care of performing the idle state's per-frame logic.

using Unity.Entities;

[UpdateInGroup(typeof(UpdateStateSystemGroup))]
public class IdleStateSystem : SystemBase
{
    protected override void OnUpdate()
    {
        Entities
            .WithAll<IdleState>()
            .WithNone<EnterIdle, ExitIdle>()
            .ForEach((Entity e, int entityInQueryIndex) =>
            {
                // Insert your game logic here.
            }).ScheduleParallel();
    }
}

The third and final system will be responsible for performing the transition out of the idle state.

using Unity.Entities;

[UpdateInGroup(typeof(ExitStateSystemGroup))]
public class ExitIdleStateSystem : SystemBase
{
    private EndSimulationEntityCommandBufferSystem ecbSystem;

    protected override void OnCreate()
    {
        ecbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    protected override void OnUpdate()
    {
        var ecb = ecbSystem.CreateCommandBuffer().ToConcurrent();

        Entities
            .WithAll<IdleState, ExitIdle>()
            .ForEach((Entity entity, int entityInQueryIndex) =>
            {
                // Insert your game logic here.
                ecb.RemoveComponent<ExitIdle>(entityInQueryIndex, entity);
                ecb.RemoveComponent<IdleState>(entityInQueryIndex, entity);
            }).ScheduleParallel();

        ecbSystem.AddJobHandleForProducer(Dependency);
    }
}

An important thing to note is that an extra call to Dependency.Complete() might be required here if you need the Exit logic to always happen before the Enter logic. Generally speaking, you want to go wide and reduce sync points as much as possible for performance reasons, but ordering requirements and dependencies are sometimes unavoidable.

You would define equivalent systems for the Patrol state.

Note how all these systems belong to custom system groups. This provides a clear ordering between them and a convenient visual arrangement within Unity's Entity debugger window:

using Unity.Entities;

public class EnterStateSystemGroup : ComponentSystemGroup
{
}
 using Unity.Entities;

[UpdateAfter(typeof(EnterStateSystemGroup))]
[UpdateBefore(typeof(ExitStateSystemGroup))]
public class UpdateStateSystemGroup : ComponentSystemGroup
{
}
using Unity.Entities;

public class ExitStateSystemGroup : ComponentSystemGroup
{
}

This is a very simple starting point to data-oriented state machines that you can further expand upon as needed in your own games.