Game loops and state machines

Play this lesson

The pause that did not pause

Kai and Remy's first Hyper Shards had a strange bug. With the pause menu open, pressing fire still used up laser energy. And pressing pause on the game over screen started the music again.

The code kept a pile of true-or-false switches: isPaused, isGameOver, onTitle. Every function checked some of them and forgot others.

This lesson is about the shape underneath every game: the game loop, and state machines that keep a game in exactly one mode at a time.

The game loop

A normal app waits for you to click. A game cannot wait: aliens keep flying even if you do nothing. So every game runs a game loop, three jobs repeated every frame, many times a second:

Bolt says: In a browser, requestAnimationFrame(loop) asks the browser to run loop just before it next repaints the screen, usually at the screen's refresh rate. It runs only once, so loop asks again at the end of every frame. PixiJS runs a loop like this for us, called the ticker.

Quick check

In one frame of Hyper Shards, the player presses fire. In which order does the loop handle it?

  1. Read the press, create and move the laser, then draw it
  2. Draw the laser, then read the press, then move it
  3. Move the laser, draw it, then read the press a second later
Show the answer

Read the press, create and move the laser, then draw it

Input comes first so the update can react to it in the same frame, and drawing comes last so the picture shows the newest positions. Any other order makes the game feel a frame late.

Game state and drawing, kept apart

Game state is all the data that describes the game right now: positions, health, the score, the wave and which screen is showing. Update changes the state. Render only reads it and paints it, and never changes it.

Keeping them apart pays off. Pausing means "skip update, keep rendering". Saving means saving the state. Update can even be tested with no screen at all.

Update moves things by speed * dt, where dt is the seconds since the last frame (the game math lesson shows why). Some games update in fixed steps of exactly 1/60 of a second, so physics comes out the same on every device.

Bolt says: How long a frame may take, the 16.7 millisecond budget, is in the 60 frames per second lesson.

Finite state machines

A finite state machine (FSM) has a fixed list of states. It is always in exactly one of them, and it moves between them only through set transitions, each started by an event. Hyper Shards' screens form one with four states:

In code, that is one variable: let state = 'title';

One variable beats many switches

Three true-or-false switches make 2 x 2 x 2 = 8 combinations, but only 4 of them make sense. The other 4, like paused and game over at once, are bugs waiting to happen. One state variable can only hold one value, so those impossible mixes can no longer exist.

A state machine also decides which inputs count. While state is 'paused', the code for the fire key is never even asked. Robert Nystrom's book Game Programming Patterns shows the same fix for a hero who could jump again in mid-air.

Bug hunt

game.js
let state = 'title';function update(dt) {  if (input.fire) fireLaser();  if (state === 'playing') {    moveAliens(dt);    moveLasers(dt);  }}function onPauseKey() {  if (state === 'playing') state = 'paused';  else if (state === 'paused') state = 'playing';}

Testers say pressing fire on the pause menu still uses up laser energy. Which line is the bug?

  1. Line 1
  2. Line 3
  3. Line 4
  4. Line 10
Show the answer

Line 3

Line 3 reads the fire key before checking the state, so it runs on every screen. Moving it inside the playing block that starts on line 4 means only the playing state listens for fire.

Characters have states too

The same idea works for one character. Sprig, the hero of Garden Defense, has four states: idle, walk, jump and hurt. Each state picks Sprig's animation, says which buttons count, and says how to leave:

Enemies get state machines too, like a beetle that wanders, chases and runs away. The enemy brains lesson builds on this one.

Read the code

sprig.js
function onJumpPressed() {  const s = sprig.state;  if (s === 'idle' || s === 'walk') {    sprig.state = 'jump';    sprig.vy = -500;  }}function onHit() {  if (sprig.state === 'hurt') return; // safe  sprig.state = 'hurt';  sprig.hurtTimer = 1; // seconds}

A beetle hits Sprig. Half a second later, the player presses jump. What happens?

  1. Nothing: Sprig is in the hurt state, and only idle or walk can start a jump
  2. Sprig jumps, because the jump button always works
  3. Sprig gets hurt a second time, because the beetle is still there
  4. The game crashes, because Sprig is in two states at once
Show the answer

Nothing: Sprig is in the hurt state, and only idle or walk can start a jump

For one second Sprig's state is hurt. onJumpPressed only allows a jump from idle or walk, and onHit returns early while Sprig is hurt. A state variable can never hold two states, so there is nothing to crash.

Systems, modules and ECS

As a game grows, update splits into systems, each doing one job for the whole game every frame. Hyper Shards runs input, movement, collisions, scoring and drawing, each in its own module, a file with one job (see Code like LEGO in Stage 2).

Some games go further with ECS, entity component system. An entity is just an ID number. Components are plain data stuck onto it, like position, velocity or health. A system works on every entity that has the components it needs: movement moves anything with a position and a velocity: ship, laser or shard.

Bolt says: Ask Claude Opus 5.5 to list your game's states and transitions before it writes any code. Next lesson: controls that feel fast and fair, even when the player presses a little early.

Fact-checked 9 October 2026