Skip to content

Game State Machine

Organise game flow with distinct states (title, playing, game over). Clean separation of game phases.

Code status: illustrative pseudocode. Adapt the listing to your program and check it on your target machine. This page links no assembly or execution record for the listing itself.

Overview

A state machine keeps your game organised by separating distinct phases — title screen, gameplay, pause, game over — into independent states. Each state handles its own input, updates, and rendering. Transitions between states are explicit and controlled.

We can use the same idea on different machines: name the phases and define their transitions. Input, drawing, timing and sound still need implementations for the target. A shared state diagram does not make those hardware routines interchangeable.

Decide which transition wins

A transition is a change from one phase to another. Before writing the tests, we choose their order. For this example:

Current state Condition Next state
Title New Fire press Playing, with a fresh session
Playing Player dead after this update’s collisions Game Over
Playing Player alive and new Pause press Paused
Paused New Pause press Playing, preserving the session
Game Over New Fire press Title

Death wins over pause. We finish this update’s movement and collisions before deciding whether to pause. Another game could check pause before advancing the simulation, but that would be a different policy to explain and test.

We choose at most one transition per update. The new state’s update handler runs on the next update; it does not consume the same button press again immediately.

Pseudocode

The input reader uses the edge-detection pattern: one normalised sample per update, with separate held and newly pressed bits. A held Pause button must not alternate between Playing and Paused.

Code
STATE_TITLE     = 0
STATE_PLAYING   = 1
STATE_PAUSED    = 2
STATE_GAMEOVER  = 3

game_state: byte = STATE_TITLE
call initialise_title
call seed_input_history

; Run at the game's chosen update opportunities
update_game:
    call update_input
    next_state = game_state

    switch (game_state)          ; exactly one case, no fall-through
        case STATE_TITLE:
            if fire_just_pressed:
                next_state = STATE_PLAYING

        case STATE_PLAYING:
            update player using held input
            update enemies and gameplay timers
            check collisions
            if player_dead:
                next_state = STATE_GAMEOVER
            else if pause_just_pressed:
                next_state = STATE_PAUSED

        case STATE_PAUSED:
            if pause_just_pressed:
                next_state = STATE_PLAYING

        case STATE_GAMEOVER:
            if fire_just_pressed:
                next_state = STATE_TITLE

        default:
            report invalid state and stop at a diagnostic point

    if next_state != game_state:
        call change_state(next_state)

    prepare presentation for game_state
    return

fire_just_pressed and pause_just_pressed are Boolean tests of this update’s edge bits. Ordinary irrelevant input is ignored. An impossible internal state is a programming error to diagnose, not an excuse to fall through silently to Game Over.

Entry and exit have different jobs

Per-update work repeats. Entry and exit work happens once when we actually change state. For this small game, a direct routine is enough:

Code
change_state(next_state):
    old_state = game_state

    ; Exit responsibilities
    if old_state == STATE_TITLE:
        stop title music
    if old_state == STATE_PAUSED:
        remove pause overlay
    if old_state == STATE_PLAYING:
        if next_state == STATE_PAUSED:
            pause music progression; stop transient effects
        else if next_state == STATE_GAMEOVER:
            stop gameplay music and effects

    ; Entry responsibilities
    if next_state == STATE_PLAYING:
        if old_state == STATE_TITLE:
            initialise a fresh session
            start gameplay music
        else if old_state == STATE_PAUSED:
            resume music progression
    else if next_state == STATE_PAUSED:
        show pause overlay
    else if next_state == STATE_GAMEOVER:
        preserve final score
        show game-over result; request result cue once
    else if next_state == STATE_TITLE:
        stop result audio
        initialise title presentation; start title music

    game_state = next_state
    suppress transition-button edges for the new state
    return

The transition table defines the legal callers of this routine. A future request for any other transition needs an explicit rule rather than accidentally selecting an entry branch.

A fresh session initialises player and enemy state, score, lives, level data and gameplay timers. Resume does not call that initialiser. It keeps positions, score, lives and timers exactly as pause left them. Entering Title can preserve the previous final score for display; starting the next session resets the new session’s score.

“Suppress transition-button edges” means retaining the current sampled held state as history and clearing the pending press edges, as described in the input pattern. It does not mean reading the hardware a second time. Keep sampling while paused and on menus so a release followed by a new press can be observed.

The drawing and sound operations above describe responsibilities. Their implementations must respect the machine’s transfer windows, audio ownership and memory lifetimes. Preparing a screen is not permission to write every display register immediately. An interrupt handler sharing state needs a safe handover; this sequential example does not make a multi-step transition atomic.

What does pause stop?

For this example we choose the following policy:

Work While paused
Player, enemies, collisions and gameplay countdowns Do not advance
Gameplay animation Hold its current phase
Input sampling and pause-menu feedback Continue
Required display service Continue without advancing gameplay
Music Hold the musical position; silence output through the audio driver
Transient gameplay effects Stop; do not replay on resume

Pausing the note sequencer alone may leave hardware sounding its last note. The audio driver must implement the chosen silence/resume behaviour, or the game must choose a simpler policy such as stopping and restarting the tune. The Spectrum beeper and hardware audio channels need different implementations.

We do not disable all interrupts to pause a game. We stop the selected update work while keeping required input, display and audio servicing valid. On resume, a frame-counted game simply continues its frozen counters. A clock-based scheduler must reset its reference time or otherwise exclude the paused interval, so resume does not run a burst of missed simulation or musical events.

Predict two competing requests

We are Playing with score 7. This update’s collision kills the player, and its input sample also contains a new Pause press. Predict the next state and the work that should happen once.

Trace death and pause

The death condition selects Game Over. Because Pause is in an else if, it cannot overwrite that choice. Exit stops gameplay audio; entry preserves score 7, prepares the result and requests its cue once. The next update uses Game Over’s handler, so movement and gameplay timers do not advance.

With two independent if statements assigning the state, Pause could replace Game Over. The program might still draw a plausible screen while leaving a dead player in a paused session. A visible result alone would not expose the mistake.

Now trace a restart and a separate pause/resume. Begin on Game Over: press Fire, keep holding it for one update, release it, then press again. Once Playing, do the same press/hold/release/press sequence with Pause while the player stays alive. Predict the state after each update and mark when a fresh session is initialised.

Compare restart and resume
Update Starting state and observation Expected result
1 Game Over, new Fire press Title; stop result audio; no fresh session yet
2 Title, Fire still held Stay on Title
3 Title, Fire released Stay on Title
4 Title, Fire pressed again Playing; initialise score, lives, objects and timers once
5 Playing, alive, new Pause press Finish this simulation update, then pause
6 Paused, Pause still held Stay paused; simulation values unchanged
7 Paused, Pause released Stay paused; simulation values unchanged
8 Paused, Pause pressed again Resume without resetting the session or simulating the paused interval

Check the score, an enemy position and a gameplay timer through updates 5–8. Explain which values change during update 5 and why none should advance during 6–8. Repeat the Title → Playing route: initialisation must restore the same starting conditions every time, while the application may retain an intentional high score.

Explain why clearing input history to zero during the transition would change the held-button result. Revisit the input entry policy if the distinction between a held button and a new press is unclear.

Choosing a dispatch implementation

Start with a short sequence of comparisons and routine calls. On the Spectrum we can use Z80 comparisons and calls; C64 and NES code can use their 6502-family equivalents; Amiga code can use 68000 comparisons and calls. Preserve the distinction between calling a handler that returns and jumping to code that continues elsewhere.

A jump table may become useful with more states, but it needs a valid index, an address or offset format suitable for the CPU, and a defined return path. It does not solve transition priority, initialisation or cleanup. Measure a real implementation before claiming that a table is faster or assigning a universal byte or cycle cost.

When this helps

Use named states when different phases have different rules, such as a title screen, active play and results. A tiny single-screen experiment may need only a condition and a restart routine. Add structure when it makes responsibilities clearer.

Before adding a state, answer: how do we enter it, what continues, what stops, what survives leaving it, and which transition wins if two conditions occur together? Those answers make the state machine useful; setting a state byte alone does not perform the transition’s work.

Callable native routines

The reusable implementations separate a one-step transition decision from the caller’s session reset. Each includes a verified reset-on-resume mistake and its repair: C64, NES, Spectrum and Amiga. The interactive Spectrum state loop remains the real-keyboard companion.