The game loop and fixed timestep
x += v per frame): the game runs faster on faster hardware. The cure is a fixed timestep: the simulation ticks at a strictly constant dt, and rendering happens whenever it can. The bridge between them is the accumulator: you bank the elapsed time and run as many fixed steps as fit into it; the leftover time is absorbed by interpolation at draw time. That gives you frame-rate independence, stable physics and determinism (replays, lockstep netcode).
The mechanism
The simplest loop has looked the same since the 1970s:
On 60 Hz hardware, one turn of the loop has this budget:
Blow the budget and you get a dropped frame, a hitch, audio desync. But the real trap is deeper — what the time step inside update() actually equals.
Bug #1: frame-tied movement
If update() says "move by v per call", the object's speed becomes proportional to the frame rate. Distance covered in one real second:
This is literally the reason old PCs had a Turbo button: a game was calibrated for one processor, ran unplayably fast on a quicker one, and "turbo" had to be switched off to throttle the clock back down.
The half-fix: multiply by dt
The obvious step is scaling by the real delta: x += v·dt. Speed no longer depends on FPS. But dt wanders from frame to frame, and that hurts physics: a variable integration step → a different error every frame, unstable contacts, tunneling through walls at large dt, and — above all — non-determinism: the same input gives a different result. Replays and lockstep are broken.
The fix: fixed step + accumulator
Decouple the simulation (strictly constant dt) from rendering (whenever it lands). Bank the elapsed time in an accumulator a and run a whole number of fixed steps:
The number of steps per frame is
The simulation always sees the same dt — determinism and stable physics. Rendering can run more often (between ticks) or less often (several ticks per frame).
Leftover time → interpolation
After the loop the accumulator still holds a < dt — time that wasn't stepped. If you draw the latest state as is, then when render>sim you get judder (temporal aliasing): the picture only updates simHz times per second. The cure is interpolating between the previous and the current simulation state:
A worked example. dt = 16.67 ms (sim at 60 Hz). A render frame took 55 ms (a hitch): a banked 55 ms → n = ⌊55/16.67⌋ = 3 steps (3·16.67 = 50.01 ms), leftover a ≈ 4.99 ms. The simulation honestly caught up 3 ticks, and the ~5 ms that weren't stepped go into interpolation. A frame took 8 ms (rendering at 120 Hz): n = 0 steps, a = 8 ms → draw with interpolation at α = 8/16.67 ≈ 0.48 between the last two ticks.
The spiral of death — and the clamp that stops it
If update() itself takes longer than dt, the accumulator grows faster than it drains: every frame adds more steps than it managed to run → the next frame is longer still → the spiral of death, and the game locks up. The defense is a clamp on the maximum number of steps (or on frameTime): if too much has piled up, throw the excess away. The simulation honestly "slows down" (slow-mo) instead of freezing outright. In practice: frameTime = min(realDt, 0.25 s) → no more than ~15 steps per frame at 60 Hz.
🕹 Games to play — and what to notice
The same defect — "logic tied to the frame" — surfaces in every era, from DOS to Bethesda. For each case: how it was done (or broken) and what to switch on or fiddle with to see it with your own eyes. From "no loop discipline at all" to "done right".
Early PC games ran the loop "flat out" with no timing at all: gameplay speed = processor clock speed. A game was calibrated for a specific CPU; on a faster one it flew by unplayably. Hence the Turbo button on the case, which in fact lowered the clock — so old games wouldn't turn into a slide show in reverse.
🎮 Play: open any DOS game in DOSBox and drag the "cycles" slider (Ctrl+F11 / Ctrl+F12). The whole game — movement, animation, timers — speeds up and slows down along with the "processor". That is a game loop with no decoupling from hardware whatsoever.
On the 60 FPS versions (PC, current-gen) weapon durability dropped roughly twice as fast as on the 30 FPS consoles — when hitting bodies, walls and floors. The cause is a classic: the durability subtraction ran once per render frame rather than once per fixed tick. Twice the frames → twice the deductions. FROM patched it in 2015.
🎮 Play: on an unpatched DS2, beat on a corpse or a wall at 30 and at 60 FPS and compare the durability loss. The textbook "per-frame instead of per-tick" that should have lived in a fixed step.
The Havok integrator is hard-wired to 60 FPS (fMaxTime ≈ 0.0166 s). Remove the frame cap and above 60 the physics loses its mind: objects fly apart, water flickers, NPCs wander off their routes, carts launch into the sky. The community fix is a dynamic fMaxTime driven by the current FPS: exactly the clamped accumulator from this lesson, bolted on from the outside.
🎮 Play: in Skyrim, remove the frame cap (without the fix mods) and walk into a cluttered room — the dishes and corpses will stage a poltergeist. That is what "the physics step is nailed to the render rate" means.
Precision platformers keep the simulation on a fixed step, so it is deterministic: the same input, frame by frame, gives the same result. That is what makes frame-perfect tricks, reproducible TAS runs and replays possible. The "feel" in game feel rests on a stable tick and predictably polled input.
🎮 Watch: a Celeste TAS recording or a speedrun — frame-perfect techniques repeat identically from run to run. That only works because the simulation advances in equal fixed steps regardless of rendering.
Deep end: numerical integration — why a fixed dt stabilizes physicsskippable
Motion is an ODE: ẋ = v, v̇ = a(x). A real frame discretizes it, and the choice of discretization decides whether the simulation blows up.
Explicit (forward) Euler — and why it "pumps in" energy
You update position from the old velocity: x += v·dt; v += a·dt. For an oscillator (a spring, an orbit) total energy grows every step — the amplitude diverges. Per-step error is O(dt²), globally O(dt); at large dt it is unstable on top of that.
Semi-implicit (symplectic) Euler — the workhorse of games
Velocity first, then position from the new velocity: v += a·dt; x += v·dt. Swap two lines and the method becomes symplectic: energy does not drift systematically, orbits and springs stay stable. Almost every game engine integrates this way. Free in cost, an order of magnitude more stable.
Why the step must be FIXED
- Constant error. A variable
dtmeans a different local error every frame → jittery behavior; a fixed step makes it predictable. - No tunneling. At a large
dtan object jumps straight through a thin wall (per-step displacement > collider thickness). A small fixed step caps the maximum displacement per tick. - Determinism. Equal steps + one seed → a bit-for-bit identical run. That is a mandatory condition for lockstep netcode (RTS) and rollback (fighting games), and for replays and TAS.
RK4 is more accurate, but it needs several force evaluations per step and is overkill for gameplay physics — physics engines take a fixed step + symplectic integration + sequential impulse for contacts.
Deep end · engineering: where dt lives in engines, and where "floaty" input comes fromskippable
- The split in engine APIs. Unity:
FixedUpdate()— physics on a fixed step (Time.fixedDeltaTime, 0.02 s = 50 Hz by default);Update()— rendering/input with a variable delta. Godot:_physics_process(delta)(fixed, "Physics Ticks/sec", 60) versus_process(delta)(variable). Unreal — sub-stepping in physics. This is the accumulator, hidden inside the engine. - Interpolation against judder. Rendering faster than the simulation → you visually interpolate between the last two ticks (
Rigidbody.interpolationin Unity). Without it, a high FPS won't save you from strobing at a low sim rate. - The CPU→GPU pipeline. The CPU runs 1–2 frames ahead of the GPU. Input is polled once per render frame → it feels "floaty"; the mitigations are late input polling, buffering, NVIDIA Reflex / AMD Anti-Lag.
- Server tick rate. In a networked game the authoritative simulation ticks at a fixed rate (64 Hz, say); the client's rendering is decoupled and interpolates other entities. Without a fixed tick, desync is inevitable.
dt, whatever else is going on. Rendering is a photographer snapping shots at arbitrary moments. The accumulator is the queue of parts piled up between beats; interpolation is the photographer catching a part between two belt positions and showing it where it would be right now. The belt's beat never changes — otherwise the parts come out different sizes (non-determinism).
ML / AI (your domain): the fixed step ⇄ discretization in ODE solvers, Neural ODEs and diffusion (fixed vs adaptive step; why DDPM walks a fixed grid of noise steps). The replay buffer in RL is literally an accumulator between the environment's step rate and the training update rate; gradient accumulation banks micro-batches up to a single fixed optimizer "step". Determinism = reproducible training (fixed seed, fixed data order).
Backend / systems: token-bucket rate limiting is the same accumulator; decoupling ingest rate from processing rate through a queue; a control loop at a fixed rate instead of "whenever it arrives".
Control theory / DSP: a fixed sampling rate (Nyquist), discrete controllers — a variable step breaks both the analysis and the stability.
The principle: don't let the consumer's rate dictate your logic; bank into a buffer and process in fixed portions — then the result is stable and reproducible.
_physics_process(delta) and then in _process(delta), one at a time. Turn off V-Sync and push the FPS up and down: the _process version will move at a different speed or judder, the _physics_process one won't. Change "Physics Ticks/sec" (60 → 10) and toggle interpolation — you'll see the strobing and how it gets cured.n frames instead of seconds. Force a hitch (load a heavy scene) and check that you get smooth slow-mo rather than a spiral of death.If x += v·dt is already frame-rate independent — why a fixed step at all?
dt gives a different integration error every frame (jittery physics), a risk of tunneling during hitches and, above all, non-determinism: the same input → a different result. Replays, lockstep netcode and TAS need bit-for-bit reproducibility, and only a constant step delivers it. v·dt fixes speed, but it fixes neither stability nor reproducibility.Why does swapping two lines (semi-implicit Euler) change stability so much?
What is the "spiral of death" physically, and why does a clamp cure it?
update() takes longer than dt, more time flows into the accumulator per frame than flows out → the step count for the next frame grows → the frame gets longer still → positive feedback, and the game stalls. A clamp on frameTime (or on the maximum step count) breaks the loop: the excess time is discarded and the simulation shifts into slow-mo instead of freezing. The price is that on heavy frames game time falls behind real time, but that beats a freeze.If the sim runs at 60 Hz and the monitor at 144 — will there be judder at 144 Hz without interpolation?
α = a/dt between the last two ticks fills the gaps and gives smooth motion at any render rate. The alternative is extrapolation (predicting forward), but it gets sharp changes wrong and produces snap-backs.Can you just raise the sim rate and forget about interpolation?
a (rendering still isn't a multiple of the sim rate). For physics this is sometimes exactly what's done, but "free smoothness" is interpolation, not brute force. On mobile or weak hardware a high sim rate simply doesn't fit.- Glenn Fiedler, "Fix Your Timestep!" (gafferongames.com) — the canonical treatment of the accumulator and interpolation.
- Robert Nystrom, "Game Programming Patterns" — the Game Loop chapter (free online).
- Module 1, "Core Game Loop Architecture" + Module 8, "Game Loop Patterns".