# 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