Rhythm Game — Architecture
A rhythm game is an audio application with graphics attached. Every design choice below flows from one hard fact: the audio clock is the only clock that matters, and everything the player sees or presses must be reconciled against it. This document maps the full game — the parts inherited from the prototype (audited and fixed in stages 01–03) and the parts you build from stage 04 on.
The timing model (read this twice)#
Four clocks exist, and they disagree:
- The song, playing through the audio driver — the ground truth.
- The engine's report of the song —
get_playback_position()updates only per audio chunk (~10ms steps), so it stutters;AudioServer.get_time_since_last_mix()interpolates inside a chunk. - Output latency — what you hear trails what the engine processed by
AudioServer.get_output_latency()(tens of ms; huge on Bluetooth). - Input latency — a keypress reaches the game after the player's intent by hardware + OS + frame time.
Song time = playback position + time-since-mix − output latency − first-beat offset. Judging
subtracts input latency separately (it shifts judgment, not display). All positions derive
from beats = song time / beat duration — notes never move by += speed * delta, they
are placed at the beat's position every frame, so they can't drift.
Canonical reference: Sync the gameplay with audio.
Systems map#
| System | Pattern | Why |
|---|---|---|
| Conductor | One node owning song time; public API play(), get_current_beat(), beat_duration() | The single source of musical truth; everything asks it, nothing computes time on its own |
| Smoothing (stage 04) | Blend the jittery audio clock with the steady system clock (Time.get_ticks_usec) | Raw audio time steps ~10ms; on a 144Hz monitor that's visible stutter. The official demo uses a 1€ filter; a lerp-follower gets 90% of the benefit at 10% of the math |
| Charts | ChartResource (Array[NoteData]), one .tres per song | Data assets: a new song is a new file, zero code |
| Chart recording (stage 05) | Play the song, tap the keys, save what you tapped — minus your own input latency | Authoring charts by hand in an inspector is torture; recording is how real rhythm games chart |
| Note spawning & judgment | Per-lane first-in-first-out queues; only the oldest note per lane is judgeable | Prevents one keypress eating two notes; makes "too early" ignorable and "way late" a miss |
| Judgment channel | enum Judgment { PERFECT, GOOD, MISS } + a signal carrying the enum and the timing error in ms | The error signed value (early/late) feeds scoring, feedback, and calibration |
| Scoring & feedback (stage 06) | Combo, accuracy %, live early/late display | A rhythm game without timing feedback is unlearnable — the ms display is the practice loop |
| Calibration (stage 06) | A tap-to-the-metronome screen that measures the player's total latency and stores it | Every player's hardware differs; shipping without calibration ships a broken game for someone |
| Game flow (stage 07) | Menu → song select → play → results; pause done through process modes | The pause system is where the audit's process_mode lesson pays off: audio keeps its clock authority while gameplay freezes |
| Shipping (stage 08) | Runtime data to user://, charts read from res://, export presets | res:// is read-only in exports — the audit's disk bug becomes a design rule |
Data model#
- Definitions:
ChartResource(song stream, BPM, first-beat offset,Array[NoteData]) ·NoteData(beat, lane). - Runtime: score/combo/accuracy for the current run · the player's calibration offset
(saved to
user://settings.cfg) · recorded-chart buffers (saved touser://until promoted into the project).
The design decisions — yours to make#
- Smoothing strategy (stage 04): (A) lerp-follower — a smoothed time that chases raw song time, simple, one tunable; (B) dual-clock — advance by the system clock, correct drift against the audio clock gradually (the official demo's approach, minus the filter math). A is fewer lines; B is what you'd ship. Measure jitter first, then choose. Your call and reason: ______
- Where calibration applies (stage 06): (A) fold the measured offset into the Conductor's song time — display and judgment both shift; (B) apply it only in judgment — visuals stay glued to raw audio time. Real games disagree on this; test both with your own hands on a Bluetooth speaker before deciding. Your call and reason: ______
- Recording destination (stage 05): (A) straight into the loaded
ChartResource— convenient, but you're mutating a shared resource at runtime (the audit taught you why that bites); (B) a fresh buffer saved touser://, promoted manually. One of these is correct. Explain why in your own words. Your call and reason: ______
Build order#
Stages 01–03 (the audit) made the inherited code understood, fixed, and properly bounded. Then: 04 smooth conductor → 05 chart recording → 06 scoring, feedback, calibration → 07 menus, song select, pause, results → 08 export and ship. Each stage ends with the game playable and better than before — there is no "big rewrite" stage, because there doesn't need to be one.