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