I have actually never before done footstep audio set for a game. I wanted the system to be able to play audio from animation events and work on multiple different surfaces. Read how I ended up doing it.
Triggering footstep audio from the animations
First things first, I needed to get audio events firing when the character feet hit the ground. I figured I would use animation events to play the footsteps. I named these “Footstep”.

This was pretty simple. I just selected the FBX with all the animations and created animation events on just the instance the feet is just about to land.
This was not perfect though. As I am using a blend tree with multiple animations playing at once, I get multiple events of footsteps, causing a cacophony of sound. I googled a bit and people seemed to agree that the solution for this was to not use animation events at all! But I really did not want to create a system that looks at the height of the foot from the ground and samples character velocities, but instead wanted the simplicity of animation events. The solution was to read the blend weight of each animation clip on the event and only play the audio for animation clips that were blended on more than 45%
Code for playing footstep audio only once
[SerializeField] private float _minBlend = 0.45f;
public void Footstep(AnimationEvent evt)
{
if (evt.animatorClipInfo.weight > _minBlend)
{
//play audio
}
}
This was great. I could now only hear one footstep at a time! it was time to figure out which audio file to play!
Footstep audio arrays
I did not plan to use any middleware for the audio, like wwise or fmod. But instead I would build my own tools to determine which audio files to play and how. For footsteps, I wanted a set of global arrays that contain multiple variations of footsteps sounds on multiple surfaces. The player character and NPC’s would then pick up correct files from this list.
The way I went about it was to create a custom component called “FootstepSystem”. it is very simple, just a C# script with auto clip arrays for each surface.
FootstepSystem.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine; public class FootstepSystem : MonoBehaviour
{
public AudioClip[] _footstepsOnWoodFloor;
public AudioClip[] _footstepsOnTileFloor;
public AudioClip[] _footstepsOnPuddle;
public AudioClip[] _footstepsOnAsphalt;
}

When a scene would have a new surface, I would just add a new variable array to this script. This script is then part of a prefab that is placed in every level in the game, allowing anything to access it at any time.
EDIT: Recently I upgraded this array system to be a much more robust one. Read about it here!
Creating the footstep audio clips
The sound files I sourced from online, looking for free SFX. Eventually I fund a website called Pixbay. It has a ton of free to use sound effects that can easily be used as placeholders or even final sounds. Most of the footsteps were long files with tens of steps in them. To cut them to individual clips I used Adobe Audition. I have no idea what I am doing as I use it, but it seems to do the trick anyway. I also managed to clean up some of the noise in the files with Audition.

My next problem was the random was truly random and kept playing the same audio twice, or more in a row from the away. I figures I need to write a solution that makes sure the same audio is not played multiple times.
Random with exclusion
int RandomWithExclusion(int min, int max, int exclusion)
{
var result = UnityEngine.Random.Range(min, max - 1);
return (result < exclusion) ? result : result + 1;
}
Get random number
randomNum = RandomWithExclusion(0, _footstepSystem._footstepsOnWoodFloor.Length, lastRandomNum);
This simple random with exclusion prevents the same audio from ever playing multiple times in a row. It works great and makes the audio sound a lot better. To further add variance to the sounds I added a pitch random for each footstep.
Pitch random
_audioSource.pitch = Random.Range(0.9f, 1.1f);
All of these features (prevent multiple audio files playing at once, randomise footstep audio variant, random pitch) makes the footsteps sounds pretty good in my opinion. I am fairly happy with the end results. This system by far is the most complex audio setup that the game will need and I do want to make it as interesting as possible.
Detecting different floor surfaces
The first thing I figured I will need is to know what surface my character is walking on.

Each floor type has a custom tag applied to it, Based on this tag, a different sound is played.
The tag is checked by simply raycasting down from the character and checking the hit colliders tag. A different audio is then selected to be played for each detected tag. My crude solution calls for me to edit the C# script every time a new surface is added. It is not ideal, but works great for me as I am the only person working on this stuff.
Floor detection code
//Check the floor tag or physical material
RaycastHit hit;
if (Physics.Raycast(transform.position, Vector3.down, out hit))
{
if (hit.collider.tag == "WoodenFloor")
{
//select random clip from the audio array:
randomNum = RandomWithExclusion(0, _footstepSystem._footstepsOnWoodFloor.Length, lastRandomNum);
_footstepAudio = _footstepSystem._footstepsOnWoodFloor[randomNum];
Debug.Log("Step on wooden Floor, footstep ID " + randomNum);
}
else if (hit.collider.tag == "Puddle")
{
//select random clip from the audio array:
randomNum = RandomWithExclusion(0, _footstepSystem._footstepsOnPuddle.Length, lastRandomNum);
_footstepAudio = _footstepSystem._footstepsOnPuddle[randomNum];
Debug.Log("Step on puddle, footstep ID " + randomNum);
}
else if (hit.collider.tag == "Asphalt")
{
//select random clip from the audio array:
randomNum = RandomWithExclusion(0, _footstepSystem._footstepsOnAsphalt.Length, lastRandomNum);
_footstepAudio = _footstepSystem._footstepsOnAsphalt[randomNum];
Debug.Log("Step on asphalt, footstep ID " + randomNum);
}
else
{
randomNum = RandomWithExclusion(0, _footstepSystem._footstepsOnTileFloor.Length, lastRandomNum);
_footstepAudio = _footstepSystem._footstepsOnTileFloor[randomNum];
Debug.Log("Step on tile, footstep ID " + randomNum);
}
_audioSource.PlayOneShot(_footstepAudio, Options.GetSFXVolume());
lastRandomNum = randomNum;
}
else
{
Spatial audio
In order for the sound effects to follow the characters around, I changed the default Adventure Creator character audio source spatial blend setting from 0 to 1. When the audio source follows the player on the screen the footsteps sound awesome!

I also increased the Max distance to make the footsteps and other character audio play louder as in some large scenes the footsteps vanished completely.
Reverb zones
I now had nice footsteps in all scenes, but all of the locations sounded exactly the same. Thankfully, Unity has a built in tool called “Audio Reverb Zone”. It is amazingly simple to use. Just plop that thing in a level, increase the MinDistance and select a preset and you are good to go!

Cadence.cs (full code)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.AI;
using AC;
public class Cadence : MonoBehaviour
{
private Animator CharController;
[SerializeField] private string _turnSpeedProperty = "Turn";
[SerializeField] private float _turnRecoveryRate = 10f;
[SerializeField] private float _currentTurnSpeed = 0.0f;
private float _targetTurnSpeed = 0.0f;
private float _targetTurnSpeedVelocity = 0.0f;
private AudioSource _audioSource;
[SerializeField] private AudioClip _debugAudio;
private AudioClip _footstepAudio;
[SerializeField] private bool DebugMode = false;
[SerializeField] private float _minBlend = 0.5f;
[SerializeField] private bool UseDefaultFootsteps = true;
private FootstepSystem _footstepSystem;
bool firstRun = true;
int lastRandomNum = 0;
int randomNum = 0;
// Start is called before the first frame update
void Start()
{
CharController = this.GetComponent<Animator>();
_audioSource = this.GetComponent<AudioSource>();
if (UseDefaultFootsteps == true)
{
_footstepSystem = GameObject.Find("CustomScripting").GetComponent<FootstepSystem>();
}
}
public void L(AnimationEvent evt)
{
//Debug.Log("Left foot down");
CharController.SetBool("Cadence", false);
}
public void R(AnimationEvent evt)
{
//Debug.Log("right foot down");
CharController.SetBool("Cadence", true);
}
void Update()
{
_targetTurnSpeed = CharController.GetFloat("Turning");
_currentTurnSpeed = Mathf.MoveTowards(_currentTurnSpeed, _targetTurnSpeed, _turnRecoveryRate);
CharController.SetFloat(_turnSpeedProperty, _currentTurnSpeed);
}
int RandomWithExclusion(int min, int max, int exclusion)
{
var result = UnityEngine.Random.Range(min, max - 1);
return (result < exclusion) ? result : result + 1;
}
public void Footstep(AnimationEvent evt)
{
if (evt.animatorClipInfo.weight > _minBlend)
{
if (DebugMode == true)
{
_audioSource.PlayOneShot(_debugAudio, Options.GetSFXVolume());
Debug.Log("Playing footstep audio, clip weight: " + evt.animatorClipInfo.weight + ", clip name: " + evt.animatorClipInfo.clip);
}
else if (UseDefaultFootsteps == true)
{
_audioSource.pitch = Random.Range(0.9f, 1.1f);
//Check the floor tag or physical material
RaycastHit hit;
if (Physics.Raycast(transform.position, Vector3.down, out hit))
{
if (hit.collider.tag == "WoodenFloor")
{
//select random clip from the audio array:
randomNum = RandomWithExclusion(0, _footstepSystem._footstepsOnWoodFloor.Length, lastRandomNum);
_footstepAudio = _footstepSystem._footstepsOnWoodFloor[randomNum];
Debug.Log("Step on wooden Floor, footstep ID " + randomNum);
}
else if (hit.collider.tag == "Puddle")
{
//select random clip from the audio array:
randomNum = RandomWithExclusion(0, _footstepSystem._footstepsOnPuddle.Length, lastRandomNum);
_footstepAudio = _footstepSystem._footstepsOnPuddle[randomNum];
Debug.Log("Step on puddle, footstep ID " + randomNum);
}
else if (hit.collider.tag == "Asphalt")
{
//select random clip from the audio array:
randomNum = RandomWithExclusion(0, _footstepSystem._footstepsOnAsphalt.Length, lastRandomNum);
_footstepAudio = _footstepSystem._footstepsOnAsphalt[randomNum];
Debug.Log("Step on asphalt, footstep ID " + randomNum);
}
else
{
randomNum = RandomWithExclusion(0, _footstepSystem._footstepsOnTileFloor.Length, lastRandomNum);
_footstepAudio = _footstepSystem._footstepsOnTileFloor[randomNum];
Debug.Log("Step on tile, footstep ID " + randomNum);
}
_audioSource.PlayOneShot(_footstepAudio, Options.GetSFXVolume());
lastRandomNum = randomNum;
}
else
{
Debug.Log("Floor not found for audio");
}
}
}
else
{
if (DebugMode == true)
{
Debug.Log("Footstep event fired, no actions performed, clip weight: " + evt.animatorClipInfo.weight + ", clip name: " + evt.animatorClipInfo.clip);
}
}
}
}
I went into this task not knowing anything about creating footsteps, and came out the other end with a pretty solid experience of doing just that. I am not sure if my method of creating this effect is a sensible one, but it works for me and is very simple to set up and maintain.
If you are working on a similar task, I hope this blogpost can offer some help in figuring out all the problems you too might face.
Leave a Reply