Lesson 01
HTML5 Canvas game loop: update first, draw second.
The game loop is the heartbeat of a browser game. It reads input, moves objects, checks collisions, updates score and then redraws the scene. If this loop is messy, every later feature becomes harder to debug.
1. Start with a small, honest canvas
A canvas game needs only a canvas element and a 2D rendering context. Start with a known width and height. Later you can scale the canvas with CSS, but your internal game coordinates should stay predictable.
<canvas id="game" width="400" height="600"></canvas>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const W = canvas.width;
const H = canvas.height;
</script>
The important part is that game logic should use `W` and `H`, not hard-coded browser sizes. This keeps the same game playable in a desktop browser, an iframe or a phone viewport.
2. Separate update from draw
A common beginner mistake is to move objects while drawing them. That works for a single ball, but breaks down when you add scoring, enemies, menus, particles and input. A cleaner structure is: update all state, then draw the final state.
function update() {
player.x += player.vx;
player.y += player.vy;
checkCollisions();
updateScore();
}
function draw() {
ctx.clearRect(0, 0, W, H);
drawBackground();
drawPlayer();
drawUI();
}
function frame() {
update();
draw();
requestAnimationFrame(frame);
}
frame();
In Supagames, the small classics sometimes use a fixed timer because the mechanic is simple and predictable. The larger action games use `requestAnimationFrame`, because it fits the browser rendering pipeline and is friendlier for animation-heavy scenes.
3. Fixed interval example from Flying Bird
Flying Bird is intentionally simple. The game advances with a fixed interval, applies gravity, moves pipes and then redraws. This makes it easy to explain and tune.
const gravity = 0.5;
const jump = -8;
const pipeW = 50;
const pipeGap = 150;
const pipeSpeed = 3;
let bird = { x: 80, y: H / 2, vy: 0, r: 15 };
let pipes = [];
let score = 0;
let gameRunning = true;
let started = false;
let frameCount = 0;
Source pattern: Flying Bird.
The variables are compact, but each one has one job. `gravity` pulls the bird down. `jump` pushes it upward. `pipeSpeed` controls horizontal difficulty. `frameCount` is used for timed pipe spawning.
The full update loop is built from those values:
bird.vy += gravity;
bird.y += bird.vy;
if (bird.y + bird.r > H || bird.y - bird.r < 0) {
gameRunning = false;
draw();
return;
}
if (frameCount % 90 === 0) addPipe();
The lesson here is not that every game should use `setInterval`. The lesson is that the loop should make the game readable. When something goes wrong, you should know exactly whether the problem is gravity, input, spawning or collision.
4. requestAnimationFrame for smoother games
For bigger games, use the browser's animation clock. It pauses naturally when the tab is hidden and lines up with screen refresh better than a raw timer.
let lastTime = performance.now();
function gameLoop(now) {
const dt = Math.min(0.033, (now - lastTime) / 1000);
lastTime = now;
update(dt);
draw();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
`dt` means delta time. It tells your update code how much time passed since the previous frame. The `Math.min` clamp protects the game after a tab pause, so enemies do not teleport across the map when the browser resumes.
5. Add states before adding screens
Even a small canvas game usually has more than one state. There is a title state, a playing state, a paused state, a game-over state and sometimes a victory state. You do not need a framework for this. A single `mode` variable can keep the loop honest.
let mode = "title";
function update(dt) {
if (mode === "title") return;
if (mode === "paused") return;
if (mode === "playing") {
updatePlayer(dt);
updateEnemies(dt);
checkWinLose();
}
}
function draw() {
drawWorld();
if (mode === "title") drawTitleScreen();
if (mode === "paused") drawPauseText();
if (mode === "gameover") drawGameOver();
}
This pattern prevents a subtle bug: the game may look like it is paused because a menu is visible, but enemies continue updating behind it. If your update phase checks the mode first, menu screens are safer and easier to reason about.
For Supagames pages that include ratings or comments below the game, this also matters because the player may scroll, open a modal or type a comment after a run. The game loop should not keep consuming input or damaging the player while the user is clearly outside active play.
6. Debug checklist for game loops
- If the game instantly ends, log the player position and the first collision check before changing difficulty.
- If the screen scrolls when pressing Space, call `event.preventDefault()` for gameplay keys.
- If mobile feels laggy, use pointer events and set `touch-action: none` on the game surface.
- If animations move too fast on powerful monitors, use delta time or a fixed simulation step.
- If enemies freeze, verify that the loop still calls `update()` after menus, victory screens or comments open.
Rule of thumb: draw code should show what happened. Update code should decide what happens. Mixing both is how small games become mysterious.