OpenSNES
Modern Open-Source SNES Development SDK
Loading...
Searching...
No Matches
Game States Tutorial

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:

#include <snes/scene.h>
static void title_init(void); static void title_update(void);
static void play_init(void); static void play_update(void);
static void pause_update(void);
static const Scene title = { title_init, title_update };
static const Scene play = { play_init, play_update };
static const Scene pause = { NULL, pause_update }; // init may be NULL
// in title_update: if (start_pressed) scenePush(&play);
// in play_update: if (start_pressed) scenePush(&pause);
// in pause_update: if (start_pressed) scenePop(); // back to play, unchanged
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:

GameLoopConfig cfg = { .init = onInit, .update = onUpdate };
gameLoopRun(&cfg); // never returns
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:

#include <snes.h>
#define STATE_TITLE 0
#define STATE_PLAYING 1
#define STATE_PAUSED 2
#define STATE_GAME_OVER 3
static u8 game_state;
int main(void) {
while (1) {
switch (game_state) {
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) {
s8 brightness;
u8 i;
for (brightness = 15; brightness >= 0; brightness--) {
setBrightness((u8)brightness);
for (i = 0; i < speed; i++) {
}
}
}
static void fade_in(u8 speed) {
u8 brightness;
u8 i;
for (brightness = 0; brightness <= 15; brightness++) {
setBrightness(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 to black */
fade_out(2);
/* Screen is dark — force blank for safe VRAM access */
/* Load gameplay assets */
dmaCopyVram(gameplay_tiles, 0x1000, gameplay_tiles_size);
dmaCopyVram(gameplay_map, 0x0400, gameplay_map_size);
dmaCopyCGram(gameplay_palette, 0, 256);
/* Reset game variables */
score = 0;
lives = 3;
/* Turn screen back on and fade in */
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:

static void stateTitle(void) {
/* Poll for START press */
if ((pad_keys[0] & KEY_START) == 0) {
return; /* Not pressed — keep waiting */
}
/* Wait for START release to prevent immediate pause */
do {
} while (pad_keys[0] & KEY_START);
/* Transition to gameplay */
}
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
#define KEY_START
Definition input.h:74

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:

/* Display ready message in tilemap */
writestring(ST_READY, blockmap, 0x248, 0x3F6);
dmaCopyVram((u8 *)blockmap, 0x0000, 0x800);
/* Wait for START press */
do { WaitForVBlank(); } while ((pad_keys[0] & KEY_START) == 0);
/* Wait for START release */
do { WaitForVBlank(); } while (pad_keys[0] & KEY_START);
/* Clear message and begin */
writestring(ST_BLANK, blockmap, 0x248, 0x3F6);
dmaCopyVram((u8 *)blockmap, 0x0000, 0x800);
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:

static void handle_pause(void) {
if ((pad0 & KEY_START) == 0) return;
/* Show "PAUSED" in tilemap and flush to VRAM */
writestring(ST_PAUSED, blockmap, 0x269, 0x3F6);
dmaCopyVram((u8 *)blockmap, 0x0000, 0x800);
/* Three-phase wait: release -> press -> release */
do { WaitForVBlank(); } while (pad_keys[0] & KEY_START);
do { WaitForVBlank(); } while ((pad_keys[0] & KEY_START) == 0);
do { WaitForVBlank(); } while (pad_keys[0] & KEY_START);
/* Clear message and resume */
writestring(ST_BLANK, blockmap, 0x269, 0x3F6);
dmaCopyVram((u8 *)blockmap, 0x0000, 0x800);
}
#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:

u16 pressed = pad & ~prev_pad; /* newly pressed this frame */
if (pressed & KEY_START) {
paused = 1;
/* ... three-phase wait ... */
paused = 0;
}
if (paused) return; /* skip all game logic */
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 void die(void) {
if (lives == 0) {
/* Show GAME OVER message */
writestring(ST_GAMEOVER, blockmap, 0x267, 0x3F6);
dmaCopyVram((u8 *)blockmap, 0x0000, 0x800);
/* Halt — player must reset */
while (1) { WaitForVBlank(); }
}
/* Still have lives: reset ball position, continue */
lives--;
pos_x = 94;
pos_y = 109;
/* ... */
}
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:

static void stateGameOver(void) {
/* Force blank for guaranteed DMA */
/* Wait for START to restart */
while ((pad_keys[0] & KEY_START) == 0) {
}
do { WaitForVBlank(); } while (pad_keys[0] & KEY_START);
startGame(); /* reinitialize board, score, spawn first piece */
}
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:

static void stateGameOver(void) {
/* Show game over message */
writestring("GAME OVER", tilemap_buf, msg_pos, offset);
dmaCopyVram((u8 *)tilemap_buf, TILEMAP_ADDR, TILEMAP_SIZE);
/* Wait for player acknowledgement */
while ((pad_keys[0] & KEY_START) == 0) {
}
do { WaitForVBlank(); } while (pad_keys[0] & KEY_START);
/* Fade out, reload, fade in */
fade_out(2);
/* Reset all game state and reload assets */
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