The Game Loop
How a picture comes alive
A cartoon is many pictures in a row. A game does the same: about 60 times a second it erases the canvas and draws everything in a new position. The repeating piece of code that does this is called the game loop.
requestAnimationFrame
Asking the browser to call a function before the next screen repaint is requestAnimationFrame:
function frame() {
// 1. change the positions of objects
// 2. draw the frame
requestAnimationFrame(frame); // and ask for the next frame
}
requestAnimationFrame(frame);
Why not setInterval(frame, 16)? requestAnimationFrame adapts to the screen's refresh rate
(60, 120, 144 Hz), doesn't draw for nothing when the tab is hidden, and the frames come out smooth, without
stutter. It's the standard for animation in the browser.
You must not make a game loop with while (true) { ... }: such a loop never gives control back to the
browser, the page will freeze and nothing will be drawn.
Frame time: dt
Frames don't come evenly: on a fast computer there are 144 per second, on a slow one — 30. If you move
the ball "2 pixels per frame", it will fly at different speeds on different computers. That's why speed
is given in pixels per second, and each frame the object is moved by speed × dt, where dt is how many
seconds have passed since the previous frame.
requestAnimationFrame passes the current time in milliseconds to the function:
let last = performance.now();
function frame(now) {
const dt = (now - last) / 1000; // milliseconds → seconds
last = now;
ball.x += ball.vx * dt; // vx — pixels per second
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
If the ball flies at 120 px/s, in a frame of 1/60 of a second it moves 2 pixels, and in a frame of 1/30 — 4 pixels. Over a second, in both cases — exactly 120.
When the tab is minimized, frames stop, and on return dt turns out huge (minutes!)
— objects "teleport" through walls. That's why dt is usually limited: Math.min(dt, 0.1).
State, update and draw
Everything that describes the game at a given moment — positions, velocities, score, lives — is kept in state objects. A frame is split into two parts:
const state = {
ball: { x: 50, y: 50, vx: 150, vy: 90, r: 10 },
score: 0,
};
function update(dt) { // only changes numbers
state.ball.x += state.ball.vx * dt;
state.ball.y += state.ball.vy * dt;
}
function draw() { // only draws what's in the state
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(state.ball.x, state.ball.y, state.ball.r, 0, Math.PI * 2);
ctx.fill();
ctx.fillText(`Score: ${state.score}`, 10, 20);
}
let last = performance.now();
function frame(now) {
const dt = Math.min(0.1, (now - last) / 1000);
last = now;
update(dt);
draw();
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
Why split it? update can be tested without a screen — just call it with numbers and look at the
result (that's how the course tasks work). And draw doesn't think about the game rules, it only shows the
state. It's the same idea as "data → render" in the DOM.
Acceleration and gravity
Velocity can also change over time. Gravity is a constant downward acceleration: every second the falling speed grows by the same amount.
const GRAVITY = 300; // px/s per second
ball.vy += GRAVITY * dt; // first change the velocity
ball.y += ball.vy * dt; // then move with the new velocity
This is how jumps, falls and the beautiful parabolas of firework sparks are made.
Objects appear and disappear
Bullets, sparks and enemies live briefly. They're kept in an array: new ones are added with push, and unneeded ones
are removed by keeping only the living ones:
bullets = bullets.filter((b) => b.y > 0); // those that flew off screen — remove
Summary
- The game loop:
requestAnimationFrame(frame)at the end of every frame; nowhile (true). - Speed — in pixels per second, the shift per frame —
speed × dt,dtin seconds and no more than 0.1. - The game state lives in objects;
update(dt)changes numbers,draw()draws. - Gravity:
vy += g × dt, theny += vy × dt. Temporary objects — in an array +filter.