Skip to content

Game State Machine

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

state-machinegame-looparchitecture

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.

This pattern works identically on any platform. The concept is the same whether you're coding in 6502, Z80, or 68000.

Pseudocode

; Game states (constants)
STATE_TITLE     = 0
STATE_PLAYING   = 1
STATE_PAUSED    = 2
STATE_GAMEOVER  = 3

; Current state variable
game_state: byte = STATE_TITLE

; Main loop
game_loop:
    switch (game_state)
        case STATE_TITLE:
            call title_update
        case STATE_PLAYING:
            call playing_update
        case STATE_PAUSED:
            call paused_update
        case STATE_GAMEOVER:
            call gameover_update

    goto game_loop

; State handlers
title_update:
    draw title screen
    if fire pressed:
        game_state = STATE_PLAYING
        call init_game
    return

playing_update:
    read input
    update player
    update enemies
    check collisions
    draw game
    if player_dead:
        game_state = STATE_GAMEOVER
    if pause pressed:
        game_state = STATE_PAUSED
    return

paused_update:
    draw "PAUSED"
    if pause pressed:
        game_state = STATE_PLAYING
    return

gameover_update:
    draw "GAME OVER"
    draw final score
    if fire pressed:
        game_state = STATE_TITLE
    return

Implementation Notes

6502 (NMOS — NES, C64, Apple II, BBC Micro, Atari 8-bit):

The original NMOS 6502 doesn't have a JMP (abs,X) instruction — that was added in the 65C02. So jump-table dispatch on NMOS 6502 needs an indirect through zero-page:

; Load state, multiply by 2, fetch the target address into a zp pointer
lda game_state
asl                     ; * 2 for word table
tax
lda state_table,x
sta jmp_target
lda state_table+1,x
sta jmp_target+1
jmp (jmp_target)        ; NMOS-compatible indirect jump

; jmp_target lives in zero page (2 bytes)
state_table:
    .word title_update
    .word playing_update
    .word paused_update
    .word gameover_update

A simpler portable alternative is a CMP/BEQ chain — slower (linear in number of states), but works everywhere and is easier to read:

lda game_state
cmp #STATE_TITLE
beq title_update
cmp #STATE_PLAYING
beq playing_update
cmp #STATE_PAUSED
beq paused_update
jmp gameover_update     ; Default fall-through

65C02 (Apple IIGS, BBC Master, Atari Lynx):

lda game_state
asl
tax
jmp (state_table,x)     ; 65C02 indexed indirect — NMOS 6502 lacks this

Z80:

ld a,(game_state)
add a,a                 ; * 2
ld e,a
ld d,0
ld hl,state_table
add hl,de
ld e,(hl)
inc hl
ld d,(hl)
ex de,hl
jp (hl)                 ; Jump to address in HL

68000 (jump tables):

; States are word offsets
move.w  game_state,d0
add.w   d0,d0           ; * 2 for word table
lea     state_table,a0
move.w  (a0,d0.w),d0
jmp     (a0,d0.w)

Trade-offs

AspectCost
CPUMinimal — one table lookup per frame
Memory~20-50 bytes for jump table + state variable
ComplexityLow — very readable and maintainable

When to use: Any game with multiple screens or phases.

When to avoid: Extremely simple single-screen games where a state machine adds unnecessary complexity.

Benefits

  • Clean separation — each state is self-contained
  • Easy debugging — you always know which state you're in
  • Simple transitions — just set the state variable
  • Extensible — adding new states is trivial