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
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:
| Moment | Runs | Example |
|---|---|---|
| Enter | Once, when switching in | Play the "alert!" sound, start the attack animation |
| Update | Every frame while in it | Move toward the player |
| Exit | Once, when switching out | Stop 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
- Changing
state = ...in many places, so enter/exit code gets skipped. Always go throughChangeState. - Using the same number to enter and leave a state, causing flickering.
- Keeping the old true/false flags alongside the state. Delete them.
- Jumping straight to Version 2 for a simple enemy.
See also
This page is a simplified, AI-assisted interpretation. For the complete and authoritative reference, see the official documentation.