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 success

stop(handle, immediate)#

Stops a playing sound by handle:

// Stop with configured fade out
soundManager.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 component

Stop 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 component

Spatial 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
Audio | Web Engine Docs | Web Engine Docs