This tutorial covers game state management on the SNES, including state machines, screen transitions, and common game flow patterns (title screen, gameplay, pause, game over).
What Are Game States?
A game state represents what the game is currently doing. Most SNES games cycle through a small set of states:
| State | Purpose |
| Title | Show title screen, wait for START |
| Playing | Run game logic, handle input, render |
| Paused | Freeze game logic, show overlay message |
| Game Over | Display result, wait for restart |
Each state has its own update logic, and transitions between states involve screen fades, VRAM reloads, or both.
The scene stack (recommended) — scene module
Before hand-rolling a state variable, reach for the opt-in scene module (LIB_MODULES += scene). It gives you a stack of Scene structs — each a pair of callbacks — that push and pop, so "title → play → pause → back to
play" is a data structure instead of an enum and a switch:
static void play_init(void); static void play_update(void);
static const Scene play = { play_init, play_update };
static void play(u8 sample, u8 vol, u8 pan, u16 pitch, u16 tint)
Play + tint the backdrop + update the probe oracles.
Definition main.c:71
static void title_init(void)
Definition main.c:79
static void title_update(void)
Definition main.c:87
static void pause_update(void)
Definition main.c:127
#define NULL
Null pointer constant.
Definition types.h:167
Opt-in scene/state stack — push/pop game scenes without rolling your own state machine.
A single scene's callbacks.
Definition scene.h:144
- init runs once when a scene is first pushed — load its tiles / palettes here (it happens before the first WaitForVBlank). It is not re-run when a scene is resumed after a pop, so a pause overlay that pops back to play leaves the game exactly as it was.
- update runs every VBlank while the scene is on top. A suspended scene (one below the top) gets no callbacks — that is your pause-freeze for free.
- scenePush/scenePop record the change; the new top's init/update dispatch on the next VBlank. To replace rather than stack, call scenePop(); scenePush(&next);.
Pair it with gameLoopRun (gameloop module) so you don't even write the while (1) WaitForVBlank() loop:
void gameLoopRun(const GameLoopConfig *cfg)
Run the OpenSNES game loop. Never returns.
Scene GameLoopConfig
Game loop callbacks. Alias for Scene from <snes/scene.h>.
Definition gameloop.h:99
examples/basics/scene_stack is the worked example: title → counter → pause overlay, three Scene structs, no hand-rolled dispatch. Prefer this for new games — it is the approach the manual pattern below exists to replace.
The manual state machine (under the hood)
The classic SNES pattern uses a state variable and a switch in the main loop. It is what scene is built on top of, and it is still the right tool when you need a custom rhythm (half-rate updates, a bespoke transition) or are reading the shipped games tetris/breakout, which use it directly. Define states as constants and dispatch each frame:
#define STATE_TITLE 0
#define STATE_PLAYING 1
#define STATE_PAUSED 2
#define STATE_GAME_OVER 3
while (1) {
break;
break;
case STATE_PAUSED:
statePaused();
break;
break;
}
}
return 0;
}
int main(void)
Definition main.c:66
u8 game_state
Probe oracle: current state (0 title, 1 play, 2 over).
Definition main.c:67
void consoleInit(void)
Initialize SNES hardware.
void WaitForVBlank(void)
Wait for next VBlank period.
#define STATE_GAME_OVER
Game over: board frozen, "GAME OVER" rainbow cycle, await restart.
Definition main.c:91
#define STATE_PLAYING
Active gameplay: piece falling, player input, gravity.
Definition main.c:87
static void stateGameOver(void)
Game over state: display frozen board with rainbow "GAME OVER" text.
Definition main.c:583
static void stateTitle(void)
Title screen state: rainbow "PRESS START" text until player begins.
Definition main.c:625
static void statePlaying(void)
Main gameplay state: handle input, apply gravity, lock pieces.
Definition main.c:443
#define STATE_TITLE
Title screen: displaying "PRESS START" with rainbow cycle.
Definition main.c:85
#define BG_MODE1
Definition video.h:35
unsigned char u8
8-bit unsigned integer (0 to 255)
Definition types.h:47
OpenSNES common-case master header.
void setMode(u8 mode, u8 flags)
Set background mode.
This is the exact pattern used in examples/games/tetris/main.c. Each state function runs once per frame and can change game_state to trigger a transition.
State Transitions
Transitioning between states often requires reloading VRAM, resetting variables, or fading the screen. The SNES PPU silently ignores VRAM writes during active display, so transitions must happen during VBlank or forced blank.
Fade Out / Fade In
The INIDISP register ($2100) controls screen brightness (0-15). Stepping through brightness levels over multiple frames creates a smooth fade:
static void fade_out(
u8 speed) {
for (brightness = 15; brightness >= 0; brightness--) {
for (
i = 0;
i < speed;
i++) {
}
}
}
static void fade_in(
u8 speed) {
for (brightness = 0; brightness <= 15; brightness++) {
for (
i = 0;
i < speed;
i++) {
}
}
}
void setBrightness(u8 brightness)
Set screen brightness.
static u8 i
Definition main.c:156
signed char s8
8-bit signed integer (-128 to 127)
Definition types.h:44
A speed of 1 gives a fast 16-frame fade (~267ms). A speed of 3 gives a slower cinematic fade. See examples/transitions/fading/ for a complete working example.
VRAM Reload Between States
When switching from title screen to gameplay (or vice versa), you typically need to load different tiles, tilemaps, and palettes. Use forced blank to guarantee VRAM writes succeed:
static void transition_to_gameplay(void) {
fade_out(2);
dmaCopyVram(gameplay_tiles, 0x1000, gameplay_tiles_size);
fade_in(2);
}
u16 score
Probe oracle: coins collected.
Definition main.c:69
void setScreenOff(void)
Definition console.h:140
void setScreenOn(void)
Enable screen display.
Definition console.h:115
void dmaCopyVram(const u8 *source, u16 vramAddr, u16 size)
Copy data to VRAM (PVSnesLib compatible).
void dmaCopyCGram(const u8 *source, u16 startColor, u16 size)
Copy palette data to CGRAM (PVSnesLib compatible).
static u16 lives
Definition main.c:165
The key rule: call setScreenOff() before bulk VRAM/CGRAM writes, and setScreenOn() after. This is the forced blank pattern used throughout the OpenSNES examples.
Title Screen Implementation
A title screen waits for the player to press START, then transitions to gameplay. The pattern from examples/games/tetris/main.c:
return;
}
do {
}
u16 pad_keys[]
Raw joypad state buffer written by the NMI handler every frame.
static void startGame(void)
Initialize a new game: clear board, reset state, start music.
Definition main.c:393
The release-wait loop (do { WaitForVBlank(); } while (pad_keys[0] & KEY_START)) prevents the START press from triggering an immediate pause in the gameplay state. This is a standard debounce pattern on the SNES.
Complete Title Flow
Breakout (examples/games/breakout/main.c) shows the full sequence: display "READY" text, wait for START, clear the message, then enter the game loop:
u16 blockmap[]
BG1 tilemap RAM copy (0x400 entries = 2KB) at WRAM $0800.
#define ST_READY
Definition main.c:105
static void writestring(const char *st, u16 *tilemap, u16 pos, u16 offset)
Write a null-terminated string to a tilemap buffer.
Definition main.c:202
#define ST_BLANK
Definition main.c:108
Pause Screen
Pausing overlays a message on the current gameplay, freezes all game logic, and resumes when START is pressed again. The key is a three-phase wait: release START, wait for START press, wait for release again.
From examples/games/breakout/main.c:
}
#define ST_PAUSED
Definition main.c:107
static u16 pad0
Definition main.c:167
static void handle_pause(void)
Handle pause functionality.
Definition main.c:530
The three-phase wait prevents the unpause START press from being read as a new pause request on the next frame. Tetris (examples/games/tetris/main.c) uses the same pattern with a prev_pad edge-detection approach:
}
const char str_paused[]
"PAUSED" message string
static u8 paused
Definition main.c:155
static u16 prev_pad
Previous frame's joypad state (for edge-triggered button detection).
Definition main.c:168
unsigned short u16
16-bit unsigned integer (0 to 65535)
Definition types.h:53
void hudShowMessage(const char *str)
Definition hud.c:108
Pause Design Rules
- Do not reload VRAM for a simple pause overlay. Write to a tilemap buffer and DMA it.
- Freeze game logic by returning early from the update function while paused.
- The NMI handler keeps running during pause. Input is still read, OAM is still transferred. Only your game logic stops.
- Redraw sprites before entering the pause loop if you use delta rendering, so the screen looks correct while frozen.
Game Over
Game over detection happens in gameplay logic. When the end condition is met, display a message and wait for player input before restarting.
Simple Game Over (Breakout)
Breakout detects game over when lives reach zero. It shows a message and halts:
}
}
static s16 pos_x
Definition main.c:177
static s16 pos_y
Definition main.c:178
#define ST_GAMEOVER
Definition main.c:106
static void die(void)
Handle player losing a life.
Definition main.c:481
Restart Game Over (Tetris)
Tetris allows restarting. When a new piece cannot spawn (board full), the game transitions to STATE_GAME_OVER, which shows a message, waits for START, then calls startGame() to reinitialize everything:
}
}
const char str_gameover[]
"GAME OVER" message string
void hudClearMessage(void)
Definition hud.c:139
void renderFlush(void)
Definition render.c:382
void renderBoard(void)
Definition render.c:277
Game Over with Fade
Combining game over with a fade transition creates a polished feel:
writestring(
"GAME OVER", tilemap_buf, msg_pos, offset);
}
fade_out(2);
initGame();
fade_in(2);
}
#define TILEMAP_ADDR
VRAM word address for the BG1 tilemap (32x32 = 2KB).
Definition main.c:119
State Machine Summary
| Concern | Pattern |
| State storage | static u8 game_state; with #define constants |
| Dispatch | switch (game_state) in main loop, one call per frame |
| Transition | Set game_state = NEW_STATE; then return; |
| VRAM reload | setScreenOff() before DMA, setScreenOn() after |
| Fade | Step setBrightness(0..15) across frames |
| Button debounce | Three-phase: release, press, release |
| Pause | Overlay text, freeze logic, same VRAM |
| Game over | Message, wait for input, reinitialize or halt |
Next Steps