← Module 1/The game loop
RU
Module 1 · Arcades and foundations

The game loop and fixed timestep

Why "move by the speed every frame" is a bug, and how a single accumulator decouples the simulation from the frame rate. The most fundamental pattern in an engine.
🏠 lab~18 min
The gist in 20 seconds
Every game runs a loop: input → update state → render. The naive mistake is tying movement to the frame (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:

while(running){ input(); update(); render(); }

On 60 Hz hardware, one turn of the loop has this budget:

tframe= 1f, 160 s≈16.67 ms; 130 s≈33.3 ms

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:

s=v·f (at 120 FPS — twice as far as at 60)

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:

a += frameTime while(a >= dt){ update(dt); a -= dt; } render( alpha = a / dt )

The number of steps per frame is

n= ⌊adt⌋

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:

α=adt, xrender= (1−α) xprev + α· xcurr
frame = bank time → run fixed steps → draw with interpolation sim fixed dt (equal intervals) render variable FPS (as fast as the hardware manages) α = a/dt frame between ticks → interpolation

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".

The DOS era and the Turbo button 1980s–90s · no loop, speed = CPU clock

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.

Dark Souls II 2014 · PC · durability computed per frame

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.

Skyrim / Fallout (Creation Engine) Havok physics nailed to 60 Hz

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.

Celeste / a careful platformer fixed step done right → determinism

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 dt means a different local error every frame → jittery behavior; a fixed step makes it predictable.
  • No tunneling. At a large dt an 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.interpolation in 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.
Analogy
The simulation is an assembly line with a hard clock: the belt moves strictly once per 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).
Why it matters
Frame-rate independence is the line between "works on my machine" and "shipped". Tie logic to FPS and the game speeds up on fast hardware (DOS), eats durability at 60 frames (DS2) or shreds the physics above 60 (Skyrim). A fixed step with interpolation is the boring, correct foundation under every engine, and the only route to determinism — without which there is no lockstep, no replays and no honest TAS.
🔁 Beyond games — where this transfers
The lesson gives you three transferable moves: decouple producer and consumer rates through a buffer, discretize continuous time with a fixed step for stability and reproducibility, and determinism through fixed step + fixed seed.

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.

🏠 Lab — the game loop live
An interactive lab with no code: three objects move at "the same" speed under three strategies (frame-tied / multiplied by dt / fixed step with interpolation). Crank the render FPS and the simulation rate, toggle interpolation, hit "hitch" — and watch who desyncs and how the accumulator fills. Open the lab →
Best moment: drop the FPS to 15 in "frame-tied" mode — the red one moves four times slower than the green one (fixed step), even though their "speed" is the same.
🔧 Run it and poke at it — on your home machine
What to play is above (🕹). Here — get inside an engine and feel the fixed step with your hands:
🔧 Poke at it (debug) ~40 min, Godot
In Godot, make a falling body. Put the movement logic in _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.
🧪 Test it (QA eyes) ~15 min
Hunt for frame-dependent bugs: gameplay speeding up at high FPS, different jump distance or speed on different monitors, poltergeist physics above 60, timers counted in 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.
Checklist: saw the difference between _physics_process and _process with V-Sync off; caught the strobing at a low sim rate and removed it with interpolation; found at least one frame-dependent bug.
Connections
example
Doom and BSP — Doom's logic ran at a fixed 35 ticks/sec independently of rendering; its demos and networked play rest on that determinism.
related
Game feel — responsiveness and "juice" stand on a stable tick and predictable input polling; a ragged loop kills the feel.
next
ECS and data-oriented design — every tick runs the systems over the components; this is where the loop's structure meets data layout.
Questions worth asking
If x += v·dt is already frame-rate independent — why a fixed step at all?
Frame-rate independence isn't the only requirement. A variable 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?
Explicit Euler updates position from the old velocity and, for an oscillator, systematically adds energy — the amplitude diverges. The semi-implicit version updates velocity first, then position from the new velocity; the method becomes symplectic — it preserves phase-space volume, and energy oscillates around the true value instead of drifting. Same cost, an order of magnitude more stable; hence it is the default in games.
What is the "spiral of death" physically, and why does a clamp cure it?
If one 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?
Yes. State changes only 60 times per second while 144 frames are drawn → the same positions are shown for 2–3 frames in a row, unevenly → temporal aliasing (micro-strobing). Interpolating with α = 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?
Partly. A high fixed rate (120–240 Hz, say) makes the strobing less noticeable and improves contact accuracy, but it costs CPU linearly and doesn't fully remove the leftover 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.
Further reading