Skip to main content
Back to module
Game Prototyping & Systems · Mechanics

Game states

Reading38mLesson 3 of 3 in this module

Model menu, play, pause and game-over as a finite state machine with a pure transition function.

Why this matters

A prototype quickly grows more than one "mode": a title screen, the game itself, a pause screen, a game-over screen with a restart button. Without structure this becomes a nest of flags (isPaused, isDead, hasStarted, showMenu) that can contradict each other. Players then find the bug where pressing pause on the game-over screen revives them. A finite state machine (FSM) removes that whole class of bugs by making the modes explicit.

The idea

A finite state machine has:

  • a fixed set of states (exactly one is current),
  • a set of events (things that happen: a key press, a hit, a timer),
  • transitions: rules saying "in state S, event E moves you to state T".

Anything not listed is ignored. For a typical arcade game:

fromeventtoside effect
menuSTARTplayingscore = 0, lives = 3
playingPAUSEpaused
pausedRESUMEplaying
playingHITplaying or gameoverlives - 1; gameover at 0
playingSCOREplayingscore + points
gameoverRESTARTmenu

The table is the design. Write it before any code, and show it to teammates: holes ("what does Escape do on the game-over screen?") become obvious.

A pure transition function

The cleanest implementation is a function that takes the current game and an event and returns the next game, without changing the input:

function createGame() {
  return { state: "menu", score: 0, lives: 3 };
}

function transition(game, event) {
  switch (game.state) {
    case "menu":
      if (event.type === "START") return { state: "playing", score: 0, lives: 3 };
      return game;
    case "playing":
      if (event.type === "PAUSE") return { ...game, state: "paused" };
      if (event.type === "SCORE") return { ...game, score: game.score + event.points };
      if (event.type === "HIT") {
        const lives = game.lives - 1;
        return { ...game, lives, state: lives <= 0 ? "gameover" : "playing" };
      }
      return game;
    // paused and gameover follow the same pattern
    default:
      return game;
  }
}

Events are objects ({ type: "SCORE", points: 10 }) so they can carry data. The spread { ...game, ... } copies the old fields and overrides a few, so the original is untouched.

Why bother with purity?

  • Testing: transition(createGame(), { type: "START" }) is a one-line test with no canvas, timer or keyboard.
  • Replays and debugging: a list of events plus the starting state reproduces any bug exactly.
  • Undo, rewind, networking: keeping old states is free when nothing mutates them.

Ignoring invalid events

Notice the return game; lines. A HIT in the menu, a PAUSE on the game-over screen, a SCORE while paused: all ignored. This is the main safety property of an FSM. Instead of asking "which flags must I check here?", each state lists what it accepts and everything else is a no-op.

Wiring it to the loop

The game loop asks the state what to do:

let game = createGame();

function update(dt) {
  if (game.state === "playing") updateWorld(dt);   // physics only while playing
}

function draw() {
  drawWorld();
  if (game.state === "menu") drawTitle();
  if (game.state === "paused") drawOverlay("Paused");
  if (game.state === "gameover") drawOverlay(`Game over: ${game.score}`);
}

window.addEventListener("keydown", (e) => {
  if (e.key === "Enter") game = transition(game, { type: game.state === "gameover" ? "RESTART" : "START" });
  if (e.key === "Escape") game = transition(game, { type: game.state === "paused" ? "RESUME" : "PAUSE" });
});

Input handlers translate keys into events; the transition function decides what those events mean right now.

When states get bigger

  • Enter/exit actions: run code once when entering a state (play a jingle on game over).
  • Hierarchical states: "playing" can contain sub-states like "alive", "respawning", "level-complete".
  • Per-entity FSMs: enemies use the same pattern: idle, patrol, chase, attack. The NesHacker video shows how classic games used state machines for almost everything.
  • Behaviour trees replace FSMs for complex AI, but an FSM is the right first tool for game flow.

Common mistakes

  • Boolean soup: several flags that can be true at once instead of one state value.
  • Mutating the input in a function meant to be pure, so tests pass or fail depending on order.
  • Forgetting to reset score and lives on START, so a new run keeps the old score.
  • Handling events everywhere: pause logic scattered across input, physics and UI code.
  • Using < instead of <= for lives, so the player gets an extra life at zero.

Quick recap

  • One state value, a fixed list of events, and a transition table.
  • A pure transition(game, event) returns a new game and ignores invalid events.
  • The loop updates and draws based on the current state.
  • Write the table first; it is the design.

Try it

Open the Do tab to implement createGame and a pure transition function for menu, playing, paused and game over.

Also in this lesson: