Simple State Machines

Stop your enemy code from turning into a mess of if statements. Give it clear states like Patrol, Chase and Attack.

A state machine is like a traffic light. It's always in exactly one state: red, amber or green. Each state has its own behaviour, and there are clear rules for switching ("after 30 seconds, go from green to amber"). It can never be red and green at the same time.

The problem

Enemy code often starts like this and gets worse with every feature:

void Update()
{
    if (isDead) return;
    if (canSeePlayer && !isAttacking && distance < 2 && !isStunned) { ... }
    else if (canSeePlayer && !isAttacking && !isStunned) { ... }
    else if (!canSeePlayer && wasChasing) { ... }
    // ...and 50 more lines of true/false flags
}

Lots of true/false flags mean lots of combinations, and some of them make no sense ("attacking while dead"). A state machine replaces the flags with one variable: the current state.

Draw it first

PatrolChaseAttacksees playerloses playerclose enoughtoo far
Boxes are states, arrows are the rules for switching. Sketch this on paper before writing code.

Version 1: an enum and a switch

This is enough for most enemies. Uses a NavMeshAgent to move.

using UnityEngine;
using UnityEngine.AI;

public class EnemyBrain : MonoBehaviour
{
    private enum State { Patrol, Chase, Attack }

    [SerializeField] private Transform player;
    [SerializeField] private Transform[] patrolPoints;
    [SerializeField] private float seeRange = 10f;
    [SerializeField] private float attackRange = 2f;

    private State state = State.Patrol;
    private NavMeshAgent agent;
    private int patrolIndex;

    void Awake() => agent = GetComponent<NavMeshAgent>();

    void Update()
    {
        float distance = Vector3.Distance(transform.position, player.position);

        switch (state)
        {
            case State.Patrol:
                if (!agent.pathPending && agent.remainingDistance < 0.5f)
                {
                    patrolIndex = (patrolIndex + 1) % patrolPoints.Length;
                    agent.SetDestination(patrolPoints[patrolIndex].position);
                }
                if (distance < seeRange) ChangeState(State.Chase);
                break;

            case State.Chase:
                agent.SetDestination(player.position);
                if (distance < attackRange) ChangeState(State.Attack);
                else if (distance > seeRange * 1.5f) ChangeState(State.Patrol);
                break;

            case State.Attack:
                transform.LookAt(new Vector3(player.position.x, transform.position.y, player.position.z));
                // swing the sword, play the animation, etc.
                if (distance > attackRange * 1.2f) ChangeState(State.Chase);
                break;
        }
    }

    private void ChangeState(State next)
    {
        // "Exit" the old state
        if (state == State.Attack) agent.isStopped = false;

        state = next;

        // "Enter" the new state
        if (state == State.Attack) agent.isStopped = true;
        if (state == State.Patrol) agent.SetDestination(patrolPoints[patrolIndex].position);
        Debug.Log($"{name} is now {state}");
    }
}

Why seeRange * 1.5f? If "start chasing" and "stop chasing" used the same distance, an enemy standing right on the line would flicker between states every frame. A small gap between the two numbers stops that.

Enter, Update, Exit

Every state usually needs three moments:

MomentRunsExample
EnterOnce, when switching inPlay the "alert!" sound, start the attack animation
UpdateEvery frame while in itMove toward the player
ExitOnce, when switching outStop the attack animation

Putting all switching through one ChangeState method gives you one place for enter/exit code, and one place to add a Debug.Log when something goes wrong.

Version 2: one class per state

When states get big (a boss with 8 phases), put each one in its own class:

public interface IState
{
    void Enter();
    void Tick();   // called every frame
    void Exit();
}

public class StateMachine
{
    public IState Current { get; private set; }

    public void ChangeState(IState next)
    {
        Current?.Exit();
        Current = next;
        Current.Enter();
    }

    public void Tick() => Current?.Tick();
}
public class ChaseState : IState
{
    private readonly EnemyController enemy;
    public ChaseState(EnemyController enemy) => this.enemy = enemy;

    public void Enter() => enemy.PlaySound("alert");
    public void Tick()
    {
        enemy.MoveTowardPlayer();
        if (enemy.PlayerInAttackRange) enemy.Brain.ChangeState(enemy.AttackState);
    }
    public void Exit() { }
}

The enemy's MonoBehaviour creates the states once in Awake, calls brain.ChangeState(patrolState) to begin, and calls brain.Tick() in Update. Start with Version 1; switch to Version 2 only when the switch statement becomes hard to read.

What about the Animator?

The Animator window is also a state machine, but for animations. Keep game logic (what the enemy decides) in your code, and just tell the Animator what to show: animator.SetBool("Attacking", true).

Common Mistakes

See also