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

The Game Loop

The game loop is the heartbeat: input, update, render. The choice of fixed vs variable timestep decides whether your game survives slow hardware.

Game dev — the game loop

EXAMPLE
// ===== Naive loop (frame-locked) =====
function naive() {
  let running = true;
  while (running) {
    handleInput();
    update(/* dt */ 1 / 60);   // assume 60 Hz; lies on slow hardware
    render();
  }
}

// ===== Variable timestep (use the real dt) =====
let lastTime = performance.now();
function variableFrame(now) {
  const dt = (now - lastTime) / 1000;
  lastTime = now;
  handleInput();
  update(dt);                // physics scales with real dt
  render();
  requestAnimationFrame(variableFrame);
}
// Pros: easy. Cons: fast tunnelling on big dt, non-deterministic.

// ===== Fixed timestep (the recommended baseline) =====
// Decouples physics from render. Deterministic; stable on slow machines.
const STEP = 1 / 60;
const MAX_FRAME = 0.25;        // clamp big stalls (tab returns, GC pauses)
let accumulator = 0;
let lastFrame = performance.now();

function fixedFrame(now) {
  const dt = Math.min(MAX_FRAME, (now - lastFrame) / 1000);
  lastFrame = now;
  accumulator += dt;
  while (accumulator >= STEP) {
    handleInput();
    update(STEP);              // physics stepped at fixed rate
    accumulator -= STEP;
  }
  const alpha = accumulator / STEP;
  render(alpha);               // interpolate rendering between physics steps
  requestAnimationFrame(fixedFrame);
}
requestAnimationFrame(fixedFrame);

// ===== Interpolating render =====
// Each physics body keeps its previous + current state. The renderer blends:
function renderBody(b, alpha) {
  const x = b.prevX + (b.x - b.prevX) * alpha;
  const y = b.prevY + (b.y - b.prevY) * alpha;
  drawSprite(b.sprite, x, y);
}

// On each physics step, snapshot prev -> current:
function update(dt) {
  for (const b of bodies) {
    b.prevX = b.x; b.prevY = b.y;
    b.vx += b.ax * dt; b.vy += b.ay * dt;
    b.x  += b.vx * dt; b.y  += b.vy * dt;
  }
}

// ===== When fixed timestep matters =====
// - Physics-driven games: deterministic for multiplayer + replays
// - Engines: Unity uses FixedUpdate at 50 Hz by default
// - Anything where input timing matters (fighting games, rhythm)

// ===== When variable timestep is fine =====
// - Story-driven, point-and-click, mostly tweened animation
// - Tools and editors

// ===== Pause + freeze patterns =====
let isPaused = false;
function fixedFramePaused(now) {
  const dt = (now - lastFrame) / 1000;
  lastFrame = now;
  if (!isPaused) {
    accumulator += Math.min(MAX_FRAME, dt);
    while (accumulator >= STEP) {
      handleInput();
      update(STEP);
      accumulator -= STEP;
    }
    render(accumulator / STEP);
  } else {
    render(accumulator / STEP); // still draw so menus animate
  }
  requestAnimationFrame(fixedFramePaused);
}

// ===== Order matters =====
// Within a step:
//   1. Read input (snapshot device state)
//   2. AI / behaviour
//   3. Physics integration
//   4. Collision detection + response
//   5. Game-event resolution (deaths, scoring)
//   6. After the physics loop: render with interpolation

// ===== Patterns to internalise =====
// - Fixed timestep + interpolated render = stable, smooth, deterministic
// - Clamp dt with a MAX_FRAME after big stalls
// - Snapshot previous state before each physics step for interpolation
// - Separate input + physics + render code; their cadences differ
// - Profile both the inner physics loop and the render pass independently

// ===== Pitfalls =====
// - Multiplying movement by dt INSIDE a fixed update -> double-applied dt; movement explodes
// - Forgetting clamp -> after a 5s stall, accumulator runs the simulation forward 300 steps
// - Mixing input polling in render -> input latency dependent on FPS
// - Physics that uses Math.random() per step on multiple clients -> diverges without a seeded RNG
// - Animation tied to FPS rather than time -> looks fast on 144Hz, slow on 60Hz

Why it matters

The game loop decides whether your physics are correct and your game is fair on the user laptop. Fixed timestep + interpolated rendering + clamped dt is the bedrock. Order the steps deliberately (input, update, render) and the same skeleton ports from Canvas to Unity to Godot.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Update fixed-step, render variable-step is the gold standard:
let accumulator = 0;
const step = 1/60;
function frame(dt) {
    accumulator += dt;
    while (accumulator >= step) { update(step); accumulator -= step; }
    render(accumulator / step);   // interpolate for smoothness
}
Try it Yourself »

Exercise

Standard browser scheduler for a game loop.

(frame);

Discussion

Loading…