Delta Time
Frame-rate independence means motion, physics, and animation use elapsed wall time rather than per-frame counts. Without it, a 144 Hz monitor plays the game three times faster than a 30 Hz monitor — with it, the same gameplay lands across every device.
Delta time, fixed steps, accumulators
EXAMPLE
// 1) The bug — per-frame movement
let x = 0;
function update() {
x += 5; // 5 pixels every frame
requestAnimationFrame(update);
}
update();
// On a 60 Hz monitor: 300 px/sec.
// On a 144 Hz monitor: 720 px/sec. Same code, very different game.
// 2) The fix — multiply by elapsed seconds (delta time)
let last = performance.now();
let x2 = 0;
const SPEED = 300; // px / SEC
function update(now) {
const dt = (now - last) / 1000; // seconds
last = now;
x2 += SPEED * dt;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
// Same speed regardless of refresh rate.
// 3) Clamp the delta to avoid spirals of death after lag spikes
function update3(now) {
let dt = (now - last) / 1000;
last = now;
if (dt > 0.05) dt = 0.05; // cap to 50 ms — gameplay slows briefly instead of teleporting
/* … */
}
// 4) Fixed timestep for physics (Glenn Fiedler pattern)
// Variable dt → integrator instability, especially for collisions and joints.
// Run physics on a FIXED step (e.g. 60 Hz), render at whatever rate the screen wants.
const STEP = 1 / 60; // seconds
let accumulator = 0;
let prevState = { x: 0, vx: 0 };
let currState = { x: 0, vx: 0 };
function physicsTick(state, dt) {
state.x += state.vx * dt;
state.vx += 50 * dt; // some acceleration
}
function loop(now) {
const frameDelta = Math.min(0.1, (now - last) / 1000);
last = now;
accumulator += frameDelta;
while (accumulator >= STEP) {
prevState = { ...currState };
physicsTick(currState, STEP);
accumulator -= STEP;
}
// Interpolate between the two states for smooth rendering on monitors that don't sync to physics
const alpha = accumulator / STEP;
const x = prevState.x * (1 - alpha) + currState.x * alpha;
render(x);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
// 5) Unity — built-in delta time
// MonoBehaviour
void Update()
{
transform.position += Vector3.right * speed * Time.deltaTime; // movement
}
void FixedUpdate()
{
rigidbody.AddForce(Vector3.up * jumpForce); // physics — uses Time.fixedDeltaTime
}
// Use Update for input + camera, FixedUpdate for Rigidbody / physics.
// Time.deltaTime in FixedUpdate equals Time.fixedDeltaTime.
// 6) Godot — process / physics_process
# GDScript
func _process(delta: float) -> void:
position += Vector2.RIGHT * speed * delta
func _physics_process(delta: float) -> void:
velocity += gravity * delta
move_and_slide()
# delta is in seconds; _physics_process runs at a fixed rate (60 Hz by default).
// 7) Phaser — preupdate / update
update(time, delta) {
// delta is in milliseconds in Phaser
const dt = delta / 1000;
this.player.x += this.player.speed * dt;
}
// 8) Frame-rate independent acceleration / deceleration
// Naive: v += a * dt — this works for constant acceleration, but smoothing values
// like 'lerp current toward target by 10% per frame' break with variable dt.
//
// Fixed: convert per-frame smoothing into a per-second rate.
// target = lerp(current, target, 1 - exp(-decay * dt))
// decay = ln(1 / (1 - perFrameRate)) * targetFps
const DECAY = 5; // higher = faster catch-up
current += (target - current) * (1 - Math.exp(-DECAY * dt));
// 9) Use a high-resolution clock
performance.now(); // browser — sub-millisecond
Date.now(); // millisecond — lossy for 144 Hz timing
requestAnimationFrame((ts) => ts); // callback gets a high-res timestamp
// 10) Pause / slow-mo
let timeScale = 1.0;
function loop(now) {
const real = (now - last) / 1000;
last = now;
const dt = real * timeScale; // 0 = paused, 2 = 2x speed
update(dt);
render();
requestAnimationFrame(loop);
}
// timeScale = 0 in pause menus, timeScale = 0.3 for slow-mo cutscenes.
// 11) Frame-time budget — keep your loop under 16.7 ms
const frameStart = performance.now();
/* update + render */
const frameMs = performance.now() - frameStart;
if (frameMs > 16) console.warn('long frame', frameMs.toFixed(1));
// 12) Debug overlays
function drawDebug(dt, ctx) {
ctx.fillStyle = 'white';
ctx.fillText(`fps: ${(1 / dt).toFixed(0)}`, 8, 16);
ctx.fillText(`dt: ${(dt * 1000).toFixed(1)} ms`, 8, 32);
}
// 13) Determinism
// Variable dt is non-deterministic by design — replays, lockstep multiplayer, and physics
// regression tests all need a fixed timestep. Render-side smoothing remains variable.
// 14) Common bugs
// • Hard-coded per-frame movement (x += 5) — runs differently on every monitor
// • Delta multiplied where it shouldn't be (Time.deltaTime in FixedUpdate is fixedDeltaTime,
// not the variable Update one)
// • dt of 0 on the first frame — pass through cleanly; never divide by it
// • Accumulating spike — long pause + uncapped accumulator → 200 physics steps in one frame
// • Mixing input polling at variable rate with physics at fixed rate — buffer input across steps
// • Floating-point drift in 'add SPEED * dt every frame' over hours — periodically resync to canonical state
Why it matters
Multiply every movement by dt, run physics on a fixed step with an accumulator, and clamp huge deltas so a lag spike slows the game instead of teleporting it. The result is the same gameplay on a phone’s 30 Hz screen and a 240 Hz desktop — without it, frame-rate is a hidden gameplay variable.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Always multiply by dt for framerate-independent motion: player.x += speed * dt;Try it Yourself »
Exercise
Framerate-independent motion.
player.x += speed *
;
Two letters.
Discussion
Loading…