Building a Fixed-Timestep Game Loop in HTML Canvas
How the DN framework handles timing — the fixed-timestep update/render pattern, delta time, and why it matters for consistent game feel across devices.
Building a Fixed-Timestep Game Loop
Written alongside LoopShoots — the first Arcade game built on the DN framework.
Every game needs a heartbeat. In browser games, that’s the game loop — a function that runs on every frame and handles two jobs: update (move things, check collisions, run logic) and render (draw the current state to the canvas).
Getting this wrong causes stuttery movement, physics that run faster on high-refresh monitors, and input lag. The fixed-timestep pattern solves all of this.
The Problem with requestAnimationFrame Alone
requestAnimationFrame fires roughly 60 times per second, but not exactly.
On a 144hz monitor it fires more often. Under CPU load it fires less often.
If your update step just runs once per frame, your game literally runs
faster on better hardware.
The Fixed-Timestep Solution
Instead of “update once per frame,” you track how much real time has passed and update in fixed-size chunks:
class GameLoop {
constructor({ fps = 60, updateCallback, renderCallback } = {}) {
this.dt = 1 / fps; // target seconds per update (1/60 ≈ 0.0167s)
this.accTime = 0; // accumulated unprocessed time
this.lastTime = undefined;
this.updateCB = updateCallback;
this.renderCB = renderCallback;
}
processFrame = (time) => {
if (this.lastTime === undefined) this.lastTime = time;
// Accumulate elapsed time (convert ms to seconds)
this.accTime += (time - this.lastTime) / 1000;
this.lastTime = time;
// Run update in fixed steps until we've caught up
let didUpdate = false;
while (this.accTime >= this.dt) {
this.updateCB?.(this.dt); // pass dt so physics is framerate-independent
this.accTime -= this.dt;
didUpdate = true;
}
// Only render if something changed
if (didUpdate) this.renderCB?.();
requestAnimationFrame(this.processFrame);
}
startLoop() {
requestAnimationFrame(this.processFrame);
}
}
Using It
const loop = new GameLoop({
fps: 60,
updateCallback: (dt) => {
// dt is always 1/60 — use it for movement
player.x += player.speed * dt;
},
renderCallback: () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// draw everything
},
});
loop.startLoop();
Why dt in the Update Callback?
player.speed * dt means “move at speed pixels per second.” With dt = 1/60,
that’s speed / 60 pixels per frame — consistent regardless of actual frame rate.
What’s Next
This pattern is the foundation of every game in the DN framework. The next tutorial covers the SceneManager — how to switch between menu, game, and game-over states without spaghetti code.
This tutorial is in progress. Check back when LoopShoots ships.