Topic 14 of 15 12 min intermediate

State Machines

Taming "is jumping and also dead"

Every character starts with one bool. Then isJumping, isAttacking, isDashing, isStunned, isDead. Five bools is 32 combinations, most of which are nonsense, and the bug reports are always about the nonsense ones.

The problem, concretely

C#
private void Update()
{
    // Every new ability adds a clause to every other ability
    if (!isDead && !isStunned && !isAttacking && canMove)
        Move();

    if (!isDead && !isStunned && !isDashing && isGrounded)
        TryJump();

    // ...and the bug is always the combination you forgot
}

A state machine enforces the thing the bools only imply: you are in exactly one state at a time, and transitions between them are explicit.

A minimal state machine

IState.cs
C#
public interface IState
{
    void Enter();
    void Tick();
    void Exit();
}
StateMachine.cs
C#
public class StateMachine
{
    public IState Current { get; private set; }

    public void ChangeTo(IState next)
    {
        if (next == null || next == Current) return;

        Current?.Exit();
        Current = next;
        Current.Enter();
    }

    public void Tick() => Current?.Tick();
}
The next == Current guard matters more than it looks. Without it, re-entering the same state fires Exit then Enter every frame, which restarts animations and replays sounds.

A state

ChaseState.cs
C#
public class ChaseState : IState
{
    private readonly Enemy enemy;

    public ChaseState(Enemy enemy) => this.enemy = enemy;

    public void Enter()
    {
        enemy.Animator.Play("Run");
        enemy.Agent.isStopped = false;
    }

    public void Tick()
    {
        enemy.Agent.SetDestination(enemy.Target.position);

        if (enemy.DistanceToTarget > enemy.GiveUpDistance)
            enemy.Machine.ChangeTo(enemy.PatrolState);
        else if (enemy.DistanceToTarget < enemy.AttackRange)
            enemy.Machine.ChangeTo(enemy.AttackState);
    }

    public void Exit() => enemy.Agent.isStopped = true;
}

Notice that the state decides its own exits. Each state only needs to know the handful of states it can lead to, which is why adding a sixth state does not touch the other five.

Wiring it up

C#
public class Enemy : MonoBehaviour
{
    public StateMachine Machine { get; private set; }
    public IState PatrolState { get; private set; }
    public IState ChaseState { get; private set; }
    public IState AttackState { get; private set; }

    private void Awake()
    {
        Machine = new StateMachine();
        PatrolState = new PatrolState(this);
        ChaseState  = new ChaseState(this);
        AttackState = new AttackState(this);
    }

    private void Start() => Machine.ChangeTo(PatrolState);

    private void Update() => Machine.Tick();
}
States are created in Awake and reused, not allocated per transition. Creating a new state object on every change would allocate constantly and trigger GC spikes.

Code or Animator?

Unity's Animator is itself a state machine, and it is tempting to let it own gameplay state. Usually that is the wrong call.

OwnerRight forNote
Code state machineGameplay logic and AIDebuggable, testable, no editor round-trip.
AnimatorWhich clip plays, blendingVisual state only.
Both, code drivingMost gamesCode decides, Animator reflects it.
Putting gameplay decisions in Animator transitions means your logic lives in a graph you cannot diff, cannot unit test, and cannot read in a pull request. Let code decide and tell the Animator what happened.

Debugging

Always expose the current state

Half of state machine debugging is knowing which state you are actually in. A read-only field in the Inspector removes the guessing entirely.

C#
// Visible in the Inspector while playing
[SerializeField, ReadOnly] private string currentState;

private void Update()
{
    Machine.Tick();
    currentState = Machine.Current?.GetType().Name ?? "none";
}

Logging every transition is also worth it while building. If a state flickers between two values every frame, you have two states each pushing back to the other.

What to take away

  • Boolean flags multiply; states do not.
  • Enter / Tick / Exit is enough structure for most games.
  • Guard against re-entering the current state.
  • Let each state own its own exit conditions.
  • Create states once in Awake, never per transition.
  • Code owns gameplay state; the Animator only reflects it.