All tutorials Mighty Professional
Build a Game Engine · The runtime spine

The Game Loop & Time

The loop that drives the whole engine is a few lines long, and each naive version of it has a well-known failure. Tie the simulation to frame rate and the game runs in slow motion on a weak machine and too fast on a quick one; scale by measured frame time instead and a frame spike can make the physics blow up. The fix is a with an , and a render that interpolates between simulation states. We build it from the naive version up, and stop at every failure that forced the next change.

Time~55 min LevelBeginner to mid PrereqsYou can read C++ or Rust. The 3D Math tutorial helps, since interpolation blends two states. StackC++ & Rust
◂ Build a Game Engine Phase 3 · The runtime spine Next · Platform & Window ▸

01The loop and the kinds of time

A game is a loop: read input, advance the simulation, draw a frame, repeat. The whole subject of this tutorial is the middle step, advancing the simulation, and specifically how much time each advance represents. Get that wrong and everything downstream (physics, animation, networking) inherits the error.

It helps to separate the kinds of time an engine juggles[1]. Real time is the wall clock. Simulation time is how far the game world has advanced, which can run slower than real time (a hitch), faster (fast-forward), or stop (pause) without the program freezing. Local time is per-system, like the playhead of one animation clip. This tutorial is about keeping simulation time correct and stable as real time does whatever the hardware makes it do.

What you'll have by the end

A fixed-timestep loop with an accumulator and render interpolation, written in C++ and Rust with matching names; a precise account of why variable-timestep loops are non-deterministic and unstable; the and the two guards against it; and the difference between , which two widely cited articles blur in their naming.

02The naive loop

The first loop everyone writes advances the world by a fixed amount of work per iteration, with no notion of elapsed time:

The frame-rate-dependent loop
while (running) {
    pollInput();
    update();        // advances by a fixed step of WORK, not time
    render();
}
while running {
    poll_input();
    update();        // advances by a fixed step of WORK, not time
    render();
}

The simulation speed equals the loop's iteration rate, which is set by the hardware and the current load[2]. A faster CPU runs the game faster; a slower one runs it in slow motion. It's why some early PC games became unplayably fast on later hardware.

Trap: vsync does not fix this

Capping the loop with vsync bounds the upper rate, so the coupling stops being visible on hardware fast enough to hit the cap. The cap is the monitor's refresh rate, though, so the same game runs 2.4× faster on a 144 Hz display than on a 60 Hz one, and a machine that can't keep up still under-runs and goes slow-motion. Vsync hides the symptom on some setups; it doesn't decouple simulation from frame rate.

03Variable timestep

The obvious fix: measure how long the last frame took and scale everything by that delta time.

Variable timestep
auto previous = Clock::now();
while (running) {
    auto now = Clock::now();
    double dt = std::chrono::duration<double>(now - previous).count();
    previous = now;
    update(dt);     // position += velocity * dt
    render();
}
let mut previous = Instant::now();
while running {
    let now = Instant::now();
    let dt = (now - previous).as_secs_f64();
    previous = now;
    update(dt);     // position += velocity * dt
    render();
}

Wall-clock speed is now consistent across frame rates, and for a lot of gameplay code this is fine. For the simulation core it brings three new failures:

04Semi-fixed: clamp the step

First partial fix: never integrate more than a maximum dt at once. If a frame took longer, take several capped steps[3].

Semi-fixed timestep (Fiedler)
while (frameTime > 0.0) {
    double step = std::min(frameTime, dt);  // never exceed dt
    integrate(state, step);
    frameTime -= step;
}
while frame_time > 0.0 {
    let step = frame_time.min(dt);          // never exceed dt
    integrate(&mut state, step);
    frame_time -= step;
}

This bounds the step size, which removes large-step instability and reduces tunneling (a fast enough object still crosses a thin wall within one dt). It is still not deterministic: the last step each frame is a partial dt, so step sizes still vary frame to frame. That trailing remainder is exactly what the accumulator pattern fixes, by deferring it instead of stepping it.

05Fixed timestep and the accumulator

The canonical loop. Accumulate real frame time, then consume it in whole dt chunks, carrying the leftover forward to the next frame[3]. Every simulation step is now exactly the same size, which makes it stable and reproducible.

Fixed timestep with an accumulator
using Clock = std::chrono::steady_clock;     // monotonic; never wall-clock for dt
const double dt = 1.0 / 60.0;                  // fixed simulation step (seconds)
double accumulator = 0.0;
auto currentTime = Clock::now();

while (running) {
    auto newTime = Clock::now();
    double frameTime = std::chrono::duration<double>(newTime - currentTime).count();
    if (frameTime > 0.25) frameTime = 0.25;  // spiral-of-death clamp (Fiedler's example)
    currentTime = newTime;

    accumulator += frameTime;
    while (accumulator >= dt) {           // >=, not >: a full step is available
        previousState = currentState;     // keep the prior state for interpolation
        integrate(currentState, dt);       // every step is exactly dt
        accumulator -= dt;                // carry the remainder (< dt) to next frame
    }

    double alpha = accumulator / dt;       // 0..1: how far past the last step we are
    render(lerp(previousState, currentState, alpha));
}
use std::time::Instant;                        // monotonic; analog of steady_clock
let dt: f64 = 1.0 / 60.0;                     // fixed simulation step (seconds)
let mut accumulator: f64 = 0.0;
let mut current_time = Instant::now();

while running {
    let new_time = Instant::now();
    let mut frame_time = (new_time - current_time).as_secs_f64();
    if frame_time > 0.25 { frame_time = 0.25; }   // spiral-of-death clamp
    current_time = new_time;

    accumulator += frame_time;
    while accumulator >= dt {                  // >=, not >: a full step is available
        previous_state = current_state.clone();   // keep the prior state for interpolation
        integrate(&mut current_state, dt);    // every step is exactly dt
        accumulator -= dt;                    // carry the remainder (< dt) forward
    }

    let alpha = accumulator / dt;             // 0..1
    render(lerp(&previous_state, &current_state, alpha as f32));
}

Three lines carry the whole design, so be precise about each:

Fixed timestep is not fixed frame rate

A fixed timestep means the simulation advances in constant dt increments. A fixed frame rate means the display refreshes at a constant rate. They are independent: the normal case is a fixed-timestep simulation running under a variable, display-driven frame rate. The 0.25 s clamp is Fiedler's example value, not a universal constant; pick yours from your worst tolerable hitch.

The widget runs the same bouncing object under three loops at once. Drag the render-rate slider and fire a frame spike to see which ones break:

The simulation ticks at 16 Hz, deliberately coarse so the ticks are visible. At any render rate above that, the bare fixed loop judders: its drawn position only changes on tick boundaries, so some frames repeat a position and others jump, and its step count reads 0, 1 or 2. The interpolated loop runs the same ticks and moves as smoothly as the variable loop, because it draws the leftover accumulator as a blend. Below 16 FPS every track is choppy, since nothing can show motion between frames the display never draws. The spike hands the variable loop all 0.32 s in one step; the fixed loops clamp it to 0.25 s and take it as four ticks, so they jump a little less and fall slightly behind the wall clock.

Wrong answers, and why: zeroing the accumulator loses time (slow sim); a final partial step is the semi-fixed variant that breaks determinism; and fixed timestep constrains the simulation, not the display or vsync.

06Decoupling sim from render

The accumulator already decoupled the two rates. The inner loop runs the simulation zero, one, or several times per rendered frame; rendering happens once at the end, as often as the display allows. Unity says the same about its fixed step: FixedUpdate is called "zero, one, or multiple times per frame depending on the frame rate"[6], and Godot splits a fixed-rate _physics_process from a variable-rate _process[10].

Where input fits, and why it is a determinism risk

In the single-threaded loop above, input is sampled once per rendered frame and fed to whatever simulation steps run that frame, not once per simulation tick. That is fine for most games and is itself a small source of input-timing variation. Lockstep and rollback netcode tie input to ticks instead, which the networking modules cover.

07Interpolation, not extrapolation

With a fixed simulation and a faster display, rendered frames fall between simulation states, so the picture stutters. The fix is to keep the previous and current states and blend them with alpha[3].

Render the blended state
// alpha in [0,1): how far between previous and current we are.
State shown = previousState * (1.0 - alpha) + currentState * alpha;
render(shown);
// alpha in [0,1): how far between previous and current we are.
let shown = lerp(&previous_state, &current_state, alpha as f32);  // previous*(1-alpha) + current*alpha
render(shown);

Because this blends two states that were already computed, it never invents an unphysical position. The cost is latency: the image shows a moment between the last two ticks, so it trails the newest simulated state by up to one fixed step, and real time by more. Unity describes its rigidbody interpolation this way, using "the previous two physics updates" so the body appears "to move slightly behind where it should be"[8]; Godot's docs put the rendered position "between 1 and 2 ticks into the past"[11].

The naming trap: interpolation vs extrapolation

Two of the most-cited game-loop articles blur the terms. deWiTTERS[5] names its render factor interpolation in code that extrapolates: it predicts forward from the current state and speed (position + speed * interpolation). Nystrom[2] renders the same kind of forward prediction, describes it as "interpolated between where it was on the last frame and where we think it will be on the next frame", and only then calls it extrapolation. Extrapolation adds no latency, but when the prediction is wrong (a collision, a direction change) the next tick corrects it and the object visibly snaps. True interpolation (Fiedler, Unity, Godot) blends past and present and trades latency for never overshooting. When an article says "interpolation", check which of the two its code does.

Toggle interpolation off to see the stutter, then switch between interpolate and extrapolate and bounce the ball off a wall to see the overshoot:

Off: the ball jumps once per simulation tick. Interpolate: smooth, but the drawn ball trails the true tick position by up to one step (the ghost marks the current tick state). Extrapolate: smooth and on time mid-flight, but at a bounce the predicted position shoots through the wall and snaps back when the next tick corrects it. That snap is why interpolation is the safer default.

08The spiral of death

One failure mode of the accumulator deserves its own name. If a single simulation step costs more than dt of real time to compute, each frame adds more to the accumulator than the inner loop can drain, so next frame needs even more steps, which take even longer[3]. The loop spirals until it locks up.

There are two guards, working from opposite ends, and engines use one or both:

The guard trades accuracy for liveness

Neither guard makes the simulation keep real time when the machine can't. They make it fall behind in slow motion instead of freezing. The runaway needs a per-step cost near or above dt, and several things can push a loop there or kick it off (a too-small dt, an expensive collision frame, a GC or allocation hitch, OS preemption), so there is no single fix, just the two backstops.

Raise the per-step cost above the frame budget and watch the accumulator. Then turn the clamp on:

Below budget, the accumulator stays flat and steps-per-frame hovers near one. Push the per-step cost past the budget with the clamp off and both climb every frame, the runaway. Turn the clamp and step cap on and steps-per-frame plateaus at the cap while the accumulator stays under one step, because the leftover time is dropped: the simulation slows down, but the loop stays alive.

09Determinism

A fixed timestep is the usual foundation for a reproducible simulation, since every run takes the same-sized steps, but it is not enough on its own.

Three sources of divergence survive a fixed step. Floating-point results differ across compilers, optimization levels, architectures, and fast-math settings, so the same code can produce slightly different numbers on different toolchains. Float addition is not associative, so changing the order operations are summed changes the result. And input timing or sampling can differ run to run. Nystrom puts the practical consequence plainly: with variable steps "the same bullet will end up in different places on their machines"[2]; a fixed step removes that particular variable but not the float ones.

Scope the float claim

Float math is not "always nondeterministic." With an identical binary, identical flags, and the same architecture it can be perfectly reproducible. Divergence shows up across differing toolchains or hardware, or with fast-math. The mechanics live in the IEEE-754 module; here it is enough to know fixed step alone does not buy you cross-machine determinism.

10Timing sources

Measure frame time with a monotonic clock, never a wall clock.

A note on the windowing layer

In an SDL or GLFW program you own the while loop and the accumulator drops straight in. In Rust's winit the framework owns the loop and calls you back: keep the accumulator in your handler, set ControlFlow::Poll, do the frame work in the RedrawRequested event rather than in about_to_wait, and call request_redraw() every frame so the next RedrawRequested arrives[16]. Same algorithm, inverted control flow. The Platform & Window tutorial builds this out.

11Case studies

Unity
Fixed FixedUpdate plus rigidbody interpolation. Physics runs at Time.fixedDeltaTime, default 0.02 s (50 Hz)[7]. Rigidbody Interpolate blends the previous two physics updates and so renders one update behind; Extrapolate predicts ahead instead[8]. The spiral guard is Maximum Allowed Timestep[7].
Unreal
Variable tick with optional physics sub-stepping. The frame tick is variable. Sub-stepping is off by default; turned on in the project's Physics settings, it sub-divides each frame so no physics step exceeds Max Substep Delta Time, bounded by Max Substeps[9]. The sub-stepping is hidden from gameplay code.
Godot
Split processing callbacks. _physics_process runs at a fixed Physics FPS (default 60, so 16.667 ms) and _process runs at the variable frame rate[10]. Optional physics interpolation blends the current and previous transforms, explicitly rendering in the past[11].
Source
Fixed server tick. Source "simulates the game in discrete time steps called ticks," a default 15 ms step, so 66.67 ticks per second, decoupled from the client's frame rate[12].

12Pitfalls

Game speed tracks frame rateNo delta time, or motion not scaled by it. The naive loop.
Subtle stutter at a fixed stepRendering the raw tick state without interpolating by the leftover accumulator.
Simulation runs slowDiscarding the accumulator remainder instead of carrying it.
Freeze under loadNo frame-time clamp or step cap: the spiral of death. A cap that keeps the leftover time only delays it.
Objects trail or overshootInterpolation adds up to a tick of latency; extrapolation snaps on a bounce. Pick per system.
Negative or huge dtUsing a wall clock instead of a monotonic one; an NTP step or manual clock change.
Replays desync across machinesFixed step alone is not determinism; float and toolchain differences remain.

13What's next

The loop needs a window to run in and events to pump, which is the next module, Platform & Window, followed by Input. The integrator inside integrate() is the subject of the Physics tutorial, which uses the fixed step you just built for its substeps. And the determinism caveats trace back to IEEE-754 Floating Point. The full path is on the series hub.

  1. Jason Gregory. Game Engine Architecture, "The Game Loop and Real-Time Simulation." gameenginebook.com. The kinds of time an engine tracks and the structure of the loop. (Chapter number varies by edition; cited by title.)
  2. Robert Nystrom. "Game Loop," Game Programming Patterns. gameprogrammingpatterns.com. Frame-rate coupling, fixed-update/variable-render, the step-cap guard, float divergence, and an extrapolated render first described as "interpolated."
  3. Glenn Fiedler. "Fix Your Timestep!" gafferongames.com. The canonical accumulator algorithm, the 0.25 s clamp, the alpha blend, state interpolation, the semi-fixed loop, and the spiral of death.
  4. Glenn Fiedler. "Integration Basics." gafferongames.com. Explicit Euler vs semi-implicit Euler vs RK4; supports scoping large-step instability to the integrator.
  5. Koen Witters. "deWiTTERS Game Loop." dewitters.com. Constant-logic-rate loop with frame-skip and a forward-prediction render the article calls "interpolation."
  6. Unity. MonoBehaviour.FixedUpdate, Scripting API. docs.unity3d.com. FixedUpdate "may be called zero, one, or multiple times per frame depending on the frame rate and simulation needs."
  7. Unity. Fixed updates / Time settings. docs.unity3d.com. Default fixedDeltaTime of 0.02 s and the Maximum Allowed Timestep guard.
  8. Unity. Apply interpolation to a Rigidbody. docs.unity3d.com. Interpolation blends the previous two physics updates and renders slightly behind; extrapolation predicts ahead.
  9. Epic Games. Physics Sub-Stepping in Unreal Engine. dev.epicgames.com. Variable tick with Max Substep Delta Time and Max Substeps bounding the physics step.
  10. Godot. Idle and Physics Processing. docs.godotengine.org. Fixed-rate _physics_process (default 60 Hz) vs variable-rate _process.
  11. Godot. Physics interpolation introduction. docs.godotengine.org. Blending current and previous transforms, which places the drawn object "between 1 and 2 ticks into the past."
  12. Valve. Source Multiplayer Networking. developer.valvesoftware.com. "Discrete time steps called ticks," a 15 ms default step (66.67 ticks/sec) decoupled from frame rate.
  13. cppreference. std::chrono::steady_clock. en.cppreference.com. A monotonic clock with constant tick spacing, "most suitable for measuring intervals."
  14. Rust. std::time::Instant. doc.rust-lang.org. Monotonic now() and elapsed(); Duration::as_secs_f64 for seconds.
  15. Bruce Dawson. "Windows Timer Resolution: The Great Rule Change." randomascii.wordpress.com. The default ~15.6 ms timer granularity and timeBeginPeriod.
  16. winit. ApplicationHandler. rust-windowing.github.io. Render in response to WindowEvent::RedrawRequested, not about_to_wait; ControlFlow::Poll for games.

See also