Lesson 20 of 22, in world 7: Polish & Shipping
Game States, UI & Audio
A game is more than its gameplay: there’s a title screen, a pause menu, a game-over screen, a HUD showing score and health, sound effects and music, and saved progress. This is the “wrapper” that turns a prototype into something a stranger can pick up and play.
Game states (a.k.a. scenes or screens)
The game as a whole is a state machine (lesson 15!) whose states are screens. Each one has its own update, draw and input handling. Pausing is simply a state that doesn’t call the gameplay update.
Enter start · ←→ move · P pause. The mini-diagram in the corner shows which game state is active. Each state has its own update and draw; the HUD is drawn on top in screen space. Your best score is saved in the browser.
const states = {
title: { update: () => { if (input.start) go('playing') }, draw: drawTitle },
playing: { update: updateGameplay, draw: drawWorldAndHud },
paused: { update: () => { if (input.pause) go('playing') }, draw: () => { drawWorldAndHud(); drawPauseMenu() } },
gameover: { update: () => { if (input.start) go('playing') }, draw: drawGameOver },
}
function loop(dt) { states[current].update(dt); states[current].draw() }HUD and UI
- HUD (heads-up display) is drawn in screen space after the world (lesson 13), so it ignores the camera.
- Show only what the player needs to decide: health, score, ammo, a timer. Less is more.
- UI reacts to events (lesson 16) —
health_changedupdates the hearts — rather than gameplay code poking the UI. - Menus must work with the same input as the game (keyboard/gamepad), not just the mouse.
Audio
Sound is half of game feel (lesson 19). Two kinds: sound effects (short, triggered by events, many at once) and music (long, looping, usually one at a time, cross-faded between states). The demo synthesises its blips with the Web Audio API — the same way retro tools like sfxr make chiptune effects.
const ac = new AudioContext() // must be created/resumed after a user click or key
function blip(freq, toFreq, duration) {
const osc = ac.createOscillator(), gain = ac.createGain()
osc.type = 'square'
osc.frequency.setValueAtTime(freq, ac.currentTime)
osc.frequency.exponentialRampToValueAtTime(toFreq, ac.currentTime + duration)
gain.gain.setValueAtTime(0.1, ac.currentTime)
gain.gain.exponentialRampToValueAtTime(0.0001, ac.currentTime + duration)
osc.connect(gain).connect(ac.destination)
osc.start(); osc.stop(ac.currentTime + duration)
}Saving
Persist only what you need — high scores, unlocked levels, settings — as plain serialisable data. In the browser that’s localStorage; engines write a file in the user’s data folder. Version your save format so old saves still load after updates.
Key takeaways
- The whole game is a state machine of screens: title, playing, paused, game over.
- HUD is drawn in screen space and updated by events.
- Sound effects on events, music per state; unlock audio with a first user interaction.
- Save small, serialisable, versioned data.
Check yourself
Q1. How does pausing usually work?
Q2. Why is the HUD drawn after ctx.restore() of the camera?
Q3. Why might sound not play when the page loads?
Exercises
- Add a "settings" state reachable from the title screen with a sound on/off option. Draw its transitions.
- Play Meteor Dodge until game over. Which juice from lesson 19 would improve it most?
- Design the save data for a small platformer: what fields, and what would version 2 add?