The intuitive way to write a game loop is the wrong one. Sample how much time has passed since the last frame, hand that delta to the physics step, render, repeat. This works visually for the same reason it fails as a simulation: every frame steps a different amount of time, so the same input on the same code produces different results on different machines, on different runs, and on the same machine when something incidental — a background process, a debugger pause — perturbs frame timing.
A fixed time step decouples the simulation’s clock from the display’s clock and makes the simulation deterministic by construction. This Nibble walks through the canonical accumulator pattern from Glenn Fiedler’s Fix Your Timestep!, the interpolation step that keeps it visually smooth, and the edge cases that bite in production.
Three things to take away:
- A fixed time step means physics integrates with a constant
dt(e.g., 1/60 s), regardless of how often the renderer runs. - An accumulator bridges real time and simulation time: deposit elapsed real time into a bucket each frame, withdraw fixed-size chunks for physics steps until the bucket isn’t full enough.
- Render with interpolation between the two most recent physics states to hide the step-rate / frame-rate mismatch — and clamp the accumulator to avoid the spiral of death.
Why variable dt is the wrong default
The naive loop steps physics by however much real time has passed since the last frame:
auto previous = clock::now();
while (running) {
auto now = clock::now();
double dt = std::chrono::duration<double>(now - previous).count();
previous = now;
integrate(state, dt);
render(state);
}
This produces three problems, all subtle:
- Non-determinism. The same inputs produce different states on different runs, because
dtis never the same twice. A replay system can’t reproduce a session, a multiplayer protocol can’t lock-step state across clients, and a bug that occurred at 16.7 ms can’t be reproduced at 16.4 ms. - Numerical instability at large
dt. Most numerical integrators (Euler, RK4) have stability margins that depend on step size. A frame stall doubles the nextdt; springs start oscillating, fast-moving objects pass through walls, stacked rigid bodies explode. The quality of the simulation degrades when the simulation is most asked to work. - Frame-rate-dependent gameplay. Physics that uses real
dtruns faster on faster machines unless every formula is meticulously time-scaled. Even when it is, floating-point roundoff accumulates differently per frame rate, so two players on different hardware see different outcomes.
The standard reference on this — Glenn Fiedler’s Fix Your Timestep! — opens by acknowledging that variable-dt is sometimes the right answer (single-player games where physics is decorative, with constant frame rates). For everything else, fixed steps are the foundation.
The accumulator pattern
The fix is to step physics in fixed-size chunks, regardless of how often the loop body runs. Real elapsed time goes into an accumulator each frame; physics drains the accumulator dt-sized step at a time:
const double dt = 1.0 / 60.0; // 60 Hz simulation
double accumulator = 0.0;
auto previous = clock::now();
while (running) {
auto now = clock::now();
double frameTime = std::chrono::duration<double>(now - previous).count();
previous = now;
accumulator += frameTime;
while (accumulator >= dt) {
integrate(state, dt); // always exactly dt
accumulator -= dt;
}
render(state);
}
Two clocks are now in play. Real time drives the outer loop; simulation time advances in fixed dt increments inside the inner loop. A machine running at 240 FPS renders four times between each physics step on average; a machine at 30 FPS runs two physics steps per render. The simulation itself is identical on both — same dt, same integrator, same arithmetic in the same order — so the same inputs produce the same outputs.
This is what deterministic means in this context: not just “reproducible,” but bit-for-bit reproducible given the same input sequence and initial state. Replay systems, networked lockstep protocols, and regression tests all depend on this property.
The visual stutter problem and the interpolation step
One subtlety: the renderer sees the simulation only at fixed multiples of dt. If the simulation runs at 60 Hz and the display runs at 144 Hz, most rendered frames see the same physics state as the previous one — you’d be drawing the same positions repeatedly between physics steps, producing visible stutter even though the simulation itself is smooth.
The fix is to interpolate between the two most recent physics states, weighted by how much of the current step has elapsed:
const double dt = 1.0 / 60.0;
double accumulator = 0.0;
State previousState{}, currentState{};
while (running) {
/* ...accumulator update as before... */
while (accumulator >= dt) {
previousState = currentState;
integrate(currentState, dt);
accumulator -= dt;
}
const double alpha = accumulator / dt;
State display = lerp(previousState, currentState, alpha);
render(display);
}
After the inner loop drains the accumulator, the remainder (accumulator < dt) tells you how far the renderer is into the next physics step. Dividing by dt gives an alpha in [0, 1) — 0 means “exactly at the previous step”, values near 1 mean “almost at the current step.” Linearly interpolating the two states by alpha produces smooth motion even when the display rate doesn’t match the simulation rate.
The cost is one frame’s worth of latency: the renderer is always showing a state that’s between two physics steps, never the absolute newest one. For most games this is imperceptible; for fighting games and rhythm games where input lag matters, it’s a known trade-off (and the reason some genres prefer to lock the display to the physics rate instead).
The spiral of death
If a single physics step takes longer than dt of real time to compute, the loop falls behind: the accumulator grows faster than the inner loop drains it, the next frame has more time to catch up on, more steps run, those take longer, and the game freezes while the simulation tries to catch up to a real time it can never reach. This is the spiral of death.
The defence is a max-step clamp:
const int MAX_STEPS = 5;
int steps = 0;
while (accumulator >= dt && steps < MAX_STEPS) {
integrate(state, dt);
accumulator -= dt;
++steps;
}
if (accumulator >= dt) {
// We're behind. Drop the surplus rather than chasing it.
accumulator = 0.0;
}
When the budget is exhausted, the loop abandons the surplus rather than chasing real time. The simulation slows down visibly — clock time falls behind wall time — but the game keeps responding instead of freezing. This is the deliberate trade-off: in the rare case where physics genuinely cannot keep up, the program degrades gracefully.
Float precision and integer time
The pattern above uses double for the accumulator, which is fine for most cases but not for strict determinism across machines. Floating-point accumulation is order-dependent, and different compilers, different CPUs, and different optimisation levels can produce slightly different sums.
When determinism truly matters — networked lockstep, replay systems, regression tests that compare hashes of game state — the accumulator should be an integer (typically int64_t nanoseconds, or fixed-point microseconds). Convert to floating point only at the boundary, when feeding dt to the integrator. The integrator itself can still use floats; what must be deterministic is the bookkeeping, so the same number of steps run for the same input regardless of platform.
Takeaway
Tying physics to render frame time is the source of every “works on my machine” simulation bug. A fixed time step, managed by an accumulator that bridges real and simulation time, gives you reproducible physics regardless of frame rate; interpolation between the last two physics states keeps the visuals smooth; a max-step clamp keeps a slow frame from spiralling into a freeze. The rule is simple: physics should not care what frame rate the renderer is running at, and the accumulator pattern is what makes that true.