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
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
}private void Update()
{
// One state is active. It cannot also be another.
current.Tick();
}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
public interface IState
{
void Enter();
void Tick();
void Exit();
}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();
}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
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
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();
}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.
| Owner | Right for | Note |
|---|---|---|
Code state machine | Gameplay logic and AI | Debuggable, testable, no editor round-trip. |
Animator | Which clip plays, blending | Visual state only. |
Both, code driving | Most games | Code decides, Animator reflects it. |
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.
// 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.