A player walks across a stone floor for six minutes. In that time they take somewhere between 500 and 700 steps. If the game has a single stone footstep sample, the player hears the identical waveform 600 times, and somewhere around the fourth minute the sound stops registering as a footstep and starts registering as a repeating loop. The standard fix is to record variations, but a project with four surfaces and five variations per surface is already twenty files, and the differences between those variations are often small enough that the ear still catches the repetition.
A code-generated footstep has a different property: it is a new waveform every time it fires. Not a random playback of an existing sample, but a fresh synthesis from noise, filters, and envelopes. The variation is built into the generation rather than layered on top of it.
The Unity footstep tutorial covers how to trigger footsteps on animation events, and the surface-specific footstep guide covers the design logic for making grass, wood, and metal recognizable. This article is about the synthesis itself — the code that produces the sound, with the parameter ranges that make the surface character work.
What physically happens when a foot hits the ground?
The sound of a footstep is not one event. It is three events happening within the same 300 milliseconds, and each one has a distinct physical source.
The first is the contact transient — the brief burst of sound produced by the moment of collision between the shoe and the surface. Its duration is short enough that recording it accurately requires a high sample rate; measurements of impact sounds typically place the useful content between 5 and 15 milliseconds, with energy spread across the entire audible spectrum. This is why the transient is best modeled as a burst of white noise rather than a tonal event.
The second is the surface resonance — the portion of the contact energy that the material absorbs and re-emits at its own resonant frequencies. A wooden floor produces a low-mid resonance that decays over roughly 80 to 150 milliseconds. A metal plate produces a higher resonance with a longer decay, typically 200 to 400 milliseconds. Grass absorbs almost all the contact energy and re-emits very little, which is why a grass footstep has almost no sustained tonal content.
The third is the characteristic body — the low-frequency energy from the transfer of the player's weight into the ground. It sits below roughly 150 Hz and decays over 100 to 200 milliseconds. Without it, the footstep sounds thin and paper-like, regardless of how good the transient and resonance are.
All three components come from the same physical event. They are not layered in the game engine; they are layered in the synthesis, and the sum is what the player hears as a single footstep.
How do you synthesize each of the three components?
The synthesis follows directly from the physical model. Three parallel chains, mixed at the output.
Chain 1 — the contact transient. White noise multiplied by an exponential envelope with an effectively instant attack (0 to 2 milliseconds) and a decay of 15 to 25 milliseconds. No filtering at this stage; the broadband content is what makes the transient read as a physical collision.
Chain 2 — the surface resonance. White noise passed through a resonant filter, with the center frequency and Q set by the surface. The envelope is slower than the transient: attack around 5 milliseconds, decay 60 to 400 milliseconds depending on the material. The filter is what turns the noise into a pitched resonance that suggests a specific material.
Chain 3 — the characteristic body. A low-frequency sine oscillator below 150 Hz, with an attack around 10 milliseconds and a decay of 100 to 200 milliseconds. The amplitude is typically the lowest of the three, because the body supports the transient rather than competing with it.
The three chains share a single start time. The transient dominates the first 20 milliseconds, the resonance builds over the next 30, and the body sustains through the middle. By 300 milliseconds, all three have decayed. The Godot procedural audio tutorial covers the same synthesis pattern in GDScript if that is the engine of choice; the physics model is identical.
Unity does not provide a built-in synthesis API, but AudioClip.SetData accepts a float array of samples between -1.0 and 1.0, which is enough to produce any waveform. The class below generates a footstep as a 0.25-second clip, with the surface type and weight as parameters.
using UnityEngine;
public class ProceduralFootstep : MonoBehaviour
{
public AudioSource source;
private const int SampleRate = 44100;
public enum Surface { Grass, Wood, Metal }
public void PlayFootstep(Surface surface, float weight = 1.0f)
{
AudioClip clip = GenerateFootstep(surface, weight);
source.PlayOneShot(clip);
}
private AudioClip GenerateFootstep(Surface surface, float weight)
{
float duration = 0.25f;
int sampleCount = (int)(SampleRate * duration);
float[] samples = new float[sampleCount];
float resonanceFreq, resonanceQ, resonanceDecay;
switch (surface)
{
case Surface.Grass:
resonanceFreq = 800f;
resonanceQ = 0.7f;
resonanceDecay = 60f;
break;
case Surface.Wood:
resonanceFreq = 180f;
resonanceQ = 4f;
resonanceDecay = 90f;
break;
default:
resonanceFreq = 350f;
resonanceQ = 8f;
resonanceDecay = 200f;
break;
}
float pitchShift = Random.Range(0.94f, 1.06f);
float weightShift = Random.Range(0.9f, 1.1f) * weight;
float[] noise = new float[sampleCount];
for (int i = 0; i < sampleCount; i++)
{
noise[i] = Random.Range(-1f, 1f);
}
float f = 2f * Mathf.Sin(Mathf.PI * resonanceFreq * pitchShift / SampleRate);
float q = 1f / resonanceQ;
float low = 0f, band = 0f, high = 0f;
for (int i = 0; i < sampleCount; i++)
{
float t = (float)i / SampleRate;
float transientEnv = Mathf.Exp(-t * 400f);
float transient = noise[i] * transientEnv * 0.8f;
high = noise[i] - low - q * band;
band += f * high;
low += f * band;
float resonanceEnv = Mathf.Exp(-t * resonanceDecay);
float resonance = band * resonanceEnv * 0.5f;
float bodyFreq = 90f * pitchShift;
float bodyEnv = Mathf.Exp(-t * 20f);
float body = Mathf.Sin(2f * Mathf.PI * bodyFreq * t) * bodyEnv * 0.3f;
samples[i] = (transient + resonance + body) * weightShift;
}
AudioClip clip = AudioClip.Create(
"procedural_footstep_" + surface,
sampleCount,
1,
SampleRate,
false
);
clip.SetData(samples, 0);
return clip;
}
}
The state-variable filter in the second chain is a simple IIR filter that produces a resonant peak at the specified frequency. The Q parameter controls how narrow the peak is: a high Q produces a distinct pitch that reads as a resonant material, and a low Q produces a broad, unfiltered noise that reads as absorption. The three surface presets in the switch statement are the values that make the difference audible.
What separates grass from wood from metal in numbers?
The presets in the code are starting points. The table below summarizes the parameter relationships that matter, and the pattern is what makes the surfaces distinguishable.
| Component | Grass | Wood | Metal |
|---|---|---|---|
| Transient decay | 20 ms | 25 ms | 15 ms |
| Resonance frequency | 800 Hz | 180 Hz | 350 Hz |
| Resonance Q | 0.7 | 4 | 8 |
| Resonance decay | 60 ms | 90 ms | 200 ms |
| Body frequency | 110 Hz | 90 Hz | 80 Hz |
| Body amplitude | 0.15 | 0.3 | 0.25 |
Grass has the highest resonance frequency and the lowest Q, because a broad filter at a high frequency removes almost all audible resonance — the surface is modeled as absorbing rather than re-emitting. Metal has the highest Q, which produces a narrow resonance that reads as a distinct pitch, plus a decay that is more than three times longer than wood's. Wood sits between the two, with a moderate Q and a lower frequency that produces a short low-mid thump.
These ranges come from the physical behavior of the materials rather than from an arbitrary tuning. A metal plate does have a longer ring than a wooden plank, and a grass surface does absorb more contact energy than either. The synthesis is trying to reproduce those material properties, and the numbers are the translation of "metal rings" and "grass absorbs" into filter parameters.
How do you make each footstep slightly different from the last?
The main advantage of a procedural footstep is that variation is not an additional feature — it is a property of the synthesis. Every time the code runs, every parameter that uses a random value produces a slightly different waveform.
In the code above, two parameters are randomized per trigger. The pitch shift is applied to both the resonance frequency and the body frequency, so the whole footstep moves in pitch coherently rather than drifting out of tune. The weight shift is applied to the final amplitude, so the footstep is slightly louder or quieter depending on the value. A third layer of variation can be added by randomizing the transient decay within a range — between 350 and 450 in the exponential term — which produces small differences in the sharpness of the attack that the ear reads as a difference in the shoe or the surface.
The range of variation matters more than the presence of variation. A pitch shift of ±6 percent is enough to break the repetition without making the footstep sound like a different event. A shift of ±20 percent starts to sound like a different surface. The weight shift behaves the same way: ±10 percent reads as natural, ±30 percent reads as inconsistent.
The performance cost of generating a 250-millisecond clip at 44100 Hz is 11,025 iterations of the synthesis loop, each involving a noise sample, a filter update, and a sine calculation. On desktop hardware this takes well under a millisecond. On mobile, it is measurable — a crowd scene with dozens of simultaneous footstep triggers can push the synthesis cost into visible frame drops.
The standard mitigation is caching. Generate a pool of clips at startup — five to ten variations per surface — and cycle through them at runtime. The variation is preserved because the pool contains genuinely different waveforms, and the runtime cost drops to zero because nothing is being generated during gameplay. Generating at 22050 Hz instead of 44100 Hz halves the synthesis cost with no audible loss, because a footstep has no useful content above 8 kHz. The generated clip is resampled to the audio system's native rate automatically.
When does a code-generated footstep beat a recorded one?
The procedural approach is not the right choice for every project, and the dividing line is specific.
A recorded footstep carries imperfections that make it sound like a place: a slight room tone, a specific shoe, a specific surface. A synthesized footstep is more abstract, more idealized, and the ear can sometimes hear the difference. This is not a defect — it is a design constraint. Procedural footsteps work best in stylized games where the audio is already abstract by intent, and less well in games where the audio is expected to sound like a recording of a real environment.
A project with a small number of footsteps on a small number of surfaces is better served by the traditional sample approach. Record or acquire a set of variations, let the engine randomize between them, and accept the slight repetition that comes with a finite pool. A project with many surfaces, high variation requirements, or a stylized aesthetic is a better fit for synthesis.
The clearest test is whether the game needs footsteps that sound like recordings of a real place, or footsteps that sound like the idea of a footstep. The first case is served by samples. The second case is served by code, and the code has the additional advantage of scaling — a hundred surfaces cost the same runtime as one, because the surface is a parameter rather than a set of files.
Generate a Footstep WAV for Comparison
Open the SfxMaker footstep generator and create a WAV file that follows the same three-layer model described in this article. The generated file can be compared against the code output to verify that the parameters are producing the intended surface character.
Open Footstep Sound Generator →