Audio
The audio system is built on top of Three.js Audio and provides three layers:
- GlobalAudio — master volume control, shared
AudioListener - GlobalAudioManager — load, configure, and destroy
Audio/PositionalAudioinstances - AudioBackground — playlist-based background music with auto-advance, loop, and chaining
GlobalAudio is created automatically by World and exposed as world.audio. The manager and background player are built on top of it.
🔇 Browser autoplay policy
Browsers block audio playback until the user has interacted with the page (click, tap, key press). You must start playback from within a user gesture handler:
canvas.addEventListener("click", async() => {
await audioBackground.play("ambient.forest");
});NOTE
This is a browser restriction, not an engine limitation. See Chrome Autoplay Policy.
GlobalAudio
Master volume controller. Wraps a Three.js AudioListener and notifies observers when the volume changes.
type GlobalAudioEvents = {
volumechange: [volume: number];
};
interface VolumeObserver {
onMasterVolumeChange: (volume: number) => void;
}interface GlobalAudio {
// The underlying Three.js AudioListener
readonly listener: AudioListenerAdapter;
readonly threeAudioListener: THREE.AudioListener;
// Master volume (0 to 1)
volume: number;
// Register/unregister volume observers
observe(observer: VolumeObserver): this;
unobserve(observer: VolumeObserver): this;
}GlobalAudioManager
Loads audio files, configures volume/loop, and manages cleanup.
interface AudioLoadingOptions {
name?: string;
// default false
loop?: boolean;
// default 1
volume?: number;
}interface AudioManager {
// Async — fetch + decode from a URL at runtime
loadAudio(url: string, options?: AudioLoadingOptions): Promise<THREE.Audio>;
loadPositionalAudio(url: string, options?: AudioLoadingOptions): Promise<THREE.PositionalAudio>;
// Sync — construct from a buffer already loaded by AssetManager
createAudio(buffer: AudioBuffer, options?: AudioLoadingOptions): THREE.Audio;
createPositionalAudio(buffer: AudioBuffer, options?: AudioLoadingOptions): THREE.PositionalAudio;
destroyAudio(audio: THREE.Audio | THREE.PositionalAudio): void;
}fromWorld(world)
Creates a GlobalAudioManager bound to the world's AudioListener and registers audio loaders (.mp3, .ogg, .wav, .aac, .flac) on the global AssetManager registry. Call once during game setup, before loadAssets runs.
GlobalAudioManager.fromWorld(world: World): GlobalAudioManager;Async loading (loadAudio / loadPositionalAudio)
Fetches and decodes a URL on demand. Useful for audio that is loaded dynamically at runtime (e.g. user-triggered sound effects loaded after the loading screen).
const audio = await audioManager.loadAudio("sounds/click.mp3", { volume: 0.5 });
audio.play();Sync creation from pre-loaded buffers (createAudio / createPositionalAudio)
When buffers have been pre-loaded through AssetManager (e.g. via AudioLibrary), these methods construct a ready-to-play THREE.Audio or THREE.PositionalAudio synchronously — no await needed in lifecycle methods.
// In Behavior.start():
this.#shootAudio = audioManager.createAudio(sfx.get("shoot"), { volume: 0.8 });
this.#musicAudio = audioManager.createAudio(sfx.get("music"), { loop: true, volume: 0.5 });
this.actor.add(this.#shootAudio);For 3D-positioned sound, use createPositionalAudio and add the result to an Actor:
this.#footsteps = audioManager.createPositionalAudio(sfx.get("footstep"), { loop: true });
this.actor.add(this.#footsteps);