Skip to content

Commit fb0898e

Browse files
committed
add doc about audio
1 parent d1c3c17 commit fb0898e

1 file changed

Lines changed: 215 additions & 0 deletions

File tree

docs/guide/audio.mdx

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
---
2+
title: Audio & Sounds
3+
---
4+
5+
# Audio & Sounds
6+
7+
Dreamlab provides a powerful **AudioSource** system for playing sounds in your game.
8+
Audio can be **spatial** (positioned) or **global** (same volume everywhere), and works seamlessly in both single-player and multiplayer contexts.
9+
10+
The engine handles audio playback through `AudioSource` entities that can be configured with clips, volumes, ranges, and more.
11+
12+
## Adding Audio Assets
13+
14+
### Basic Setup
15+
16+
1. In the editor, create a new `AudioSource` entity under `local`
17+
2. Name it something descriptive (e.g., "JumpSound", "BackgroundMusic")
18+
3. Configure the audio properties:
19+
- **clip** - The audio file to play
20+
- **volume** - Base volume (0.0 to 1.0)
21+
- **loop** - Whether the sound repeats
22+
23+
### Accessing Audio in Code
24+
25+
Reference your audio entity by name and cast it to `AudioSource`:
26+
27+
```ts
28+
const audioSource = this.game.local!._["MyAudioEntity"].cast(AudioSource);
29+
audioSource.play();
30+
```
31+
32+
> The `!` asserts that `local` exists (client-side).
33+
> On the server, `game.local` is `undefined`.
34+
35+
## Spatial Audio
36+
37+
AudioSource supports **spatial audio** with distance-based attenuation:
38+
39+
| Property | Description |
40+
| ------------ | ------------------------------------------------ |
41+
| **minRange** | Distance where sound starts to fade (0 = disabled) |
42+
| **maxRange** | Distance where sound becomes inaudible (0 = disabled) |
43+
| **volume** | Base volume multiplier (0.0 to 1.0) |
44+
45+
### Example: Spatial Footsteps
46+
47+
```ts
48+
playFootstep(position: Vector2): void {
49+
const audioSource = this.game.local!._.Audio._.Footstep.cast(AudioSource);
50+
51+
audioSource.minRange = 5; // Start fading at 5 units
52+
audioSource.maxRange = 20; // Inaudible after 20 units
53+
audioSource.volume = 0.5;
54+
audioSource.play();
55+
}
56+
```
57+
58+
### Disabling Spatial Audio
59+
60+
For UI sounds or player's own actions, you often want **full volume regardless of position**:
61+
62+
```ts
63+
audioSource.minRange = 0;
64+
audioSource.maxRange = 0;
65+
audioSource.volume = 1.0;
66+
```
67+
68+
## Multiplayer Audio
69+
70+
In multiplayer games, **sounds should be heard by all players** at the correct position.
71+
72+
### Solution: Centralized Sound System
73+
74+
Use a **SoundSystem** behavior to broadcast sound events:
75+
76+
```ts
77+
// In any behavior
78+
import SoundSystem from "./sound-system.ts";
79+
80+
class Weapon extends Behavior {
81+
fire(): void {
82+
// This plays for everyone at the weapon's position
83+
SoundSystem.play(this.game, "shot-sound", this.entity.transform.position);
84+
}
85+
}
86+
```
87+
88+
### SoundSystem Behavior
89+
90+
Here's a complete system for networked audio with client prediction:
91+
92+
```ts
93+
import { AudioSource, Behavior, Vector2 } from "@dreamlab/engine";
94+
import { SettingsManager } from "./settings-manager.ts";
95+
96+
export default class SoundSystem extends Behavior {
97+
onInitialize(): void {
98+
this.game.network.onReceiveCustomMessage((senderId, channel, data) => {
99+
if (channel === "playSound" && this.game.isServer()) {
100+
// Broadcast to OTHER clients (sender already played it locally)
101+
const connections = Array.from(this.game.network.connections) as any[];
102+
for (const conn of connections) {
103+
const clientId = typeof conn === "string" ? conn : (conn.id || conn.clientId || conn);
104+
if (clientId !== senderId) {
105+
this.game.network.sendCustomMessage(clientId, "playSoundLocal", data);
106+
}
107+
}
108+
}
109+
110+
if (channel === "playSoundLocal" && this.game.isClient()) {
111+
this.playLocalSound(data.soundName, data.position);
112+
}
113+
});
114+
}
115+
116+
private playLocalSound(soundName: string, position: { x: number; y: number }): void {
117+
const audioPath = this.game.local!._.Audio._[soundName];
118+
if (!audioPath) return;
119+
120+
const sound = audioPath.cloneInto(this.game.local!, {
121+
transform: { position: { x: position.x, y: position.y } },
122+
});
123+
124+
const audioSource = sound.cast(AudioSource);
125+
const userVolume = SettingsManager.getVolume();
126+
127+
setTimeout(() => {
128+
audioSource.volume = audioSource.volume * userVolume;
129+
audioSource.play();
130+
setTimeout(() => sound.destroy(), 2000);
131+
}, 0);
132+
}
133+
134+
static play(game: any, soundName: string, position: Vector2): void {
135+
if (game.isServer()) {
136+
// Server broadcasts to all clients
137+
const connections = Array.from(game.network.connections) as any[];
138+
for (const conn of connections) {
139+
const clientId = typeof conn === "string" ? conn : (conn.id || conn.clientId || conn);
140+
game.network.sendCustomMessage(clientId, "playSoundLocal", {
141+
soundName,
142+
position: { x: position.x, y: position.y },
143+
});
144+
}
145+
return;
146+
}
147+
148+
if (game.isClient()) {
149+
// Client plays sound locally immediately (no round-trip delay)
150+
const audioPath = game.local?._.Audio?._?.[soundName];
151+
if (audioPath) {
152+
const sound = audioPath.cloneInto(game.local, {
153+
transform: { position: { x: position.x, y: position.y } },
154+
});
155+
156+
const audioSource = sound.cast(AudioSource);
157+
const userVolume = SettingsManager.getVolume();
158+
159+
// For own sounds, override spatial settings to play at full volume
160+
setTimeout(() => {
161+
audioSource.minRange = 0;
162+
audioSource.maxRange = 0;
163+
audioSource.volume = 1.0 * userVolume;
164+
audioSource.play();
165+
setTimeout(() => sound.destroy(), 2000);
166+
}, 0);
167+
}
168+
169+
// Also tell server to broadcast to OTHER clients with entity position
170+
game.network.sendCustomMessage("server", "playSound", {
171+
soundName,
172+
position: { x: position.x, y: position.y },
173+
});
174+
}
175+
}
176+
}
177+
```
178+
179+
### How it Works
180+
181+
1. **Client triggers sound**: Calls `SoundSystem.play()` with sound name and position
182+
2. **Immediate local playback**: Client hears the sound instantly (no network delay)
183+
3. **Server broadcast**: Client tells server, which broadcasts to other clients
184+
4. **Remote playback**: Other clients play the sound at the specified position with spatial audio
185+
186+
> This pattern eliminates latency for the player who triggered the sound while keeping everyone else in sync.
187+
188+
## Quick Reference
189+
190+
| Task | Code |
191+
| ----------------------------- | ---------------------------------------------------------------------------- |
192+
| Play existing audio entity | `this.game.local!._["MySound"].cast(AudioSource).play()` |
193+
| Set spatial range | `audioSource.minRange = 5; audioSource.maxRange = 20;` |
194+
| Stop playing sound | `audioSource.stop()` |
195+
| Loop background music | Set `loop: true` on AudioSource component |
196+
197+
## Troubleshooting
198+
199+
| Symptom | Likely fix |
200+
| ------------------------------ | -------------------------------------------------------------------------- |
201+
| Sound doesn't play | Verify sound entity name |
202+
| Can't hear sound far away | Increase `maxRange` or disable spatial audio |
203+
| Sound plays twice in multiplayer | Use SoundSystem pattern to avoid echoes |
204+
| Volume too quiet | Check base volume, user settings, and spatial attenuation |
205+
| Overlapping sounds cut off | Clone the audio entity instead of reusing the same instance |
206+
207+
---
208+
209+
**Pro tip**: Keep all your audio entities organized under a single `Audio` container entity in `game.local` for easy reference:
210+
211+
```ts
212+
const sounds = this.game.local!._.Audio._;
213+
sounds["Jump"].cast(AudioSource).play();
214+
sounds["Shoot"].cast(AudioSource).play();
215+
```

0 commit comments

Comments
 (0)