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);
camelCase.
Discussion
Loading…