§1 Engine internals
Writing a Fixed-Timestep Game Loop That Doesn't Drift
Your game runs perfectly on your machine. Then a player with a 240 Hz monitor reports that their character sprints twice as fast and falls through the floor. Both bugs have the same root cause, and the fix is one of the most reused patterns in game programming: the fixed-timestep loop.
The trap: tying simulation to frame rate
The naive loop measures how long the last frame took and moves everything by that amount:
while (running) {
float dt = timer.elapsed(); // seconds since last frame
update(dt);
render();
}
This looks reasonable. Multiply velocity by dt and objects move at the same real-world speed regardless of frame rate — in theory. In practice two things go wrong.
First, determinism dies. Floating-point multiplication by a value that changes every frame means two runs of the same input never produce the same result. Replays desync. Networked clients drift apart. Physics that depends on accumulated state — springs, stacked boxes, anything integrated over time — behaves differently at 30 fps than at 144 fps.
Second, collision tunnelling. At a high frame rate dt is tiny, so each step is fine. But if the machine hitches and dt spikes to 250 ms, a fast object moves a huge distance in one step and skips clean through a thin wall. The collision check only ever sees the start and end points, never the wall in between.
The core idea: the simulation should advance in constant-sized slices of time, always the same size, no matter how fast or slow the frame rate is. Rendering can happen whenever it likes.
Decoupling the two clocks
A game really has two loops running at different rates. The simulation wants a steady heartbeat — say 60 updates per second, exactly 16.667 ms each. The renderer wants to draw as often as the display allows, which might be 30, 60, 144 or an uneven mess. The fixed-timestep pattern keeps them separate and reconnects them with an accumulator.
The accumulator is a single float. Each frame you add the real elapsed time to it, then spend it in fixed chunks:
const double STEP = 1.0 / 60.0; // 16.667 ms simulation slice
double accumulator = 0.0;
double previous = now();
while (running) {
double current = now();
double frameTime = current - previous;
previous = current;
// clamp to avoid the "spiral of death" after a long stall
if (frameTime > 0.25) frameTime = 0.25;
accumulator += frameTime;
while (accumulator >= STEP) {
update(STEP); // always the SAME dt
accumulator -= STEP;
}
render();
}
Read the inner while carefully. If a frame took 33 ms, the accumulator holds enough for two full simulation steps, so update runs twice — each time with the identical STEP. If a frame took 8 ms, the accumulator hasn't filled a slice yet, so update runs zero times this frame and the leftover time carries forward. The simulation always advances in whole, equal steps.
The clamp that saves you
The line if (frameTime > 0.25) is not optional. Imagine the process is paused — a breakpoint, a garbage-collection pause, the player dragging the window. When it resumes, frameTime might be several seconds. Without the clamp, the accumulator demands hundreds of simulation steps in one frame, each of which takes time, which makes the next frame even longer, which demands even more steps. The game freezes solid. This runaway is called the spiral of death, and the one-line clamp prevents it by simply dropping the missing time.
The part tutorials skip: interpolation
There's a subtle problem left. The simulation lands on tidy 16.667 ms boundaries, but rendering happens at arbitrary moments in between. If you draw objects at their raw simulation position, you get visible stutter — the render is showing a state that's slightly stale by a fraction of a step. At high refresh rates this reads as juddery motion even though the physics is perfect.
The fix is to render a blend between the previous and current simulation states, using how far into the next step the accumulator has gotten:
// after the fixed-update loop, before rendering
const double alpha = accumulator / STEP; // 0.0 .. 1.0
render(alpha);
Inside render, each interpolated body mixes its two most recent states:
void render(double alpha) {
for (auto& body : bodies) {
Vec2 shown = body.previous * (1.0 - alpha)
+ body.current * alpha;
draw(body.sprite, shown);
}
}
To make this work, your update copies current into previous before integrating the new current. Now the picture on screen is a smooth slide between two known-good states, and it stays smooth at any refresh rate while the underlying simulation remains bit-for-bit deterministic.
Rule of thumb: simulate in fixed steps for correctness; interpolate on render for smoothness. Never let the two responsibilities leak into each other.
Choosing your step size
60 Hz (16.667 ms) is a sensible default. A larger step (30 Hz) saves CPU but makes fast collisions and input feel coarse. A smaller step (120 Hz) tightens physics at real cost, since every extra step is a full simulation pass. Fighting games and anything with precise contacts often go to 120 Hz or higher; a cozy puzzle game is fine at 30. Pick the largest step that still feels right, because every step you add is work multiplied by every body in the world.
What you've actually bought
Once this loop is in place, three classes of bug disappear at once. Speed no longer depends on frame rate. Replays and lockstep netcode become possible because the same inputs produce the same states. And the tunnelling that made fast objects clip through walls is gone, because the step size is bounded and predictable. It's maybe forty lines of code, and it's the foundation almost every other system sits on. Get it right early — retrofitting it into a shipped game is genuinely painful.
Read next
- An Entity Component System From Scratch, Explained Simply →
- Profiling Your Game: Finding the 3% of Code Eating 90% of Your Frame →
Members download the runnable sample loop with interpolation wired up. See membership →