How to play a different step sound based on the terrain type in a 2D top down game?
In 2D top down games there is no reason to play the same step sound when walking on different terrain types. While it requires a certain workflow when working with tile maps it is pretty easy to achieve.
*In the video there is a slight delay as the movement speed is a bit higher than it should so the player moves faster than the next step occurs. Sorry about that :)
Tilemap workflow
I will assume that you are using tilemaps in your project. If not the setup will be probably simpler since its all about colliders and Z position. Yes Z position. I mention this because Unity 2D physics doesn’t use Z axis and it certainly doesn’t allow us to shoot a default recast in Z direction - Physics2D.Raycast() only works on XY plane. Ok lets start with setting up the tilemaps.
First we need to split our tilemaps so that our sand terrain is on a separate tilemap than the grass or anything else:
Ok with that done all we need is a collider on each terrain type (tilemap) and we are good to go? Not exactly:
Our tilemaps uses sorting layers to inform unity which on should be rendered on top. The z position doesn’t influence sorting layers. The problem is that we can have 2 tilemaps with the same sorting order so we can’t simply loop through a list of tilemaps to find the one that we need. We need to put the tilemaps each one on a different Z position. This is because we will want to use Physics2D.OverlapPoint() which:
Checks if a Collider overlaps a point in space.
(…)
If more than one Collider overlaps the point then the one returned will be the one with the lowest Z coordinate value.
Keep in mind that unity uses left-handed coordinate system so Z axis goes into the screen (positive values goes into the screen) and the camera is on z = -10. All it means is that if our player is at Z = 0 we need to put the tilemaps on Z > 0. so the collider with “lowest z coordinate value” is closest to the camera:
The result in 3D view should look something like this:
Last thing that we need is an addition of a collider for each tilemap:
First (1) we need to add a Tilemap Collider 2D so that we can detect the tilemaps hence the terrain type. Next (2) for efficiency purpose we will use Composite Collider 2D that we want to set as Trigger. It will automatically add for us the Rigidbody2D. Set the latter (4) to be static. Make sure (3) that you check “Used By composite” in the Tilemap Collider 2D. Last thing that you want to set (5) is on the GameObject itself the checkbox Static.
Now while using Composite Collider 2D will make our game more efficient we need to set a specific setting in it (otherwise we will not be able to detect this collider using the Physics2D.OverlapPoint() or any other type of raycast):
Detectiong the terrain type
Ok now we need to detect the terrain type to play a specific sound. I will add an animation event to call a method responsible for this detection only when my run animation plays the step frames:
At the end we will have a new script called “Step Sound Feedback” which will have a LayerMask to know what to shoo at. Ignore Min/Max Z depth as this is from my trial and error development process. We will also have a reference transform which will be used as the detection point for the Physics2D.OverlapPoint(). For me its Agent as this game object actually moves around the map. We will also have an AudioSource which will play the sound.
Here is the script:
public class StepSoundFeedback : MonoBehaviour { [SerializeField] private LayerMask layerMask; [SerializeField] private Transform agent; [SerializeField] private AudioSource audioSource; public void PlayStepSound() { Collider2D collider = Physics2D.OverlapPoint(agent.position, layerMask); if (collider != null) { StepSoundData data = collider.GetComponent<StepSoundData>(); if ( data != null && (audioSource.isPlaying == false || audioSource.clip != data.StepClip || audioSource.time / audioSource.clip.length > 0.2f)) { audioSource.clip = data.StepClip; audioSource.pitch = Random.Range(0.95f, 1.05f); audioSource.Play(); } } } }
The PlayStepSound() method will be played when the step animation event gets triggered. We will use Physics2D.OverlapPoint() to detect the collider with a smallest Z axis value (as I have told you Z goes into the screen so the furthest collider will have the highest Z axis value).
If collider is not null we will get StepSoundData component (code will be shown later). The idea is that it contains the audio clip and to keep our setup easily extensible we add a new tilemap and add to it this component. Than all we need is to assign the audio clip and it will work with our current setup without any changes.
if (collider != null) { StepSoundData data = collider.GetComponent<StepSoundData>(); (...)
Ok. Lets assume that the data is not null. In the next if statement I will add some specific if statement checks. I am using Blend tree for my 2d animation so to prevent the steo sound being player 2 times when the animations gets blended I am checking if we are not trying to replay the same step sound and if the sound has been already played for 20% before we play the next one:
if ( data != null && (audioSource.isPlaying == false || audioSource.clip != data.StepClip || audioSource.time / audioSource.clip.length > 0.2f ) )
Otherwise when you change the movement direction just after starting to play the step sound you will have the step sound played a second time causing strange audio artefacts.
If everything went well we will set the clip, randomize the pitch (to add some variability to the sounds) and play the clip. Easy.
audioSource.clip = data.StepClip; audioSource.pitch = Random.Range(0.95f, 1.05f); audioSource.Play();
Ok here is the StepDoundData class:
public class StepSoundData : MonoBehaviour { [SerializeField] private List<AudioClip> stepClips; public AudioClip StepClip { get { if(stepClips.Count == 1) { return stepClips[0]; } else if(stepClips.Count == 0) { return null; } return stepClips[Random.Range(0, stepClips.Count)]; } } }
We have a StepClip public property that depending on the size of the List<AudioClip> will return null, first clip or a random one if we have more than 1 clip.
Here is how you would use it:
Again this way we can easily add a new tilemap and if we put on it the StepSoundData script and assign to it the AudioClips the previously written system will work with it. It’s much better than having “if-else if” block where you check for each type of terrain to select the specific AudioClip for it.
If all went well the next time you play your game it should be tears / gaps free :)
Now you should be able to hear different sound when stepping on different terrain type. My setup could be improved as
Want to learn more about making 2D games in Unity ?
To learn more on how to improve the way you write code by making 2D games from scratch check out my Make a 2D platformer using Design Patterns video courses :
You can also support me through Patreon:
If you agree or disagree let me know by joining the Sunny Valley Studio discord channel :)
Thanks for reading!
Peter