iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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 * ;

Discussion

Loading…