Lesson Goal
Lessons 4 through 8 gave you combat, space, quests, and loot. This lesson gives those moments ears: a bus layout you can mix once, spatial rules for world SFX, and stingers that fire when progress changes without fighting your music.
By the end of this lesson, you will:
- define a DefaultBusLayout with dedicated buses for SFX, ambience, music, and UI
- place AudioStreamPlayer2D nodes for footsteps, hits, and pickups with sane attenuation
- run two or more ambience layers (for example wind plus fire crackle) under one parent ambience policy
- trigger short one-shot music or chord stingers from quest or inventory events without restarting the main loop
Step 1 - Bus layout in the project
Open Project Settings > Audio > Buses. Keep Master as the root.
Add child buses (exact names help tutorials and autoloads match):
- SFX — gameplay one-shots and spatial sounds
- Ambience — looping beds, low-pass optional later
- Music — exploration or combat stems
- UI — menus, inventory blips, dialogue advance clicks
Set every new bus to route to Master. Leave solo/mute off; you will balance in the dock during playtest.
Mini task:
Lower Ambience by about 6 dB and Music by about 3 dB versus SFX so combat hits still punch through on first listen.
Common mistake:
Putting every stream on Master. You lose the ability to duck music under dialogue later without touching individual players.
Step 2 - Tag players with buses
Any AudioStreamPlayer or AudioStreamPlayer2D has a bus property. Set defaults in scenes:
- pickups and weapon swishes:
bus = "SFX" - looping wind or cave tone:
bus = "Ambience" - exploration pad:
bus = "Music" - pause menu tick:
bus = "UI"
Pro tip:
Name nodes after role: SfxPickup, AmbWind, MusExplore, UiClick. That makes Signal connections readable in Lesson 10 when UI grows.
Step 3 - Spatial SFX with AudioStreamPlayer2D
On your pickup scene from Lesson 8, add an AudioStreamPlayer2D child:
- assign a short
.wavor.oggpickup cue bus = "SFX"max_distancearound1200(adjust to your tile size)attenuation1.0or slightly higher if you want faster falloff
Call play() in the same frame you grant the item:
$PickupSound.play()
Common mistake:
Using AudioStreamPlayer (non-spatial) for world objects. The cue will be equally loud everywhere and break immersion in larger rooms.
Step 4 - Ambience parent scene
Create audio/ambience_layer.tscn with root Node2D (or Node3D if you later go 3D). Add two children:
| Child | Role |
|---|---|
WindLoop |
AudioStreamPlayer2D, long seamless loop, bus = "Ambience", autoplay = true |
FireCrackle |
optional second AudioStreamPlayer2D near a campfire, tighter max_distance |
2D attenuation trick:
Campfire crackle uses a smaller max_distance than wind so it drops off when the player crosses the village edge; wind stays wide.
Instance ambience_layer under your main level root so it moves with the scene, not with the player.
Step 5 - Music loop and simple stinger channel
Add an autoload MusicDirector at res://systems/audio/music_director.gd (Project Settings > Autoload), or keep it as a singleton node in the main scene if you prefer fewer globals:
extends Node
var _music: AudioStreamPlayer
func _ready() -> void:
_music = AudioStreamPlayer.new()
_music.bus = "Music"
_music.volume_db = -6.0
add_child(_music)
func play_explore_loop(stream: AudioStream) -> void:
if _music.stream == stream and _music.playing:
return
_music.stream = stream
_music.play()
func play_stinger(stream: AudioStream) -> void:
var one_shot := AudioStreamPlayer.new()
one_shot.bus = "Music"
one_shot.stream = stream
one_shot.finished.connect(one_shot.queue_free)
add_child(one_shot)
one_shot.play()
play_stinger spins a temporary player so your exploration loop keeps running. Keep stingers under 2 seconds for action-adventure pacing.
Pro tip:
If stingers still feel loud, add a Music bus Compressor effect later; this lesson only needs clean routing.
Step 6 - Hook stingers to quest and inventory signals
From Lesson 7, your QuestRunner (or equivalent) likely emits something when a step completes. From Lesson 8, Inventory.item_added or inventory_changed is available.
Example: quest complete stinger (autoload name must match Project Settings)
func _on_quest_completed(_quest_id: String) -> void:
MusicDirector.play_stinger(load("res://audio/music/stinger_quest_done.ogg") as AudioStream)
Example: rare pickup flair
func _on_inventory_changed() -> void:
if Inventory.has_at_least("cellar_key", 1) and not _key_fanfare_done:
_key_fanfare_done = true
MusicDirector.play_stinger(load("res://audio/music/stinger_key.ogg") as AudioStream)
Use a bool guard so reloading a save does not spam the sting.
Troubleshooting
- No sound at all:
Project Settings > Audio > Driverdevice mismatch on Windows; try restarting editor after changing interface. - Spatial sound inaudible: player node not in scene tree under viewport, or
max_distancefar too small versus camera scale. - Double stinger: two connections to the same signal after hot reload; disconnect in
_exit_treeor use a one-shot guard flag. - Loops click: trim audio files at zero crossings or use
.oggloops exported with seam metadata.
Common Mistakes to Avoid
- driving music volume by scaling Master only (drowns UI cues)
- 10+ simultaneous ambience loops without planning CPU (start with two beds + occasional one-shots)
- shipping without a reference level: pick a peak for SFX and normalize others to it
Mini Challenge
Add a rain layer that only plays when a ProgressState flag weather_is_storm is true: tween Ambience bus volume or crossfade a second AudioStreamPlayer2D over 1 second when the flag flips.
FAQ
Should ambience be Mono or Stereo?
Stereo beds are fine for non-diegetic wind; positional crackle should usually be mono for predictable panning in 2D.
Can I use AnimationPlayer for stingers?
Yes. A track that calls play_stinger on one frame works well for cut-scene style beats.
Where does Web export matter?
Browser autoplay rules may block audio until the first user gesture; plan a title-screen click that calls AudioServer.set_bus_volume_db or starts a silent one-frame player.
Quick Recap
You now have:
- a bus spine that separates SFX, ambience, music, and UI
- spatial pickups and props using
AudioStreamPlayer2D - a pattern for layered ambience and non-blocking stingers
Next, you will build UI and onboarding flow so volume sliders and remappable actions respect these buses. Continue to Lesson 10: UI and Onboarding Flow.
For signal discipline, revisit Lesson 3: Scene and Node Foundations. For hooking audio to loot, keep Lesson 8: Inventory and Item Systems open.
Bookmark this lesson before Lesson 12 performance passes: mixing early avoids painting yourself into a wall of simultaneous voices.
