In this part we are going to add that final touch of life to our game by adding some music and SFX (sound effects) to our game. We want there to be background music when we are in our pause, death, and main menu screens, as well as in our Main scene. We’ll also add shooting and damage sound effects to our Player and Enemy, as well as dialog music and pickup effects.
WHAT YOU WILL LEARN IN THIS PART:
· How to work with the AudioStreamPlayer node.
· How to work with the AudioStreamPlayer2D node.
· How to play, set, and stop audio streams within your code.
· How to loop audio files in the Import Dock.
In Godot, you have three primary options for playing audio:
AudioStreamPlayer: This is a general audio player suitable for playing music or any non-positional audio. Use this when you don’t need the audio to have a position in the world. For example, if you have background music or dialog music that should play irrespective of the position of the characters or objects in the game, use AudioStreamPlayer.
AudioStreamPlayer2D: This is designed for 2D games where you need positional audio. It stimulates the position of the sound in a 2D space and will make sounds quieter the further away they are from the listener (camera, usually). Use this when you are making a 2D game and you want the audio to have a position in your 2D world (e.g., a sound effect that occurs at a certain location on the screen).
AudioStreamPlayer3D: Similar to AudioStreamPlayer2D, but for 3D games. It takes into account the position of the sound in three-dimensional space. Use this in 3D games when you want the sound to emanate from a specific location in the 3D world.
Throughout this tutorial, we will use our AudioStreamPlayer node for constant stable sounds such as our Background music or Dialog music. We will use our AudioStreamPlayer2D node however for our sound effects — because we want some panning in the sound and we also want the sound loudness to depend on the location of the sound.
MAIN MENU MUSIC
Open up your MainScene scene, and add a new node called AudioStreamPlayer. Rename this node to “BackgroundMusic”.
In the Inspector panel, we can assign the audio file that should play for this audio player node. Click on next to your Stream property in your Inspector panel and select “Quick Load”. This will set the AudioStream object to be played.
For our background music, we want the audio file “We Ride at Dawn.wav” to play. You can find all the music files underneath your Assets > Music directory.
In the Inspector panel, you can set its playing and auto-playing values. If the playing value is true, the audio is playing or is queued to be played (see play). If autoplay is true, the audio plays when the scene is loaded. I recommend you read the documentation to see what the rest of the properties do, such as bus or mix target.
We want this music to play as soon as the game loads, so we need to enable “autoplay”. If you want to listen to the music, you can enable “playing”, but make sure you disable it afterward!
If you now run your scene, your music should play by default but only when you are in your Main Menu scene.
PAUSE MENU
In our Player scene, let’s add an AudioStreamPlayer node. Rename it to “PauseMenuMusic”.
Let’s organize all of our music underneath a Node2D node renamed to “GameMusic”.
We also want the audio to be “We Ride at Dawn.wav” when our pause menu is open. Do not enable autoplay or playing, as we will set this node to play in our code only when our pause menu is open.
To play our audio, we simply need to reference our node and then call its .play() method. This method plays the audio in seconds. We will play this when our game is paused after we’ve called our ui_pause input.
### Player.gd
# Audio nodes
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
func _input(event):
# older code
#show pause menu
if !pause_screen.visible:
if event.is_action_pressed("ui_pause"):
#play music
pause_menu_music.play()
#pause game
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
get_tree().paused = true
#show pause screen popup
pause_screen.visible = true
#stops movement processing
set_physics_process(false)
#set pauses state to be true
paused = true
# if the player is dead, go to back to main menu screen
if health <= 0:
get_node("/root/%s" % Global.current_scene_name).queue_free()
Global.change_scene("res://Scenes/MainScene.tscn")
get_tree().paused = false
return
We also need to set our PauseMenuMusic node’s process mode to “When Paused”, because we only want this to process when our game is in the paused state.
We also need to stop our music from playing when we quit our scene or resume our game — otherwise, it will play over the other audio. We can do this via the stop() method.
# ---------------- Pause Menu -------------------------------------------
#resume game
func _on_resume_pressed():
#hide pause menu
pause_screen.visible = false
#set pauses state to be false
get_tree().paused = false
paused = false
#accept movement and input
set_process_input(true)
set_physics_process(true)
Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
#stop music
pause_menu_music.stop()
Now if you run your scene, and you pause/unpause, your pause music should play correctly.
BACKGROUND MUSIC
We also need music to play during the game loop. We will attach this node to our Player scene because we want this sound to follow them around. Let’s add another AudioStreamPlayer node to our Player scene and call it “BackgroundMusic”.
We want this audio track to be “We Don’t Need Railroads.wav”. Also, enable autoplay because we want this audio track to play by default.
We need to stop this music from playing when our pause menu is open, so update your ui_pause input to stop the background music audio track.
### Player.gd
# Audio nodes
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
func _input(event):
# older code
#show pause menu
if !pause_screen.visible:
if event.is_action_pressed("ui_pause"):
#pause game
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
get_tree().paused = true
#show pause screen popup
pause_screen.visible = true
#stops movement processing
set_physics_process(false)
#set pauses state to be true
paused = true
#play music
background_music.stop()
pause_menu_music.play()
# if the player is dead, go to back to main menu screen
if health <= 0:
get_node("/root/%s" % Global.current_scene_name).queue_free()
Global.change_scene("res://Scenes/MainScene.tscn")
get_tree().paused = false
return
Our background music should also play after we’ve pressed our confirm and resume buttons.
### Player.gd
# close popup
func _on_confirm_pressed():
level_popup.visible = false
get_tree().paused = false
Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
background_music.play()
# ---------------- Pause Menu -------------------------------------------
#resume game
func _on_resume_pressed():
#hide pause menu
pause_screen.visible = false
#set pauses state to be false
get_tree().paused = false
paused = false
#accept movement and input
set_process_input(true)
set_physics_process(true)
Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
#stop music
pause_menu_music.stop()
background_music.play()
If our game is over, we will play another sound — so we also need to stop our background music to play in our death conditional.
### Player.gd
# ------------------- Damage & Death ------------------------------
#does damage to our player
func hit(damage):
health -= damage
health_updated.emit(health, max_health)
if health > 0:
#damage
animation_player.play("damage")
health_updated.emit(health, max_health)
else:
#stop background music
background_music.stop()
#death
set_process(false)
get_tree().paused = true
paused = true
animation_player.play("game_over")
The same goes for our update_xp function.
### Player.gd
# ----------------- Level & XP ------------------------------
#updates player xp
func update_xp(value):
xp += value
#check if player leveled up after reaching xp requirements
if xp >= xp_requirements:
#stop background music
background_music.stop()
If you run your scene, your music should play when you’re in the game.
GAME OVER MUSIC
When our player dies, we also want to play our GameOverMusic. For this, we need to add another AudioStreamPlayer node to our Player scene.
We want this audio track to be “Too Late To Save The Town.wav”. Also, change its processing mode to “When Paused”.
Update your hit() function to play the audio after the background music has been stopped.
### Player.gd
# Audio nodes
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
# ------------------- Damage & Death ------------------------------
#does damage to our player
func hit(damage):
health -= damage
health_updated.emit(health, max_health)
if health > 0:
#damage
animation_player.play("damage")
health_updated.emit(health, max_health)
else:
#death
set_process(false)
get_tree().paused = true
paused = true
animation_player.play("game_over")
#stop background music
background_music.stop()
game_over_music.play()
If you run your scene, your music should play when your player dies.
DIALOG MUSIC
We’ll have to set our dialog music in two places, which are in our Player and DialogPopup scripts. If our player interacts with the NPC, the dialog music should play — and if the player is done interacting and our popup closes, our background music should play.
Add a new AnimationPlayer node with the audio track “Whiskey Barn Dance (loop).wav”. Set its processing mode to “When Paused”. You can also use the “Imposter Syndrome (wav)” audio for this.
We’re playing the audio in the Player script because if we had to do it in our DialogPopup’s open() function the music would restart each time the popup changes! Play and stop the Dialog Music as follows:
### Player.gd
# Audio nodes
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
@onready var dialog_music = $GameMusic/DialogMusic
# ------------------- Damage & Death ------------------------------
#does damage to our player
func _input(event):
# older code
#interact with world
elif event.is_action_pressed("ui_interact"):
var target = ray_cast.get_collider()
if target != null:
if target.is_in_group("NPC"):
# Talk to NPC
target.dialog()
# Music
background_music.stop()
dialog_music.play()
return
### DialogPopup.gd
extends CanvasLayer
# Node refs
@onready var animation_player = $"../../AnimationPlayer"
@onready var player = $"../.."
@onready var background_music = $"../../GameMusic/BackgroundMusic"
@onready var dialog_music = $"../../GameMusic/DialogMusic"
#closes the dialog
func close():
get_tree().paused = false
self.visible = false
player.set_physics_process(true)
Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
# Music
dialog_music.stop()
background_music.play()
If you now run your scene, your dialog music should play when your player interacts with the NPC.
LEVEL UP MUSIC
When our player level’s up, we want to play a little victory tune. We will still use the AnimationPlayer node for this because we want the audio noise to come from a central point — we don’t want it to pan between our left and right ear.
Add a new node and call it “LevelUpMusic”. We want the audio to be “Retro PowerUP StereoUP 05.wav”.
Now, in our update_xp() function, we want to play our LevelUpMusic.
### Player.gd
# Audio nodes
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
@onready var dialog_music = $GameMusic/DialogMusic
@onready var level_up_music = $GameMusic/LevelUpMusic
# ----------------- Level & XP ------------------------------
#updates player xp
func update_xp(value):
xp += value
#check if player leveled up after reaching xp requirements
if xp >= xp_requirements:
#stop background music
background_music.stop()
level_up_music.play()
If you run your scene, your LevelUpMusic music should play when your player levels up after completing quests and shooting enemies.
PICKUPS SOUND EFFECT
If our player runs over some ammo or a drink, or even our quest items, we want to play a pickup sound effect. For this, we will use an AudioStreamPlayer2D node, since we want some audio attenuation for our sound effects. It gives the sound a bit more of a realistic feel, as it plays more like environmental background noise.
You can attach this node to something like a fire scene, which, depending on how far/close you are from the fire, the sound volume will differ. Since we’ll be adding this sound to our Player, the sound won’t attenuate much since our camera always focuses on our player.
Add a new AudioStreamPlayer2D node and call it “PickupsMusic”. Set its audio file to “stamfull.wav”.
In our code, we need to play this audio when our player runs over a pickup. We can do this in our add_pickup() function.
### Player.gd
# Audio nodes
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
@onready var dialog_music = $GameMusic/DialogMusic
@onready var level_up_music = $GameMusic/LevelUpMusic
@onready var pickups_sfx = $GameMusic/PickupsMusic
# ---------------------- Consumables ------------------------------------------
# Add the pickup to our GUI-based inventory
func add_pickup(item):
if item == Global.Pickups.AMMO:
ammo_pickup = ammo_pickup + 3 # + 3 bullets
ammo_pickups_updated.emit(ammo_pickup)
print("ammo val:" + str(ammo_pickup))
if item == Global.Pickups.HEALTH:
health_pickup = health_pickup + 1 # + 1 health drink
health_pickups_updated.emit(health_pickup)
print("health val:" + str(health_pickup))
if item == Global.Pickups.STAMINA:
stamina_pickup = stamina_pickup + 1 # + 1 stamina drink
stamina_pickups_updated.emit(stamina_pickup)
print("stamina val:" + str(stamina_pickup))
# SFX
pickups_sfx.play()
update_xp(5)
Now if you run over your pickups items, the audio should play!
CONSUMING SOUND EFFECT
We want a sound effect to play each time our player consumes a health or stamina drink by pressing “1” or “2”.
Add another AudioStreamPlayer2D node and call it “ConsumableMusic”. Set its audio file to “stam1.wav”.
In our input code, let’s update our ui_consume_health and ui_consume_stamina inputs to play our consumable sound effects. You can also make both of these different audio streams by loading a new stream resource into your ConsumableMusic node before playing it.
### Player.gd
# Audio nodes
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
@onready var dialog_music = $GameMusic/DialogMusic
@onready var level_up_music = $GameMusic/LevelUpMusic
@onready var pickups_sfx = $GameMusic/PickupsMusic
@onready var consume_sfx = $GameMusic/ConsumableMusic
func _input(event):
# older code
#using health consumables
elif event.is_action_pressed("ui_consume_health"):
if health > 0 && health_pickup > 0:
health_pickup = health_pickup - 1
health = min(health + 50, max_health)
health_updated.emit(health, max_health)
health_pickups_updated.emit(health_pickup)
# SFX
consume_sfx.play()
#using stamina consumables
elif event.is_action_pressed("ui_consume_stamina"):
if stamina > 0 && stamina_pickup > 0:
stamina_pickup = stamina_pickup - 1
stamina = min(stamina + 50, max_stamina)
stamina_updated.emit(stamina, max_stamina)
stamina_pickups_updated.emit(stamina_pickup)
# SFX
consume_sfx.stream = load("res://Assets/FX/Music/Free Retro SFX by @inertsongs/SFX/stam0.wav")
consume_sfx.play()
Now if you run over your pickups items, and consume them, the audio should play!
BULLET IMPACT SOUND EFFECT
When a bullet hits our Player or Enemy, we want the bullet impact sound to play. We will add this audio in our Enemy and Player scenes.
Let’s play the “Retro Impact LoFi 09.wav” sound using the AudioStreamPlayer2D node which we’ll rename as “BulletImpactMusic”. Add this node to both your Enemy and Player scenes.
After we impact with our node, let’s play this sound.
### Enemy.gd
# Audio nodes
@onready var bullet_sfx = $GameMusic/BulletImpactMusic
#will damage the enemy when they get hit
func hit(damage):
health -= damage
if health > 0:
#damage
animation_player.play("damage")
# SFX
bullet_sfx.play()
### Player.gd
# Audio nodes
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
@onready var dialog_music = $GameMusic/DialogMusic
@onready var level_up_music = $GameMusic/LevelUpMusic
@onready var pickups_sfx = $GameMusic/PickupsMusic
@onready var consume_sfx = $GameMusic/ConsumableMusic
@onready var bullet_sfx = $GameMusic/BulletImpactMusic
# ------------------- Damage & Death ------------------------------
#does damage to our player
func hit(damage):
health -= damage
health_updated.emit(health, max_health)
if health > 0:
#damage
animation_player.play("damage")
health_updated.emit(health, max_health)
# SFX
bullet_sfx.play()
Now if we run our scene and we hit the enemy or the enemy hits us with a bullet, our sound effect should play.
SHOOTING SOUND EFFECT
When we shoot our weapons, we also want our guns to play a shooting sound effect. We’ll do this for both our Enemy and our Player.
Let’s play the “Retro Weapon Gun LoFi 03.wav” sound using the AudioStreamPlayer2D node which we’ll rename as “ShootingMusic”. Add this node to both your Enemy and Player scenes.
Let’s play this sound effect in our ui_attack input.
### Player.gd
# Audio nodes
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
@onready var dialog_music = $GameMusic/DialogMusic
@onready var level_up_music = $GameMusic/LevelUpMusic
@onready var pickups_sfx = $GameMusic/PickupsMusic
@onready var consume_sfx = $GameMusic/ConsumableMusic
@onready var bullet_sfx = $GameMusic/BulletImpactMusic
@onready var shooting_sfx = $GameMusic/ShootingMusic
func _input(event):
#input event for our attacking, i.e. our shooting
if event.is_action_pressed("ui_attack"):
#checks the current time as the amount of time
var now = Time.get_ticks_msec()
#check if player can shoot if the reload time has passed and we have ammo
if now >= bullet_fired_time and ammo_pickup > 0:
#SFX
shooting_sfx.play()
#shooting anim
is_attacking = true
var animation = "attack_" + returned_direction(new_direction)
animation_sprite.play(animation)
#bullet fired time to current time
bullet_fired_time = now + bullet_reload_time
#reduce and signal ammo change
ammo_pickup = ammo_pickup - 1
ammo_pickups_updated.emit(ammo_pickup)
For our enemy, we’ll need to update its process() function to also have a reload time so that it doesn’t loop the audio excessively together. We did this in the Player script.
### Enemy.gd
# Audio nodes
@onready var bullet_sfx = $GameMusic/BulletImpactMusic
@onready var shooting_sfx = $GameMusic/ShootingMusic
#------------------------------------ Damage & Health ---------------------------------
func _process(delta):
#regenerates our enemy's health
health = min(health + health_regen * delta, max_health)
#checks the current time as the amount of time passed
var now = Time.get_ticks_msec()
#check if enemy can shoot
if now >= bullet_fired_time:
# What's the target?
var target = $RayCast2D.get_collider()
if target != null:
if target.name == "Player" and player.health > 0:
# SFX
shooting_sfx.play()
#shooting anim
is_attacking = true
var animation = "attack_" + returned_direction(new_direction)
animation_sprite.play(animation)
#reload time to bullet fired time
bullet_fired_time = now + bullet_reload_time
Now if you run your scene, your player’s shooting sound should play when you press CTRL, and the enemy’s shooting sound should play when they attack you.
ENEMY DEATH SOUND EFFECT
Finally, we also want to play a sound effect when our enemy dies.
Add a new AudioPlayer2D node to your EnemySpawner scene and call it “Death Music”. The audio file should be “dmg0.wav”.
We will play this sound effect when our spawner decreases our enemy count by 1.
###EnemySpawner.gd
# Audio nodes
@onready var death_sfx = $GameMusic/DeathMusic
# Remove enemy
func _on_enemy_death():
enemy_count = enemy_count - 1
death_sfx.play()
If you run your scene and kill the enemies, the sound effect should play.
PLAYING DIFFERENT MUSIC IN OUR MAIN SCENES
In our Main_2 scene, we want to play our “Imposter Syndrome (wav)” audio track instead of the Background music we assigned to our Player. To do this, we can simply re-assign our stream resource and then play the node again.
Before we do this, we have to re-import our “Imposter Syndrome (wav)” file to be a looped audio file. If we don’t do this, it plays and then stops when it finishes. To re-import it, click on it and open your Import Dock. Set its import mode to “Forward”, and then re-import it.
You’ll notice that we haven’t imported any of our other audio files with looping enabled, and that is because the default import settings for WAV files in Godot 4 are set to recognize and utilize looping, while MP3 settings are not. Thus, if we were using the audio files with a .mp3 extension, we would’ve imported them to enable looping, but since we were using .wav audio files, the Godot engine automatically recognized which audios should loop.
### Main_2.gd
extends Node2D
@onready var background_music = $Player/GameMusic/BackgroundMusic
#connect signal to function
func _ready():
background_music.stream = load("res://Assets/FX/Music/Free Retro SFX by @inertsongs/Imposter Syndrome (theme).wav")
background_music.play()
# Change scene
func _on_trigger_area_body_entered(body):
if body.is_in_group("player"):
Global.change_scene("res://Scenes/Main.tscn")
Global.scene_changed.connect(_on_scene_changed)
#only after scene has been changed, do we free our resource
func _on_scene_changed():
queue_free()
Now if you run back and forth between your different scenes, your music should change.
And there you have it. You now have a full game with music, a GUI, enemies, an NPC, and a basic quest! If this is the end of the road for you, and you are ready to move on to the next project, then I’ll see you in the next part when I show you how test, debug, and export your project. Remember to save and make a backup, and I’ll see you in the next part.
The final source code for this part should look like this.
FULL TUTORIAL
The tutorial series has 23 chapters. I’ll be releasing all of the chapters in sectional daily parts over the next couple of weeks.
If you like this series or want to skip the wait and access the offline, full version of the tutorial series, you can support me by buying the offline booklet for just $4 on Ko-fi!😊
You can find the updated list of the tutorial links for all 23 parts in this series here.
Top comments (3)
Really great piece. It was really insightful . I would like to know how you made the screenshots by the way ,if you dont mind ?
Thank you! I use the Windows snipping tool for screenshots. You can use it by pressing Windows Key + Shift + S
Oh great..Thank you. You've been so helpful