Play music that changes in combat
Goal
Exploration music plays from the start; when an enemy notices the player, drums and a lead come in on the next bar without the music restarting, and they fade back out a few seconds after the fight ends.
Files you will edit
assets.jsonsrc/systems/music.ts
Steps
Declare the layers in
assets.json. They are stems: loops of the same length and tempo, exported from one session, so they stay in phase. Keep them short (one to four bars, mono is fine): stems are decoded whole, andseconds x 48000 x channels x 4bytes each is what a phone holds.json{ "version": 1, "assets": [ { "id": "arena", "type": "splat", "src": "arena.spz" }, { "id": "music-pad", "type": "audio", "src": "music/pad.ogg", "tags": ["music"] }, { "id": "music-drums", "type": "audio", "src": "music/drums.ogg", "tags": ["music"] }, { "id": "music-lead", "type": "audio", "src": "music/lead.ogg", "tags": ["music"] } ] }Start the stems once, with only the pad audible.
bpmgives the music a beat grid, so later changes can land on a bar.tsimport type { GameContext } from 'gameable'; const DRUMS = 1; const LEAD = 2; const CALM_AFTER = 4; // seconds without an enemy in sight before the drums go let started = false; let inCombat = false; let calmFor = 0; export function music(ctx: GameContext): void { if (!started) { started = true; ctx.music.stems(['music-pad', 'music-drums', 'music-lead'], { bpm: 96, gains: [1, 0, 0], loop: true, fadeMs: 1500, }); } const threatened = enemiesAlerted(ctx) > 0; if (threatened) calmFor = 0; else calmFor += ctx.dt; if (threatened && !inCombat) { inCombat = true; ctx.music.layer(DRUMS, 1, 400); ctx.music.layer(LEAD, 0.8, 1200); } else if (inCombat && calmFor > CALM_AFTER) { inCombat = false; ctx.music.layer(DRUMS, 0, 3000); ctx.music.layer(LEAD, 0, 3000); } }enemiesAlertedis your game's own query (the enemy recipe's AI state).For a different track rather than a layer — a boss theme — crossfade to it on the next bar. A single track streams, so it can be minutes long:
tsctx.music.play('music-boss', { bpm: 140, fadeMs: 2000, start: 'bar', loop: true });
Verify
sh
npm run devWalk toward an enemy: the drums enter on a bar line, not mid-beat, and the pad never restarts. Walk away and they fade out after four seconds. Headlessly:
sh
npm testts
expect(harness.commands('set-music-layer')).toContainEqual(
expect.objectContaining({ layer: 1, gain: 1 }),
);See also
- Trigger on the beat
- Play a sound
packages/audio/README.md— gameable/audio- Add an enemy