Skip to main content
Back to module
Game Prototyping & Systems · The Game Loop in Code

The game loop

Reading53mLesson 2 of 3 in this module

Write an update/draw loop with requestAnimationFrame and delta time so movement is the same at any frame rate.

Why this matters

Every real-time game runs the same heartbeat dozens of times per second: read input, update the world, draw it. That heartbeat is the game loop. Get it wrong and your game runs twice as fast on a 120 Hz monitor, stutters when a laptop is busy, or teleports objects through walls after the player switches tabs. Get it right once and every prototype you build afterwards feels consistent.

requestAnimationFrame

In the browser, requestAnimationFrame(callback) asks the browser to call your function just before the next screen repaint. It passes a high-resolution timestamp in milliseconds. To keep looping, the callback requests the next frame itself:

function loop(timestamp) {
  tick(timestamp);                 // do one frame of work
  requestAnimationFrame(loop);     // schedule the next one
}
requestAnimationFrame(loop);

Why not setInterval(tick, 16)? requestAnimationFrame lines up with the display's refresh, pauses in background tabs (saving battery), and gives you an accurate timestamp. setInterval drifts and keeps running when nobody is watching.

The problem: frame rate varies

A naive update moves a fixed amount per frame:

ball.x += 2;   // 2 pixels per frame

At 60 frames per second that is 120 pixels per second. On a 144 Hz monitor it is 288. On a struggling laptop running at 30 fps it is 60. The same game now has three different speeds, which wrecks balance and makes difficulty impossible to tune.

The fix: delta time

Measure how much time passed since the last frame, called delta time or dt, and express speeds per second:

let lastTime = null;

function tick(timestamp) {
  if (lastTime === null) lastTime = timestamp;   // first frame: dt = 0
  let dt = (timestamp - lastTime) / 1000;         // ms -> seconds
  lastTime = timestamp;

  update(dt);
  draw();
}

function update(dt) {
  ball.x += ball.vx * dt;   // vx is in pixels per SECOND
}

With vx = 120, the ball moves 120 pixels every second whether that second contains 30, 60 or 144 frames. A quick self-test: two updates of 0.25 seconds must leave the ball exactly where one update of 0.5 seconds does.

Clamp dt after a pause

When the player switches tabs, the browser stops calling your loop. When they come back, the first dt might be 30 seconds. Multiply a velocity by 30 and your ball leaves the level, passing through every wall on the way. The standard defence is a cap:

const MAX_DT = 0.1;   // never simulate more than 100 ms in one step
dt = Math.min(dt, MAX_DT);

The game appears to "pause" during the gap instead of jumping forward. Many engines also use a fixed timestep (always step physics by exactly 1/60 s, running several steps if needed) for repeatable physics. For prototypes, variable dt with a cap is usually enough; the Clear Code video explains both.

Worked example: a bouncing ball

const ball = { x: 20, y: 90, size: 20, vx: 120 };

function update(dt) {
  ball.x += ball.vx * dt;
  if (ball.x + ball.size > canvas.width) {     // hit the right wall
    ball.x = canvas.width - ball.size;
    ball.vx = -Math.abs(ball.vx);
  } else if (ball.x < 0) {                     // hit the left wall
    ball.x = 0;
    ball.vx = Math.abs(ball.vx);
  }
}

Two details make this robust. First, the ball is pushed back inside the wall, so it can never get "stuck" flipping direction every frame. Second, Math.abs sets the direction explicitly instead of blindly negating, which is safe even if the ball is somehow already moving away.

Keep update and draw separate

update(dt) changes numbers. draw() reads numbers and paints. Do not move things inside draw, and do not draw inside update. This separation means you can:

  • run update many times without drawing (to fast-forward or test),
  • pause by skipping update while still drawing a pause screen,
  • test the rules by calling update(0.5) directly and checking the state.

The last point is exactly how professional teams write automated tests for game logic, and how the Do activity checks your work: it calls your functions with chosen values instead of waiting for real frames.

Common mistakes

  • Speeds per frame instead of per second. Anything that moves should be multiplied by dt.
  • Forgetting to divide by 1000. Timestamps are milliseconds; speeds per second need seconds.
  • No dt cap. A tab switch becomes a teleport.
  • Calling requestAnimationFrame(loop) from several places, which starts several loops and doubles the speed.
  • Starting with lastTime = 0. The first dt is then the whole time since page load.

Quick recap

  • The loop is read input, update, draw, repeated with requestAnimationFrame.
  • Delta time = seconds since the last frame; multiply velocities by it.
  • Cap dt (for example at 0.1 s) to survive pauses.
  • Keep update and draw separate so logic is testable.

Try it

Open the Do tab to write update(dt) and tick(timestamp) for a bouncing ball, including the delta-time cap.

Also in this lesson: