Audio in Scripts
Learn how to play sounds, control playback, and handle audio events from your game scripts.
Web Engine provides multiple ways to play and control audio from scripts. Use the SoundManager API for one-shot sound effects, or control AudioSource components directly for persistent, looping sounds.
Playing Sounds from Scripts#
For one-shot sound effects (explosions, gunshots, UI clicks), use theSoundManager singleton:
import { SoundManager, AudioChannel } from '@web-engine-dev/core'; export default (api) => { const soundManager = SoundManager.getInstance(); return { onStart() { // Initialize audio system soundManager.init(); }, onUpdate(dt) { // Play sound effect when player shoots if (api.input.fireDown) { const handle = soundManager.play(gunShotAssetId, { volume: 0.8, pitch: 0.9 + Math.random() * 0.2, // Random pitch variation spatial: false, // 2D sound group: AudioChannel.SFX, priority: 150, // Medium priority fadeIn: 0.01, // Quick fade in }); // Store handle if you need to stop it later api.log('Playing sound, handle:', handle); } } };};SoundManager API#
play(assetId, options)#
Plays a one-shot sound and returns a handle for controlling it:
const handle = soundManager.play(assetId, { volume: 1.0, // Volume (0.0 - 1.0) pitch: 1.0, // Playback speed (0.01 - 4.0) loop: false, // Loop the sound spatial: true, // Enable 3D spatial audio refDistance: 1.0, // Distance for volume rolloff maxDistance: 100.0, // Maximum audible distance rolloffFactor: 1.0, // Distance attenuation factor group: AudioChannel.SFX, // Audio channel (SFX, Music, Voice) priority: 128, // Voice stealing priority (0-255, higher = less likely to steal) fadeIn: 0.0, // Fade in duration in seconds fadeOut: 0.0, // Fade out duration in seconds}); // Returns -1 if failed, or handle >= 0 on successstop(handle, immediate)#
Stops a playing sound by handle:
// Stop with configured fade outsoundManager.stop(handle); // Stop immediately (no fade)soundManager.stop(handle, true);updatePosition(handle, x, y, z)#
Updates the 3D position of a spatial sound:
export default (api) => { let bulletSoundHandle = -1; return (dt) => { // Play bullet whiz sound at bullet position if (api.input.fireDown) { bulletSoundHandle = soundManager.play(whizAssetId, { spatial: true, refDistance: 2.0, maxDistance: 50.0, }); } // Update sound position as bullet moves if (bulletSoundHandle >= 0) { const bulletPos = getBulletPosition(); soundManager.updatePosition( bulletSoundHandle, bulletPos.x, bulletPos.y, bulletPos.z ); // Check if sound is still playing if (!soundManager.isPlaying(bulletSoundHandle)) { bulletSoundHandle = -1; // Sound finished } } };};isPlaying(handle)#
Check if a sound is still playing:
if (soundManager.isPlaying(handle)) { api.log('Sound still playing');} else { api.log('Sound finished or stopped');}Controlling AudioSource Components#
For persistent, looping sounds, control AudioSource components directly:
import { AudioSource, AudioChannel } from '@web-engine-dev/core'; export default (api) => { return { onStart() { // Setup looping engine sound AudioSource.assetId[api.entity] = engineSoundAssetId; AudioSource.volume[api.entity] = 0.5; AudioSource.pitch[api.entity] = 0.8; AudioSource.loop[api.entity] = 1; // Enable looping AudioSource.spatial[api.entity] = 1; AudioSource.refDistance[api.entity] = 5.0; AudioSource.maxDistance[api.entity] = 100.0; AudioSource.group[api.entity] = AudioChannel.SFX; // Start playing AudioSource.playing[api.entity] = 1; }, onUpdate(dt) { // Adjust pitch based on speed const speed = api.velocity.length(); const maxSpeed = 100; const enginePitch = 0.5 + (speed / maxSpeed) * 1.5; AudioSource.pitch[api.entity] = enginePitch; // Adjust volume based on throttle const throttle = api.input.move.y; // Forward/back input const engineVolume = 0.3 + Math.abs(throttle) * 0.7; AudioSource.volume[api.entity] = engineVolume; }, onDestroy() { // Stop playing when entity is destroyed AudioSource.playing[api.entity] = 0; } };};Audio Logic Nodes#
Web Engine provides logic nodes for visual scripting with audio:
Play Sound Node#
// audio/play_sound logic node// Inputs:// - exec: trigger// - assetId: asset ID// - volume: number (0.0-1.0)// - pitch: number (0.01-4.0)// - loop: boolean// - spatial: boolean// - priority: number (0-255)// - fadeIn: number (seconds)// - fadeOut: number (seconds)//// Outputs:// - exec: trigger (fires after sound starts)// - handle: number (sound handle for stop node) // For one-shot sounds, plays via SoundManager// For looping sounds, uses AudioSource componentStop Sound Node#
// audio/stop_sound logic node// Inputs:// - exec: trigger// - handle: number (from Play Sound output)// - immediate: boolean (skip fade out if true)//// Outputs:// - exec: trigger (fires after stop command) // Stops sound by handle (works with both SoundManager and AudioSource)Set Volume Node#
// audio/set_volume logic node// Inputs:// - exec: trigger// - volume: number (0.0-1.0)//// Outputs:// - exec: trigger//// Sets volume on the entity's AudioSource componentSpatial Audio from Scripts#
Play 3D positional sounds at world positions:
export default (api) => { const soundManager = SoundManager.getInstance(); return { // Play explosion at impact point onCollision(other) { const impactPos = api.position; const handle = soundManager.play(explosionAssetId, { volume: 1.0, spatial: true, refDistance: 10.0, // Loud explosion maxDistance: 200.0, // Audible from far away rolloffFactor: 0.8, // Slow falloff for large explosion group: AudioChannel.SFX, priority: 200, // High priority, don't steal fadeIn: 0.02, fadeOut: 0.5, }); // Position is locked at creation for one-shots // To move a sound, you must update it every frame soundManager.updatePosition( handle, impactPos.x, impactPos.y, impactPos.z ); } };};Dynamic Audio Examples#
Footstep System#
export default (api) => { const soundManager = SoundManager.getInstance(); let stepTimer = 0; const stepInterval = 0.4; // Steps every 0.4 seconds let isLeftFoot = true; // Different surfaces have different sounds const footstepSounds = { grass: grassStepAssetId, concrete: concreteStepAssetId, metal: metalStepAssetId, }; return (dt) => { const speed = api.velocity.length(); const isMoving = speed > 0.1; if (isMoving) { stepTimer += dt; // Adjust step rate based on speed const currentInterval = stepInterval / (speed / 5.0); if (stepTimer >= currentInterval) { stepTimer = 0; isLeftFoot = !isLeftFoot; // Detect surface type (simplified) const surfaceType = api.groundMaterial || 'grass'; const assetId = footstepSounds[surfaceType] || footstepSounds.grass; // Play footstep with variation soundManager.play(assetId, { volume: 0.6, pitch: 0.95 + Math.random() * 0.1, // Slight pitch variation spatial: true, refDistance: 2.0, maxDistance: 30.0, group: AudioChannel.SFX, priority: 100, }); } } else { stepTimer = 0; // Reset when stopped } };};Dynamic Music System#
export default (api) => { let currentMusicEntity = null; let fadeTimer = 0; const fadeDuration = 2.0; // 2 second crossfade const musicTracks = { explore: exploreMusicAssetId, combat: combatMusicAssetId, victory: victoryMusicAssetId, }; function playMusic(trackName, fadeIn = true) { // Stop current music with fade if (currentMusicEntity) { AudioSource.playing[currentMusicEntity] = 0; } // Create new music entity const entity = api.world.createEntity(); currentMusicEntity = entity; AudioSource.assetId[entity] = musicTracks[trackName]; AudioSource.volume[entity] = fadeIn ? 0.0 : 0.7; AudioSource.loop[entity] = 1; AudioSource.spatial[entity] = 0; // 2D music AudioSource.group[entity] = AudioChannel.Music; AudioSource.playing[entity] = 1; if (fadeIn) { fadeTimer = 0; // Start fade in } } return (dt) => { // Fade in music if (fadeTimer < fadeDuration && currentMusicEntity) { fadeTimer += dt; const progress = Math.min(1.0, fadeTimer / fadeDuration); AudioSource.volume[currentMusicEntity] = progress * 0.7; } // Check game state and switch music if (api.isInCombat && currentTrack !== 'combat') { playMusic('combat'); } else if (!api.isInCombat && currentTrack === 'combat') { playMusic('explore'); } };};Ambient Sound System#
export default (api) => { const soundManager = SoundManager.getInstance(); const ambientSounds = [ { id: birdChirpAssetId, interval: 5.0, timer: 0 }, { id: windGustAssetId, interval: 8.0, timer: 0 }, { id: distantThunderAssetId, interval: 15.0, timer: 0 }, ]; return (dt) => { // Play random ambient sounds at intervals ambientSounds.forEach(sound => { sound.timer += dt; if (sound.timer >= sound.interval) { sound.timer = 0; // Random position around player const angle = Math.random() * Math.PI * 2; const distance = 20 + Math.random() * 30; const x = api.position.x + Math.cos(angle) * distance; const z = api.position.z + Math.sin(angle) * distance; const y = api.position.y; const handle = soundManager.play(sound.id, { volume: 0.3 + Math.random() * 0.2, pitch: 0.9 + Math.random() * 0.2, spatial: true, refDistance: 5.0, maxDistance: 100.0, group: AudioChannel.SFX, priority: 50, // Low priority }); soundManager.updatePosition(handle, x, y, z); // Randomize next interval sound.interval = (5.0 + Math.random() * 10.0); } }); };};Audio Scripting Best Practices#
- Use SoundManager for one-shots — play(), not AudioSource, for single-fire sounds
- Use AudioSource for loops — Persistent, looping sounds should use AudioSource component
- Store handles carefully — Only store handles you need to stop or update
- Check isPlaying before updates — Don't update positions of finished sounds
- Set priority appropriately — Critical sounds get higher priority (150-255)
- Add variation — Randomize pitch/volume to avoid repetitive sounds
- Respect voice limits — System has 64 voice pool, don't spam sounds
- Use fade in/out — Prevents audio pops and clicks
- Clean up on destroy — Stop sounds when entities are destroyed
- Route to correct channel — Use SFX, Music, or Voice appropriately
Performance Tips#
- Pool exhaustion — Monitor console for pool warnings, increase priority if needed
- Spatial culling — Sounds auto-stop beyond 1.5× maxDistance
- Avoid per-frame play() — Use timers/events to trigger sounds
- Reuse AudioSource entities — Don't create/destroy, toggle playing instead
- Use lower priority for ambient — Let important sounds steal ambient slots
- Short maxDistance for small sounds — Cull sounds early when not needed