Skip to main content

Audio & Sounds

Dreamlab provides a powerful AudioSource system for playing sounds in your game. Audio can be spatial (positioned) or global (same volume everywhere), and works seamlessly in both single-player and multiplayer contexts.

The engine handles audio playback through AudioSource entities that can be configured with clips, volumes, ranges, and more.

Adding Audio Assets

Basic Setup

  1. In the editor, create a new AudioSource entity under local
  2. Name it something descriptive (e.g., "JumpSound", "BackgroundMusic")
  3. Configure the audio properties:
    • clip - The audio file to play
    • volume - Base volume (0.0 to 1.0)
    • loop - Whether the sound repeats

Accessing Audio in Code

Reference your audio entity by name and cast it to AudioSource:

const audioSource = this.game.local!._["MyAudioEntity"].cast(AudioSource);
audioSource.play();

The ! asserts that local exists (client-side). On the server, game.local is undefined.

Spatial Audio

AudioSource supports spatial audio with distance-based attenuation:

PropertyDescription
minRangeDistance where sound starts to fade (0 = disabled)
maxRangeDistance where sound becomes inaudible (0 = disabled)
volumeBase volume multiplier (0.0 to 1.0)

Example: Spatial Footsteps

playFootstep(position: Vector2): void {
const audioSource = this.game.local!._.Audio._.Footstep.cast(AudioSource);

audioSource.minRange = 5; // Start fading at 5 units
audioSource.maxRange = 20; // Inaudible after 20 units
audioSource.volume = 0.5;
audioSource.play();
}

Disabling Spatial Audio

For UI sounds or player's own actions, you often want full volume regardless of position:

audioSource.minRange = 0;
audioSource.maxRange = 0;
audioSource.volume = 1.0;

Multiplayer Audio

In multiplayer games, sounds should be heard by all players at the correct position.

Solution: Centralized Sound System

Use a SoundSystem behavior to broadcast sound events:

// In any behavior
import SoundSystem from "./sound-system.ts";

class Weapon extends Behavior {
fire(): void {
// This plays for everyone at the weapon's position
SoundSystem.play(this.game, "shot-sound", this.entity.transform.position);
}
}

SoundSystem Behavior

Here's a complete system for networked audio with client prediction:

import { AudioSource, Behavior, Vector2 } from "@dreamlab/engine";
import { SettingsManager } from "./settings-manager.ts";

export default class SoundSystem extends Behavior {
onInitialize(): void {
this.game.network.onReceiveCustomMessage((senderId, channel, data) => {
if (channel === "playSound" && this.game.isServer()) {
// Broadcast to OTHER clients (sender already played it locally)
const connections = Array.from(this.game.network.connections) as any[];
for (const conn of connections) {
const clientId = typeof conn === "string" ? conn : (conn.id || conn.clientId || conn);
if (clientId !== senderId) {
this.game.network.sendCustomMessage(clientId, "playSoundLocal", data);
}
}
}

if (channel === "playSoundLocal" && this.game.isClient()) {
this.playLocalSound(data.soundName, data.position);
}
});
}

private playLocalSound(soundName: string, position: { x: number; y: number }): void {
const audioPath = this.game.local!._.Audio._[soundName];
if (!audioPath) return;

const sound = audioPath.cloneInto(this.game.local!, {
transform: { position: { x: position.x, y: position.y } },
});

const audioSource = sound.cast(AudioSource);
const userVolume = SettingsManager.getVolume();

setTimeout(() => {
audioSource.volume = audioSource.volume * userVolume;
audioSource.play();
setTimeout(() => sound.destroy(), 2000);
}, 0);
}

static play(game: any, soundName: string, position: Vector2): void {
if (game.isServer()) {
// Server broadcasts to all clients
const connections = Array.from(game.network.connections) as any[];
for (const conn of connections) {
const clientId = typeof conn === "string" ? conn : (conn.id || conn.clientId || conn);
game.network.sendCustomMessage(clientId, "playSoundLocal", {
soundName,
position: { x: position.x, y: position.y },
});
}
return;
}

if (game.isClient()) {
// Client plays sound locally immediately (no round-trip delay)
const audioPath = game.local?._.Audio?._?.[soundName];
if (audioPath) {
const sound = audioPath.cloneInto(game.local, {
transform: { position: { x: position.x, y: position.y } },
});

const audioSource = sound.cast(AudioSource);
const userVolume = SettingsManager.getVolume();

// For own sounds, override spatial settings to play at full volume
setTimeout(() => {
audioSource.minRange = 0;
audioSource.maxRange = 0;
audioSource.volume = 1.0 * userVolume;
audioSource.play();
setTimeout(() => sound.destroy(), 2000);
}, 0);
}

// Also tell server to broadcast to OTHER clients with entity position
game.network.sendCustomMessage("server", "playSound", {
soundName,
position: { x: position.x, y: position.y },
});
}
}
}

How it Works

  1. Client triggers sound: Calls SoundSystem.play() with sound name and position
  2. Immediate local playback: Client hears the sound instantly (no network delay)
  3. Server broadcast: Client tells server, which broadcasts to other clients
  4. Remote playback: Other clients play the sound at the specified position with spatial audio

This pattern eliminates latency for the player who triggered the sound while keeping everyone else in sync.

Quick Reference

TaskCode
Play existing audio entitythis.game.local!._["MySound"].cast(AudioSource).play()
Set spatial rangeaudioSource.minRange = 5; audioSource.maxRange = 20;
Stop playing soundaudioSource.stop()
Loop background musicSet loop: true on AudioSource component

Troubleshooting

SymptomLikely fix
Sound doesn't playVerify sound entity name
Can't hear sound far awayIncrease maxRange or disable spatial audio
Sound plays twice in multiplayerUse SoundSystem pattern to avoid echoes
Volume too quietCheck base volume, user settings, and spatial attenuation
Overlapping sounds cut offClone the audio entity instead of reusing the same instance

Pro tip: Keep all your audio entities organized under a single Audio container entity in game.local for easy reference:

const sounds = this.game.local!._.Audio._;
sounds["Jump"].cast(AudioSource).play();
sounds["Shoot"].cast(AudioSource).play();