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
| Aspect | Cost |
|---|---|
| CPU | Minimal — one table lookup per frame |
| Memory | ~20-50 bytes for jump table + state variable |
| Complexity | Low — 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