I did footstep audio refactoring for my rather simple footsteps audio system in order to turn it into a fully fledged surface type / character type retrieval setup that I can expand in the editor. The old system had hard coded arrays, but this new one is user editable directly in editor!

The new footsteps system with support for multiple characters in action.

The audio playback / sound banks code

The footsteps audio refactoring also greatly simplified the footstep audio playback system as the old relied on multiple if clauses but this new one retrieves the sounds based on 2 strings (surface type and character type) sent to the system in a single function.

The old if-ridden footsteps audio playback 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
                {
                    Debug.Log("Floor not found for audio");
                }
The new, footstep Audio playback code
                //Check the floor tag or physical material
                RaycastHit hit;
                if (Physics.Raycast(transform.position, Vector3.down, out hit))
                {
                    //Get random audio clip from the footstep system based on surface and character type.
                    _footstepSystem.CallFootstep(ReturnValue, hit.collider.tag, CharacterTypeEnum.ToString());
                }
                else
                {
                    Debug.Log("Floor not found for audio");
                }

For this system I used chatGPT more than ever. It required multiple lines of code I had never even attempted before. Retrieving data from other game objects by string, editor scripting and nested arrays.

A list of some of the things I asked from chatGPT for this feature:

  • Arrays with sub-arrays – success
  • Retrieving data from these sub-arrays based on string values as search terms – perfect success
  • Call a function in another game object and get a value in return – success
  • Enum list as a dropdown in inspector – fail

Most of the answers chatGPT gave to me I had to edit heavily or pick apart and built my own. For some of my questions, like the enum drop down list, chatGPT simply was not able to give a working solution, for whatever reason.

But the data retrieval code worked perfectly out of the box. I pasted all my code to chatGPT and asked how to retrieve the data from that array structure and the solution it came up with simply just worked!

I am amazed that the outcome of this footstep audio refactoring is exactly as I imagined it!

The old footsteps sound bank system
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;
}
The new footsteps sound bank system
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
#endif
using UnityEngine;

public class FootstepSystem : MonoBehaviour
{
    public MainArrayElement[] mainArray;
    public AudioClip[] _fallbackFootsteps;

    int lastRandomNum = 0;
    int randomNum = 0;

    int RandomWithExclusion(int min, int max, int exclusion)
    {
        var result = UnityEngine.Random.Range(min, max - 1);
        return (result < exclusion) ? result : result + 1;
    }

    public void CallFootstep(System.Action<AudioClip> callback, string _surfaceTag, string _characterType)
    {
        callback(GetFootstepSound(_characterType, _surfaceTag));
    }

    // Method to get the desired audio clip
    public AudioClip GetFootstepSound(string characterType, string surfaceType)
    {
        foreach (MainArrayElement mainElement in mainArray)
        {
            if (mainElement.surfaceType == surfaceType)
            {
                foreach (SubArrayElement subElement in mainElement.Characters)
                {
                    if (subElement.CharacterTypes.Contains(characterType))
                    {
                        // If any of the character types match, return a random audio clip
                        if (subElement.AudioClips.Length > 0)
                        {
                            randomNum = RandomWithExclusion(0, subElement.AudioClips.Length, lastRandomNum);
                            Debug.Log("Footstep file found for " + characterType + " on a " + surfaceType + " surface. Using clip " + randomNum);
                            lastRandomNum = randomNum;
                            return subElement.AudioClips[randomNum];
                        }
                    }
                }
            }
        }

        // If no matching audio clip is found, return a random clip from fallbackFootsteps
        if (_fallbackFootsteps.Length > 0)
        {
            randomNum = RandomWithExclusion(0, _fallbackFootsteps.Length, lastRandomNum);
            Debug.Log("Footstep file NOT found for " + characterType + " on a " + surfaceType + " surface. Using fallback footsteps clip " + randomNum);
            lastRandomNum = randomNum;
            return _fallbackFootsteps[randomNum];

        }

        // If no fallbackFootsteps are available, return null
        return null;
    }
}


[System.Serializable]
public class SubArrayElement
{
    public String[] CharacterTypes;
    public AudioClip[] AudioClips;
}

[System.Serializable]
public class MainArrayElement
{
    public String surfaceType;
    public SubArrayElement[] Characters;
}
#if UNITY_EDITOR
[CustomEditor(typeof(FootstepSystem))]
public class YourScriptEditor : Editor
{
    SerializedProperty mainArrayProp;
    SerializedProperty fallbackFootstepsProp;

    private void OnEnable()
    {
        mainArrayProp = serializedObject.FindProperty("mainArray");
        fallbackFootstepsProp = serializedObject.FindProperty("_fallbackFootsteps");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        EditorGUI.BeginChangeCheck();

        EditorGUILayout.PropertyField(mainArrayProp, true);
        EditorGUILayout.PropertyField(fallbackFootstepsProp, true);

        if (EditorGUI.EndChangeCheck())
        {
            serializedObject.ApplyModifiedProperties();
            return;
        }

        serializedObject.ApplyModifiedProperties();
    }
}
#endif

Now characters that share animation files can have unique locomotion sounds that respect physical surfaces and the sound banks are a breeze to set up and multiple characters can even share a single sound bank if required!

I also added extensive debug logging in order to see what is happening under the hood when the characters are walking around. You can never have too much debug data available!

Robot sounds

Now that I had support for multiple character types, it was time to make sounds for them!

I loaded a few sounds that I thought could be great building blocs for the robot sounds from Pixbay.

Footsteps audio refactoring audition project file

I ended up making the sounds a combination of 3 different sounds in Audition: A pre-made robot walking sound, the normal footstep the character uses, but made a little beefier and a weird rusty mech stomp sound.

These new sounds are then exported as separate clips from audition and set up in the footsteps audio manager. These files were created for RobotHeavy on Asphalt.

The end result is ok, but I am planning to add support for cloth rustling / robotic hydraulic and servo sounds separate from the footsteps. As these sounds are really always the same regardless of the surface underneath. But this is not the topic for today’s blog post!

One response to “Footsteps audio refactoring”

  1. […] EDIT: Recently I upgraded this array system to be a much more robust one. Read about it here! […]

Leave a Reply

Your email address will not be published. Required fields are marked *

Recent Posts


Archive


Social Links