Summary

The AI state machine pattern applies the Gang of Four State design pattern to NPC/agent behaviour. Each behaviour (Idle, Attack, Flee, Patrol) is encapsulated in its own class. A central StateMachine holds a reference to the current state and delegates to it — the current state runs its logic and decides whether to transition to another state. This gives you a modular, testable, extendable AI architecture. It remains one of the core teaching architectures in classic game-AI texts. (Millington, Artificial Intelligence for Games, see source-artificial-intelligence-for-games; Buckland, Programming Game AI by Example, see source-programming-game-ai-by-example)

(Prof Charles, CRE341 Wk 3.r1, see source-cre341-lectures)


Why not a switch statement?

The naive approach — a large switch on a currentState enum — works until you need to add states. Adding a new state means editing the switch, risking regressions in existing branches. The State pattern instead puts each behaviour in its own class: new states are new classes, not new branches.

ApproachAdding a stateTesting a stateTransitions defined by
switch on enumModify existing switchHard to isolateCentralised in switch
State patternNew class, no edits to machineEach class independentlyThe state class itself

Implementation

The interface

public interface IState
{
    void Enter(StateMachine context);
    void Update(StateMachine context);
    void Exit(StateMachine context);
}

Enter and Exit allow setup/teardown when a state is activated or left — play an animation, reset a timer, etc.

The state machine

public class StateMachine : MonoBehaviour
{
    IState currentState;
 
    public void SetState(IState newState)
    {
        currentState?.Exit(this);
        currentState = newState;
        currentState.Enter(this);
    }
 
    void Update()
    {
        currentState?.Update(this);
    }
}

The machine holds no behaviour itself. It delegates entirely to currentState.

Concrete states — Unity example

public class IdleState : IState
{
    public void Enter(StateMachine ctx)  { /* play idle animation */ }
    public void Exit(StateMachine ctx)   { }
 
    public void Update(StateMachine ctx)
    {
        if (ctx.CanSeeEnemy())
            ctx.SetState(new AttackState());
    }
}
 
public class AttackState : IState
{
    public void Enter(StateMachine ctx)  { /* play attack animation */ }
    public void Exit(StateMachine ctx)   { }
 
    public void Update(StateMachine ctx)
    {
        if (!ctx.CanSeeEnemy())
            ctx.SetState(new IdleState());
        else if (ctx.HealthLow())
            ctx.SetState(new FleeState());
        else
            ctx.AttackTarget();
    }
}
 
public class FleeState : IState
{
    public void Enter(StateMachine ctx)  { /* play flee animation */ }
    public void Exit(StateMachine ctx)   { }
 
    public void Update(StateMachine ctx)
    {
        ctx.MoveAwayFromEnemy();
        if (ctx.IsSafe())
            ctx.SetState(new IdleState());
    }
}

State transition diagram

[Idle] --enemy detected--> [Attack]
[Attack] --enemy lost------> [Idle]
[Attack] --low health------> [Flee]
[Flee] ---safe environment-> [Idle]

Trade-offs

Use this pattern when:

  • The agent has 3+ distinct behaviours with different update logic
  • You expect to add new behaviours during development
  • You want to test individual behaviours in isolation
  • Transitions have meaningful side effects (animation changes, sound cues)

Alternatives to consider:

  • Behaviour Trees (see game-ai-agent-design) — better for complex hierarchical decisions and reactive priority systems; more composable than state machines but heavier to implement
  • Simple enum + switch — acceptable for 2–3 states with simple logic and no anticipated extension
  • Unity Animator Controller — handles animation state machines but is not general AI logic (see unity-animator-scripting)

OOP benefits

The pattern relies on three OOP principles (Prof Charles, CRE341 Wk 3.r1):

PrincipleHow it applies
EncapsulationEach state contains its own behaviour and transition logic
Inheritance / InterfaceAll states implement IState, enforcing a uniform API
PolymorphismThe machine calls currentState.Update() without knowing which state it is

This means states can be:

  • Unit tested independently (inject a mock StateMachine)
  • Added without touching the machine or other states
  • Combined across different game objects with different machine configurations