doc 2 of 10

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:

  1. The song, playing through the audio driver — the ground truth.
  2. The engine's report of the songget_playback_position() updates only per audio chunk (~10ms steps), so it stutters; AudioServer.get_time_since_last_mix() interpolates inside a chunk.
  3. Output latency — what you hear trails what the engine processed by AudioServer.get_output_latency() (tens of ms; huge on Bluetooth).
  4. 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#

SystemPatternWhy
ConductorOne 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
ChartsChartResource (Array[NoteData]), one .tres per songData 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 latencyAuthoring charts by hand in an inspector is torture; recording is how real rhythm games chart
Note spawning & judgmentPer-lane first-in-first-out queues; only the oldest note per lane is judgeablePrevents one keypress eating two notes; makes "too early" ignorable and "way late" a miss
Judgment channelenum Judgment { PERFECT, GOOD, MISS } + a signal carrying the enum and the timing error in msThe error signed value (early/late) feeds scoring, feedback, and calibration
Scoring & feedback (stage 06)Combo, accuracy %, live early/late displayA 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 itEvery 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 modesThe 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 presetsres:// 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 to user:// until promoted into the project).

The design decisions — yours to make#

  1. 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: ______
  2. 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: ______
  3. 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 to user://, 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.