Play a sound
Status: planned
@aosengine/audio is implemented, but the fps template this recipe edits lands in milestone M2, so the paths below do not exist yet.
Goal
A pickup makes a noise from where it sits in the world, at a volume that falls off with distance, and your game reacts when the sound finishes.
Files you will edit
assets.jsonsrc/systems/pickups.ts
Steps
Declare the sound in
assets.json. The id is the contract; the path is the host's business and never appears in game code.json{ "version": 1, "assets": [ { "id": "arena", "type": "splat", "src": "arena.spz" }, { "id": "pickup-chime", "type": "audio", "src": "sfx/chime.ogg", "tags": ["sfx"] } ] }Play it from the system that handles the pickup. Passing
entitymakes the voice positional: the host tracks that entity and pans the sound as you move around it.playreturns the guest-minted handle.tsimport type { GameContext } from '@aosengine/sdk'; // Module scope is fine for a handle: it is set in a system, not at import // time, so the Wizer snapshot does not freeze a value into the binary. let chime = 0; export function pickups(ctx: GameContext): void { for (const e of touched(ctx)) { // Positional: the host follows the entity until the sound ends. chime = ctx.audio.play('pickup-chime', { entity: e, volume: 0.8, bus: 'sfx' }); ctx.despawn(e); } }React when it finishes, in the same file. The host emits
sound-endedwith the handle you were given, and it arrives in the next tick'sctx.events.tsexport function pickupAudio(ctx: GameContext): void { for (const ev of ctx.events) { if (ev.tag === 'sound-ended' && ev.val.sound === chime) chime = 0; } }
Verify
sh
npm run devWalk into the pickup. The chime comes from the pickup's direction — turn around and it swaps ears — and the object disappears. Then assert it headlessly:
sh
npm testts
expect(harness.commands('play-sound')).toContainEqual(
expect.objectContaining({ asset: 'pickup-chime', bus: 'sfx' }),
);See also
- Assets and the manifest
- Engine modules
packages/audio/README.md— @aosengine/audio- Recipe template