In this episode, we’re going to address an often overlooked aspect of game development, at least in the early stages: sound! We’re adding background music and dynamic footstep sounds that change based on what surface you’re walking on.
Adding audio to your game can significantly alter how the experience feels, but it often gets pushed to the end of development (at least in our experience). This can lead to a game that looks great but feels flat or unpolished. By integrating sound early, we can create a more immersive and engaging experience.
Episode Project Files
DownloadBefore you continue, you'll want to download these assets as we'll be using them in this episode. These assets are bundled in a ZIP file that you'll need to extract before copying the files into your project.
Thanks for the music, Sealios!
The music tracks created for this episode were provided by Sealios Spring, a Zelda fan who we found trying to trick his friends by emulating Koji Kondo’s style. We reached out to him and he was kind enough to create some original tracks for us that you are free to use in your own projects.
Adding background musicJump to 0:20
Our first step is to add an AudioStreamPlayer node and rename it to something like BackgroundMusic. In the Inspector panel, we can use the Stream property to select our desired music file.
You’ll find the available music tracks in the Audio / Music folder of the assets for this episode.
One important detail: you may need to double-click your MP3 file in the FileSystem and check the Loop checkbox, then reimport. Otherwise your music will play once and stop, which isn’t what we usually want for background music.
Streaming vs Sampling
Godot supports both WAV (uncompressed, higher quality, larger files) and MP3 (compressed, smaller files, slight quality loss).
Beyond file format, you can also choose how Godot handles the audio:
- Stream: Reads from disk during playback. Uses less memory but has a tiny startup delay. Perfect for music.
- Sample: Loads entirely into memory. Instant playback, but uses more RAM. Ideal for sound effects.
We typically use MP3 files imported as Stream for background music to keep our project size and memory usage low, and WAV files imported as Sample for sound effects to ensure instant, responsive playback.
Setting up audio busesJump to 1:33
Before we go any further, we should set up our audio “routing” system. These are called “Audio Buses” and you can think of them like channels on a mixing board… if you know what that is. Basically they let us control different groups of sounds and music independently.
The reason we’re starting with this step is to avoid having to redo audio settings later. Whenever we add a new audio node to our project, we can just choose the appropriate bus and not have to come back to it later.
Open the Audio bus panel (you’ll find it at the bottom of the editor) and let’s create our bus structure. We’ll leave the default Master bus1 as it is and add a new bus called User. This will act like a new master bus for the buses we create next, but we’ll allow players to adjust the volume of this bus in the settings in the future.
Next we’ll create 2 more buses and route both of them into the User bus we just created (not Master directly). These buses will be called Music and SoundFX, and I bet you can guess what they will be used for.
This setup gives us flexibility. Players can adjust their overall volume (User bus) or fine-tune music vs sound effects separately. You can even mute buses during development by clicking the “M” button. After hearing the same music loop 500 times, you’ll appreciate this feature2.
Back on our BackgroundMusic node, be sure to set the Bus property to “Music” and check that the Autoplay option is enabled. This way, the music starts as soon as the scene loads. Give it a try and make sure everything sounds good.
Preparing our footstep sound playerJump to 4:06
Now let’s move over to our player.tscn scene. We’re going to add an AudioStreamPlayer3D node as a child of our Player node. The 3D version is important here, as it makes our footstep sounds come from the player’s position in 3D space.
Rename this node to Footsteps and make sure it’s positioned at the bottom of the character where the footsteps would naturally come from3.
In the Inspector, let’s temporarily set the Stream property to one of our stone step sounds (we’ll replace this with something more dynamic soon). Oh, and don’t forget to set the Bus property to “SoundFX” since footsteps are sound effects.
Triggering footstep sounds from animationJump to 4:50
Now we need to trigger the footstep sounds at the right time during our run animation. The problem is, our animation is imported from an external file (our character model), so we can’t edit it directly. We need to save a custom version first.
In the FileSystem panel, navigate to the character model in the Meshes folder and double click to open it. Using the list on the left, locate the freehand_run animation. Now on the right hand side, look for the “Save to File” section. We want to toggle on the Enabled option here and then click the button to select the filepath where we’ll save the modified animation track.
Create an Animations folder and save it there as something like freehand_run_custom.tres. Then make sure to check Keep custom tracks, otherwise we risk losing the changes we’re about to make to this animation if/when we reimport the character model later.
Now in the AnimationPlayer node, we can select the run animation.
Animation preview quirks
Sometimes changing the animation in the AnimationPlayer doesn’t cause the animation to change on the character model in the 3D viewport. This is because we’re actually relying on the AnimationTree node to control which animation is playing.
If you encounter this issue, just switch over to the AnimationTree node and use the dropdown on the
movementTransition node to switch to the “run” state. This should force the animation to update in the viewport.
With the run animation selected, click “Add Track” and choose “Audio Playback Track” and select the Footsteps node as the target. This adds a new audio track to the animation timeline.
Now scrub through the animation to find where each foot hits the ground. Insert keyframes slightly before impact (our sounds have a tiny amount of latency, so this helps account for that). Click on the keyframe to show its properties in the Inspector panel, then select stone_step_1.wav sound from the Audio / Footsteps folder. Repeat this process for the other foot.
The AudioStreamRandomizerresourceJump to 10:10
Here’s where things get interesting. The brain is great at picking up patterns and noticing repetition, and if every footstep sounds exactly the same, it’s going to be noticeable real quick.
Thankfully, Godot has a built-in resource called AudioStreamRandomizer that can help us out. Our AudioStreamPlayer nodes are designed to work with any AudioStream resource, not just single audio files. This means we can swap out our static audio file for a randomizer that plays different sounds each time.
Click on one of your audio keyframes and look at the Stream property. Instead of loading a single audio file, click the dropdown and create a new AudioStreamRandomizer . Then click into that our new randomizer resource and you’ll see a Streams property where you can add elements, aka, different audio streams to randomize between.
We have 4 different stone step sounds, so we need to add a total of 4 elements. In the first element, we’ll select stone_step_1.wav, then stone_step_2.wav for the second, then stone_step_3.wav, and finally stone_step_4.wav for the last one.
We can then adjust the Random Pitch to about 1.2 and Random Volume Offset DB to 0.1 to add a bit more variation every time we play a sound using this randomizer.
Once you’re done, go ahead and give it a try before moving on. As you run around, you should hear different some slightly different sounds each time your foot hits the ground.
Saving our randomizer as a reusable resourceJump to 11:30
Before we move on, we’re going to save what we did as a reusable resource. That way we can easily reuse this resource in other keyframes, scripts, and so on — without having to repeat the previous steps each time.
Click the dropdown next to the resource and choose “Save As” to save it as a .tres file in the the same folder as the footstep sounds themselves. Just name it stone_step_sounds.tres, it’ll make sense why in just a moment.
Attaching custom data to our worldJump to 13:25
Here’s where we get fancy. We want different surfaces to make different sounds. To do this, we need two custom scripts:
- A script to attach to each surface in our level that holds a reference to which footstep sound randomizer to use.
- A script to attach to our Footsteps node that checks what surface we’re currently standing on and plays the appropriate sound.
Go ahead create a new script called footstepper.gd and save it in the Scripts folder. Then create another script named worldsound.gd and open it. This script is going to be super simple as it just holds a reference to whatever AudioStream resource we want to use for this surface.
1class_name WorldSounds
2extends Node
3
4@export var footstep_sounds: AudioStreamThe class_name keyword is a new one, but it’s very simple: It lets us reference this script type from other scripts. Remember how we declare the type of a variable or parameter after the name (and a colon)? By using class_name, we can now use WorldSounds as a type to reference this script from other scripts.
Pop over to the start.tscn scene and attach this script to the StaticBody3D under the level node in the scene tree. If we select the node and look at its property in the Inspector, we can now see the footstep_sounds property4 we created in the script.
Using the dropdown and selecting “Quick Load” we can assign our stone_step_sounds.tres resource to this property. This means that whenever we walk on this surface, we’ll use the stone footstep sounds.
Creating randomizers for each surface typeJump to 15:55
Down in the FileSystem, under the Audio / Footsteps folder, find the stone_step_sounds.tres file we created earlier. Right click on it and select “Duplicate” to create a copy and name this copy wood_step_sounds.tres.
Double click it to show its properties in the Inspector panel and swap out the audio streams in the Streams list to use wood_step_1.wav, wood_step_2.wav, wood_step_3.wav, and wood_step_4.wav. Locate the StaticBody3D node under the wood node in the scene tree and attach the WorldSounds script to it. Then assign the wood_step_sounds.tres resource to its footstep_sounds property.
Repeat this process for the other surface types (sand, grass, dirt), creating a new randomizer resource for each one, and assigning it to the appropriate StaticBody3D node in the scene tree.
A little hiccup
Unfortunately, when making the
test_levelasset for this course, we accidentally swapped the object names and materials for thedirtandgrassmeshes. So you’ll want to assign thedirt_step_sounds.tresresource to the StaticBody3D under the grass node, and vice versa for thegrass_step_sounds.tresresource.
Creating the FootstepperScriptJump to 18:58
Right now, nothing is going to happen when we walk on different surfaces. This is where our new footstepper.gd script comes in. Let’s look at the full code first, then we’ll break it down step-by-step.
1extends AudioStreamPlayer3D
2
3@onready var character: CharacterBody3D = get_parent()
4
5func play_footstep() -> void:
6⇥if not character.is_on_floor():
7⇥⇥return
8
9⇥for index in character.get_slide_collision_count():
10⇥⇥var col := character.get_slide_collision(index)
11
12⇥⇥if col.get_normal() == character.get_floor_normal():
13⇥⇥⇥var collider := col.get_collider()
14
15⇥⇥⇥if collider is WorldSounds:
16⇥⇥⇥⇥var info: WorldSounds = collider
17⇥⇥⇥⇥stream = info.footstep_sounds
18⇥⇥⇥⇥break
19
20⇥play()First we’re going to extend AudioStreamPlayer3D since this script will be attached to our Footsteps node. Then we’ll get a reference to our player character (the parent node)5 so we can ask it about what its touching due to move_and_slide().
5func play_footstep() -> void:Next we’re going to create a new function called play_footstep(). This function will be called from our animation keyframes instead of directly playing a sound. The reason for this indirection is that we need to do some logic to figure out what sound to play first.
6⇥if not character.is_on_floor():
7⇥⇥returnOnce inside the function, we’ll first check if the character is on the floor using is_on_floor(). If we’re not on the floor, then it stands to reason that we’re not going to playing footstep sounds, so we’ll exit the function early using return to prevent any further code from running.
If we are on the floor, then we need to figure out what surface is the floor. Because the character could be touching multiple surfaces at once (including walls and ceilings), we need to loop through all the collision data from our last movement using get_slide_collision_count().
9⇥for index in character.get_slide_collision_count():
10⇥⇥var col := character.get_slide_collision(index)We can repeat a block of code using the for keyword. In this case, we want to loop from 0 to character.get_slide_collision_count() - 1. We’ll put the current index in a variable called index6 so we can use it inside the loop.
We’ll assign the collidion data to a new variable called col using get_slide_collision(index). We need to use this second function get the actual collision dta for the current index of the loop.
Why start at 0?
In most programming languages, counting starts at 0. This is because an
indexis actually an “offset” from the start of a list. So the first item is at index0, the second item is at index1, and so on.
12⇥⇥if col.get_normal() == character.get_floor_normal():
13⇥⇥⇥var collider := col.get_collider()For each collision, we check if it’s actually the floor by comparing normals to what the CharacterBody3D script is considering its floor. If it is, then we can get the actual object we collided with using get_collider().
15⇥⇥⇥if collider is WorldSounds:
16⇥⇥⇥⇥var info: WorldSounds = collider
17⇥⇥⇥⇥stream = info.footstep_sounds
18⇥⇥⇥⇥breakWe only care if the colliding object has our WorldSounds script attached, so we use the is keyword to check if our collider is of type WorldSounds. If it is, then we can safely cast it to that type by assigning it to a new variable called info that is explicitly typed as WorldSounds.
We can then access the footstep_sounds property we created earlier and assign it to the stream property, which is inherited from AudioStreamPlayer3D . Finally, we can stop looping using the break keyword. This “breaks” out of the loop, meaning we won’t process anymore surfaces, saving processing power that would be wasted.
20play()Finally, we call play() to tell the AudioStreamPlayer3D node to play whatever sound we just assigned to the stream property.
Connecting everything upJump to 28:54
The last step we need to do is replace our audio track keyframes with function calls. Head back to the player.tscn scene and drag the foot_stepper.gd script onto your Footsteps node to attach it.
In our AnimationPlayer , delete the audio track and add a new Call Method Track instead. Target the Footsteps node and insert keyframes at the same positions as before in the timeline. For each keyframe, select play_footstep as the method.
Hit play and run around your level. You should hear different footstep sounds as you move between stone, wood, grass, and other surfaces. The randomizer keeps it from sounding repetitive, and the animation timing keeps it synced where we want it to play.
Wrapping up
We’re working on getting another batch of assets done for the next few episodes, which is where we’ll be tackling state machines, combat, and enemies! In the meantime, if you have any questions about what we covered in this episode, feel free to ask in the Discord .
Want to compare your work to ours? You can find a copy of the project on our GitHub repository. We track each episode as its own commit, so you can see exactly what changed between each episode. Check your work!