The workhorse of game AI: an agent is always in one state (patrol, chase, attack) and switches on events. FSMs are simple, readable, and power the majority of enemy behavior in shipped games.
A finite state machine models an agent as being in exactly one state at a time, with defined transitions between them triggered by conditions. An enemy might Patrol until it sees the player (→ Chase), close in until in range (→ Attack), or lose sight (→ Patrol). This maps directly to how designers think about behavior, which is why FSMs remain the most common game-AI tool.
A guard's FSM — one active state, transitions on conditions:
[PATROL] --sees player--> [CHASE] --in range--> [ATTACK]
^ | |
| | lost player | player fled
+----- lost player -------+----------------------+
+----------- (returns to patrol) ----------------+
At any moment the guard is in ONE state. Each state has:
- behavior to run while in it (walk a route, move toward player, swing)
- transitions: "if <condition> go to <state>"
Simple, readable, and how most shipped enemy AI actually works.A clean FSM implementation gives each state an update function and a way to request transitions. The machine calls the current state’s update each frame and switches when asked, optionally running enter/exit hooks. Keeping states small and transitions explicit makes the behavior easy to read, extend, and debug — the practical payoff of the pattern.
class StateMachine:
def __init__(self, start):
self.state = start
self.states = {}
def add(self, name, update_fn):
self.states[name] = update_fn
def update(self, agent):
# run the current state; it may return a new state name to switch to
next_state = self.states[self.state](agent)
if next_state and next_state != self.state:
self.state = next_state # transition
def patrol(agent):
agent.walk_route()
if agent.can_see(agent.player): return "chase"
def chase(agent):
agent.move_toward(agent.player)
if agent.in_attack_range(): return "attack"
if not agent.can_see(agent.player): return "patrol"
fsm = StateMachine("patrol"); fsm.add("patrol", patrol); fsm.add("chase", chase)FSMs are great until the behavior gets rich: every new state may need transitions to and from many others, and the transition count explodes quadratically into an unreadable tangle (the "spaghetti state machine"). Hierarchical state machines help by nesting states, but past a certain complexity you switch to behavior trees, which scale far better — the subject of the next lesson.
The FSM scaling problem:
3 states -> up to 6 transitions (manageable)
10 states -> up to 90 transitions (a tangled mess: "spaghetti FSM")
Every new state may need transitions TO and FROM many existing states ->
the wiring grows ~quadratically and becomes unreadable + unmaintainable.
Mitigations:
- HIERARCHICAL FSM: group states (e.g. a "Combat" super-state containing
Attack/Dodge/Reload) to cut cross-transitions
- past ~a dozen states, move to BEHAVIOR TREES (next) — they scale better.State machines are not just for combat AI — they model any system with distinct modes: a game’s overall flow (menu → playing → paused → game over), an animation controller (idle/run/jump blends), a door (closed/opening/open/closing), a quest’s progression. Recognizing "this thing has a few clear modes with rules to switch between them" is the cue to reach for an FSM anywhere in your game.
FSMs model anything with distinct MODES + switching rules:
game flow Menu -> Playing -> Paused -> GameOver
animation Idle <-> Run <-> Jump (an "animation state machine")
a door Closed -> Opening -> Open -> Closing
a quest NotStarted -> Active -> Completed
a turn PlayerTurn -> EnemyTurn -> Resolve
Whenever you catch yourself writing lots of boolean flags (isPaused,
isJumping, isDead) that must stay consistent -> that's usually one FSM.