Trigger on the beat
Goal
Something in the game happens on every beat of the music (a light pulses, an enemy steps), a sound you ask for lands exactly on the next beat, and a key press is judged "perfect", "good" or "miss" by how close to the beat it was.
Files you will edit
assets.jsonsrc/systems/beat.ts
Steps
Declare a track and a hit sound in
assets.json. Know the track's tempo and where its first beat is (firstBeat, seconds from the file start); the engine does not guess them.json{ "version": 1, "assets": [ { "id": "arena", "type": "splat", "src": "arena.spz" }, { "id": "song", "type": "audio", "src": "music/song.ogg", "tags": ["music"] }, { "id": "clap", "type": "audio", "src": "sfx/clap.ogg", "tags": ["sfx"] } ] }Start the song with its grid, react to
beatevents, and judge presses withctx.music.offsetMs(): the signed distance from now to the nearest beat, as the player hears it.tsimport type { GameContext } from 'gameable'; const PERFECT_MS = 45; const GOOD_MS = 110; let started = false; export let pulse = 0; // 1 on the beat, decays to 0; drive a light or a scale with it export let score = 0; export function beat(ctx: GameContext): void { if (!started) { started = true; ctx.music.play('song', { bpm: 120, beatsPerBar: 4, firstBeat: 0.05 }); } pulse = Math.max(0, pulse - ctx.dt * 4); for (const ev of ctx.events) { if (ev.tag !== 'beat') continue; pulse = 1; // On a bar's last beat, schedule a clap for the next one (the next bar's // first): it starts on the audio clock, sample-exact. if (ev.val.beatInBar === 3) ctx.audio.playOnBeat('clap', { quantize: 'beat' }); } if (ctx.input.pressed('Space')) { const off = Math.abs(ctx.music.offsetMs()); if (off <= PERFECT_MS) score += 100; else if (off <= GOOD_MS) score += 50; else score -= 10; } }For a chart of notes (beat numbers),
ctx.music.nearestBeat()says which beat a press is nearest to: judge it withnearestBeat() === noteand the offset above.examples/rhythmdoes exactly that.Never
playa sound when thebeatevent arrives and expect it on the beat: the event comes up to a frame late.playOnBeatschedules it on the audio clock instead.
Verify
sh
npm run devThe pulse lands with the kick you hear, the clap falls on every bar's first beat, and tapping Space with the music scores 100s. Headlessly, feed beat events to the guest and assert what it did:
sh
npm testts
expect(harness.commands('play-sound-on-beat')).toContainEqual(
expect.objectContaining({ quantize: 'beat' }),
);See also
- Play music that changes in combat
- Read player input
packages/audio/README.md— gameable/audioexamples/rhythm— a whole rhythm game in one file