Skip to content

Systems · Simulation · core

Fixed Timestep

physics ticks at a steady rate, rendering just blends between ticks

Run physics in constant dt slices from an accumulator, clamp long frames, and interpolate between the last two states for smooth, repeatable motion.

View .md
Interactive visual

Two balls bounce from the same height. The orange one integrates with jittery frame times, so its bounces drift off the dashed line. The cyan one runs fixed physics steps and keeps hitting the line. The faint ball behind the cyan one is the raw physics state: turn interpolation off to see the cyan ball snap between ticks, and lower physics Hz to exaggerate it.

What it is

Integrating physics with the raw frame delta makes the simulation depend on frame rate. Jumps reach different heights at 30 and 144 fps, springs explode after a hitch, and replays drift. A fixed timestep always advances the simulation by the same dt (for example 1/60 s) no matter how long the frame took.

Each frame adds the real elapsed time to an accumulator, then runs as many fixed steps as fit, keeping the remainder. Clamp the frame time first (e.g. to 0.25 s). Without that clamp a slow frame queues many steps, those steps make the next frame slow too, and the game falls into the spiral of death.

The leftover time means rendering is up to one step behind. Keep the previous and current physics states and draw lerp(prev, curr, alpha) with alpha = accumulator / dt. Motion then looks smooth even when the physics rate is lower than the display's, or doesn't divide evenly into it (60 Hz physics on a 144 Hz monitor).

Key formulas

Accumulator
Render interpolation

For rotations, slerp quaternions with the same α.

Rules of thumb

  • Common rates: 50 or 60 Hz for gameplay physics, 120–240 Hz for stiff springs, vehicles and ragdolls.
  • Clamp frame time to 0.1–0.25 s and/or cap steps per frame at about 5–8.
  • Read input once per frame, but apply forces inside each fixed step.
  • Don't interpolate teleports. Copy curr into prev when you snap an object so it doesn't streak across the map.
  • Cameras that follow an interpolated object must read the interpolated position, or they jitter against it.

Implementation

Fixed update with interpolation:
const STEP = 1 / 60
let acc = 0
let last = performance.now() / 1000

function frame(nowMs: number) {
  const now = nowMs / 1000
  acc += Math.min(now - last, 0.25)   // avoid the spiral of death
  last = now
  while (acc >= STEP) {
    prev.copy(curr)                   // keep the state we step from
    physicsStep(curr, STEP)
    acc -= STEP
  }
  const alpha = acc / STEP
  mesh.position.lerpVectors(prev.position, curr.position, alpha)
  mesh.quaternion.slerpQuaternions(prev.rotation, curr.rotation, alpha)
  renderer.render(scene, camera)
  requestAnimationFrame(frame)
}

Common mistakes

  • Clamping dt and calling it a fixed step: the simulation still depends on frame rate whenever frames are faster than the clamp.
  • No upper bound on frame time, so after a tab switch or breakpoint the game runs thousands of steps and freezes.
  • Rendering the current state without interpolation: at 30 Hz physics on a 60 Hz screen, motion stutters in steps.
  • Applying a one-off impulse (jump) once per frame instead of in the next fixed step, so it fires zero or two times.
  • Assuming fixed steps alone make a game deterministic. Iteration order, float math across platforms and unseeded randomness also matter.

Prompt

Implement a fixed-timestep game loop in TypeScript for a Three.js app. A `FixedStepper` class takes a step (default 1/60) and a max frame time (0.25 s); `advance(frameTime, step)` accumulates the clamped time, calls step(dt) as many times as fits and returns the count; an `alpha` getter returns accumulator/dt. Each physics body stores prev and curr position + quaternion; before stepping copy curr→prev; render with lerpVectors/slerpQuaternions using alpha; provide `teleport()` that sets both. Write Vitest tests with power-of-two steps: 30 fps rendering at 60 Hz physics gives 2 steps per frame, step totals over 1 s don't depend on frame pattern, a 5 s frame is clamped, and alpha equals the leftover fraction.

Further reading

Updated 2026-11-01