# 4. Footsteps and Surface Audio
Part of [[Audio Mixers and Character Audio]]. Previous: [[3. Character Audio Effects Chain]]. Next: [[5. Audio Mixer Pitfalls]].
**Goal:** footsteps that change with the surface (wood, stone, grass, metal, water), never machine-gun the same clip, scale with speed, and add jump/land sounds - all data-driven so a sound designer can extend it without code.
> [!note] Verification status
> Audio Random Container behaviour from the 6000.5 Manual: create via **Assets > Create > Audio > Audio Random Container**; assign it to an Audio Source's **AudioResource** field; with **Trigger = Manual**, `AudioSource.Play` plays one clip chosen by the **Playback Mode** (Sequential / Shuffle / Random), with **Avoid Repeating Last** and volume/pitch randomization. API checked: `AudioSource.resource` (`AudioResource`, namespace `UnityEngine.Audio`), `PlayOneShot(AudioClip, float)`, `TerrainData.terrainLayers / alphamapWidth / alphamapHeight / GetAlphamaps`, `Collider.sharedMaterial` (`PhysicsMaterial`). **Code not compiled in Unity.**
## 4.1 Two ways to randomize
| | Audio Random Container (asset) | Code (clip arrays + PlayOneShot) |
|---|---|---|
| Setup | Designer-friendly window, preview with Play | Arrays in a ScriptableObject |
| No-repeat | **Avoid Repeating Last**, Shuffle | write it |
| Volume/pitch variation | built in (Randomization) | write it |
| Overlapping steps | one `Play()` per source - a new step restarts that source | `PlayOneShot` overlaps freely |
| Best for | footsteps on a dedicated feet source, one-shot props | landings that overlap steps, very fast cadences |
This part supports **both**: each surface entry can hold an `AudioResource` (any Audio Random Container or plain clip) *and/or* a clip array.
## 4.2 Data
```csharp
using UnityEngine;
/// One per physical surface type (Type Object pattern). Create: Assets > Create > Audio > Surface Definition.
[CreateAssetMenu(menuName = "Audio/Surface Definition", fileName = "Surface_")]
public class SurfaceDefinition : ScriptableObject
{
[TextArea] public string notes;
}
```
```csharp
using System;
using UnityEngine;
using UnityEngine.Audio;
[CreateAssetMenu(menuName = "Audio/Footstep Bank", fileName = "FootstepBank")]
public class FootstepBank : ScriptableObject
{
[Serializable]
public class Entry
{
public SurfaceDefinition surface;
[Header("Option A - Audio Random Container (or any AudioResource)")]
public AudioResource stepResource;
[Header("Option B - clip arrays (overlap-friendly)")]
public AudioClip[] steps;
public AudioClip[] lands;
public AudioClip[] jumps;
[Header("Variation (option B)")]
public Vector2 volumeRange = new Vector2(0.8f, 1f);
public Vector2 pitchRange = new Vector2(0.95f, 1.05f);
}
public SurfaceDefinition defaultSurface;
public Entry[] entries;
[Header("Mappings for untagged geometry")]
public PhysicsMaterial[] physicsMaterials; // index-aligned with materialSurfaces
public SurfaceDefinition[] materialSurfaces;
public TerrainLayer[] terrainLayers; // index-aligned with terrainSurfaces
public SurfaceDefinition[] terrainSurfaces;
public Entry Find(SurfaceDefinition s)
{
if (s == null) s = defaultSurface;
foreach (var e in entries) if (e.surface == s) return e;
foreach (var e in entries) if (e.surface == defaultSurface) return e;
return entries.Length > 0 ? entries[0] : null;
}
public SurfaceDefinition FromPhysicsMaterial(PhysicsMaterial m)
{
if (m == null) return null;
for (int i = 0; i < physicsMaterials.Length && i < materialSurfaces.Length; i++)
if (physicsMaterials[i] == m) return materialSurfaces[i];
return null;
}
public SurfaceDefinition FromTerrainLayer(TerrainLayer l)
{
if (l == null) return null;
for (int i = 0; i < terrainLayers.Length && i < terrainSurfaces.Length; i++)
if (terrainLayers[i] == l) return terrainSurfaces[i];
return null;
}
}
```
```csharp
using UnityEngine;
/// Optional explicit tag on any collider (highest priority in detection).
public class SurfaceTag : MonoBehaviour
{
public SurfaceDefinition surface;
}
```
> [!warning] Code not compiled in Unity
> Not compiled. `SurfaceDefinition` assets are also useful for bullet impacts and particle choice ([[3.Particles Spawning Particles on Contact]]) - one surface vocabulary for audio and VFX.
## 4.3 Surface detection
Priority: `SurfaceTag` on the hit collider (or its parents) -> collider's `PhysicsMaterial` mapping -> Terrain's dominant layer -> default.
```csharp
using UnityEngine;
public static class SurfaceDetector
{
static readonly RaycastHit[] Hits = new RaycastHit[4];
public static SurfaceDefinition Detect(Vector3 footPos, FootstepBank bank, LayerMask groundMask, float distance = 0.6f)
{
Vector3 origin = footPos + Vector3.up * 0.25f;
int n = Physics.RaycastNonAlloc(origin, Vector3.down, Hits, distance + 0.25f, groundMask, QueryTriggerInteraction.Ignore);
if (n == 0) return bank.defaultSurface;
// closest hit
int best = 0;
for (int i = 1; i < n; i++) if (Hits[i].distance < Hits[best].distance) best = i;
RaycastHit hit = Hits[best];
var tag = hit.collider.GetComponentInParent<SurfaceTag>();
if (tag != null && tag.surface != null) return tag.surface;
var fromMat = bank.FromPhysicsMaterial(hit.collider.sharedMaterial);
if (fromMat != null) return fromMat;
if (hit.collider is TerrainCollider && hit.collider.TryGetComponent(out Terrain terrain))
{
var fromTerrain = bank.FromTerrainLayer(DominantLayer(terrain, hit.point));
if (fromTerrain != null) return fromTerrain;
}
return bank.defaultSurface;
}
static TerrainLayer DominantLayer(Terrain terrain, Vector3 worldPos)
{
TerrainData data = terrain.terrainData;
Vector3 local = worldPos - terrain.GetPosition();
int x = Mathf.Clamp(Mathf.FloorToInt(local.x / data.size.x * data.alphamapWidth), 0, data.alphamapWidth - 1);
int z = Mathf.Clamp(Mathf.FloorToInt(local.z / data.size.z * data.alphamapHeight), 0, data.alphamapHeight - 1);
float[,,] a = data.GetAlphamaps(x, z, 1, 1); // allocates a small array; fine at footstep rate
int bestLayer = 0; float bestW = -1f;
for (int l = 0; l < a.GetLength(2); l++)
if (a[0, 0, l] > bestW) { bestW = a[0, 0, l]; bestLayer = l; }
var layers = data.terrainLayers;
return bestLayer < layers.Length ? layers[bestLayer] : null;
}
}
```
> [!warning] Code not compiled in Unity
> Not compiled. `Physics.RaycastNonAlloc(Vector3, Vector3, RaycastHit[], float, int, QueryTriggerInteraction)` is listed in the 6000.5 reference (same NonAlloc style as [[3. Layers Matrix and Queries]]).
## 4.4 Footstep player
Consumes the `Stepped / Landed / Jumped` events from `CCPlayerController` ([[3. CharacterController Implementation]]) **or** animation events named `Footstep`.
```csharp
using UnityEngine;
public class FootstepPlayer : MonoBehaviour
{
[SerializeField] FootstepBank bank;
[SerializeField] CCPlayerController controller; // optional: distance-based steps
[SerializeField] AudioSource feetSource; // routed to SFX/Player/Footsteps
[SerializeField] AudioSource oneShotSource; // same group, used for overlapping lands/jumps
[SerializeField] Transform leftFoot, rightFoot; // optional; falls back to transform
[SerializeField] LayerMask groundMask = ~0;
[SerializeField] float walkSpeed = 2f, sprintSpeed = 8f;
[SerializeField] float landMinSpeed = 3f, landMaxSpeed = 15f;
bool _leftNext;
AudioClip _lastClip;
void OnEnable()
{
if (controller == null) return;
controller.Stepped += OnStepped;
controller.Landed += OnLanded;
controller.Jumped += OnJumped;
}
void OnDisable()
{
if (controller == null) return;
controller.Stepped -= OnStepped;
controller.Landed -= OnLanded;
controller.Jumped -= OnJumped;
}
// ---- event sources ----
void OnStepped(float speed)
{
float loud = Mathf.Lerp(0.55f, 1f, Mathf.InverseLerp(walkSpeed, sprintSpeed, speed));
PlayStep(loud);
}
void OnLanded(float impactSpeed)
{
if (impactSpeed < landMinSpeed) { PlayStep(0.7f); return; }
float loud = Mathf.Lerp(0.6f, 1f, Mathf.InverseLerp(landMinSpeed, landMaxSpeed, impactSpeed));
var e = bank.Find(SurfaceDetector.Detect(transform.position, bank, groundMask));
PlayFromArray(e, e?.lands, loud, oneShotSource);
}
void OnJumped()
{
var e = bank.Find(SurfaceDetector.Detect(transform.position, bank, groundMask));
PlayFromArray(e, e?.jumps, 0.8f, oneShotSource);
}
/// Animation Event hook: add an event named "Footstep" on each foot-plant frame.
/// Use the event's float parameter as loudness (0 = default).
public void Footstep(AnimationEvent evt)
{
// Ignore events from clips that are blending out (common double-step bug).
if (evt.animatorClipInfo.weight < 0.5f) return;
PlayStep(evt.floatParameter > 0f ? evt.floatParameter : 1f);
}
// ---- playback ----
void PlayStep(float loudness)
{
Transform foot = _leftNext ? leftFoot : rightFoot;
_leftNext = !_leftNext;
Vector3 pos = foot != null ? foot.position : transform.position;
var e = bank.Find(SurfaceDetector.Detect(pos, bank, groundMask));
if (e == null) return;
if (e.stepResource != null)
{
// Option A: Audio Random Container (Trigger = Manual) - Play() picks the next clip.
feetSource.resource = e.stepResource;
feetSource.pitch = 1f; // variation lives in the container
feetSource.volume = loudness;
feetSource.Play();
}
else
{
PlayFromArray(e, e.steps, loudness, feetSource);
}
}
void PlayFromArray(FootstepBank.Entry e, AudioClip[] clips, float loudness, AudioSource src)
{
if (e == null || clips == null || clips.Length == 0 || src == null) return;
AudioClip clip = clips[Random.Range(0, clips.Length)];
if (clips.Length > 1 && clip == _lastClip) // avoid immediate repeat
clip = clips[(System.Array.IndexOf(clips, clip) + 1) % clips.Length];
_lastClip = clip;
src.pitch = Random.Range(e.pitchRange.x, e.pitchRange.y);
src.PlayOneShot(clip, loudness * Random.Range(e.volumeRange.x, e.volumeRange.y));
}
}
```
> [!warning] Code not compiled in Unity
> Not compiled or play-tested. Notes: (1) option B changes `src.pitch`, so option A resets the feet source to pitch 1 and leaves variation to the container; (2) `AnimationEvent.animatorClipInfo.weight` (listed on the 6000.5 `AnimationEvent` reference) is a common blending-out guard - see [[6.Animation Events Reliable Patterns]] for the full treatment; (3) don't drive steps from *both* distance events and animation events - pick one per character.
## 4.5 Setup steps
1. Create `Surface_Wood`, `Surface_Stone`, `Surface_Grass`, `Surface_Metal`, `Surface_Water` (Assets > Create > Audio > Surface Definition).
2. For each surface, either create an **Audio Random Container** (Assets > Create > Audio > Audio Random Container): add 4-8 step clips, **Trigger: Manual**, **Playback Mode: Random** or **Shuffle**, **Avoid Repeating Last** = 2, small volume and pitch randomization - or fill the clip arrays.
3. Create a `FootstepBank`, add entries, set `defaultSurface`, fill PhysicsMaterial and TerrainLayer mappings.
4. Tag special meshes with `SurfaceTag` (a wooden bridge inside a stone level).
5. On the player: `Audio_Feet` source (Output **SFX/Player/Footsteps**, Play On Awake off) and a second one-shot source for lands/jumps on the same group. Add `FootstepPlayer`, assign controller, sources, bank.
6. Walk the [[5. Gray-Box Metrics Gym]] with different surfaces on each lane - the landing sounds on the ledge lane tell you instantly whether impact scaling works.
## 4.6 Design notes
- **Footsteps are information:** surface = where am I; cadence = how fast; loudness = how noisy. Feed the same loudness value to enemy hearing in [[Organic Enemy Sensing]] - sprinting on metal should be audible to guards *and* to the player, so the stealth rule is learnable.
- **Level 16 rule of thumb:** footsteps must never be louder than threats. Mix them -6 to -12 dB below combat SFX and let the `LowHealth` snapshot pull them down further.
## Docs
- Create a randomized playlist (Audio Random Container): https://docs.unity.com/en-us/engine/6000.5/manual/audio/random-container/create-randomized-playlist
- Audio Random Container reference: https://docs.unity.com/en-us/engine/6000.5/manual/audio/random-container/ui
- Audio Random Container fundamentals: https://docs.unity.com/en-us/engine/6000.5/manual/audio/random-container/fundamentals
- `AudioSource.resource`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/AudioSource-resource.html
- `AudioResource`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Audio.AudioResource.html
- `AudioSource.PlayOneShot`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/AudioSource.PlayOneShot.html
- `TerrainData.GetAlphamaps`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/TerrainData.GetAlphamaps.html
- `AnimationEvent`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/AnimationEvent.html