I know two ways on how to handle audio in my game:
1) Use AudioStreamPlayers in every scene
Every button click sound effect gets a node in every scene that needs a click sound effect
So e.g. the pause menu scene has an AudioStreamPlayer with the click sound and the main menu scene as well

2) One global scene for all sounds
The click sound effect would only need one AudioStreamPlayer in this example, and that is in the GlobalAudio scene. Then, in the main menu script and pause menu script, I call the sound via GlobalAudio.click.play()
. “click” is defined as an @onready var click = $Path
in the GlobalAudio script.
Do you see any problems when going with option 2 for a game with like 150+ sound effects? Because in my opinion it’s the more organised one and keeps mistakes from happening (if I’m not using the 3D directional audio stuff).
Are there performance issues to have a scene with like 50 AudioStreamPlayers and more loaded at all times (but not playing more than 1 or 2 sounds at the same time)?
Option 2 without hesitation
Allows for much better control over audio, avoids AudioStreamPlayer “disappearing” along with your scenes even though they were still playing
1 Like
I’m using global for some sounds, and all my audio code is in a global script, but I am spawning some scene-local players at need. As far as I can tell, global players with some polyphony works fine for GUI sounds, music and the like, but if you want anything positional you need a new player per positional sound.
# in Audio.gd
func _play2d(pos: Vector2, variance: float, res: Resource) -> void:
var p: AudioStreamPlayer2D = AudioStreamPlayer2D.new()
p.position = pos
p.bus = "Game"
p.stream = res
p.pitch_scale = 1.0 + randf_range(-variance, variance)
p.max_distance = 2000.0
p.attenuation = 1.0
p.script = preload("res://Audio/2DSoundHandler.gd")
add_child(p)
func explosion(pos: Vector2) -> void:
_play2d(pos, 0.2, preload("res://Audio/Boom.wav"))
[...]
And:
# 2DSoundHandler.gd
extends AudioStreamPlayer2D
func _ready() -> void:
play()
func _process(_delta: float) -> void:
if !is_playing(): queue_free()
1 Like
Thanks for the answer! Do you experience any performance decreases?
Avoid globals where you can, keeping references to every music track you have forever sounds like it will eat up a player’s RAM and uncompressed audio will be big (still not gigabytes big though).
I agree that it’s obnoxious to have so many audio players for each button, little click sounds won’t take up as much space as music tracks, so that might be a worth while trade off for you. In which case I’d recommend using a polyphonic audio stream to play one-off sound effects on a single AudioStreamPlayer.
1 Like
Performance seems fine; I can have an absolute riot of explosions going on and the game is sitting pretty at 60fps on my (linux) dev system. Though admittedly it’s a bit of a beast. I’ve done some testing on a low-end system (linux again, but a 2015-era laptop) and not seen performance problems with sound.
1 Like
Have you tried using signals instead?
# 2DSoundHandler.gd
extends AudioStreamPlayer2D
func _ready() -> void:
play()
finished.connect(queue_free)
1 Like
For GUI stuff I have per-sound global audio players with polyphony.
# in Audio.gd
@onready var p_gui_select: AudioStreamPlayer = _setup_stream("GUI", 1, 1.05, preload("res://Audio/GUISelect.wav"))
@onready var p_alarm: AudioStreamPlayer = _setup_stream("GUI", 8, 1.15, preload("res://Audio/AlarmChime.wav"))
[...]
func _setup_stream(bus: String, polyphony: int, variance: float, res: Resource) -> AudioStreamPlayer:
var asr: AudioStreamRandomizer = AudioStreamRandomizer.new()
asr.add_stream(0, res)
asr.random_pitch = variance
var player: AudioStreamPlayer = AudioStreamPlayer.new()
player.stream = asr
player.bus = bus
player.max_polyphony = polyphony
add_child(player)
return player
func gui_select() -> void:
p_gui_select.play()
[...]
I have not tried using signals; I probably should; I’ll give that a try.
Edit: That worked. Thanks!