Let's Build a Game Engine in HTML
Chapter 6
6 / 9

Collision Detection

Axis-aligned boxes that push each other apart

Chapter goal

Detect AABB overlaps and resolve the player against solid platforms so they stand and slide instead of falling through.

AABB
overlap tests
penetration resolution
grounded flag

How to do it

  1. 1

    Define boxes

    An AABB is {x, y, w, h}. Two boxes overlap if they overlap on both X and Y axes.

  2. 2

    Resolve the shallow axis

    When overlapping, compute penetration on X and Y; push out along the smaller penetration so corners feel right.

  3. 3

    Mark grounded

    If you resolve upward out of a platform, set grounded=true so the next chapter can allow jumps.

Live lab

Solution code updates with sliders. Tinker is your sandbox. Play runs whichever document you launch.

Live settings

Every slider injects into the engine constants for this chapter.

Type-along practice
Photo layout
Collision Detection
1 / 80 lines

The chapter engine script is prewritten below. Type it line by line — each line explains why it belongs there. Matching a line advances automatically; Enter re-checks if you are stuck.

Prewritten engine scriptmistakes 0
1(function () {
2 "use strict";
3 // Single boot — if this document is re-parsed, abandon prior work.
4 if (window.__engineBooted) return;
5 window.__engineBooted = true;
6 let alive = true;
7 window.addEventListener("pagehide", function () { alive = false; }, { once: true });
8 const raf = window.requestAnimationFrame.bind(window);
9 window.requestAnimationFrame = function (cb) {
10 return raf(function (ts) {
11 if (!alive) return;
12 try { cb(ts); } catch (err) { console.error(err); alive = false; }
13 });
14 };
15
16const canvas = document.getElementById('c');
17const ctx = canvas.getContext('2d');
18const W = canvas.width, H = canvas.height;
19const hud = document.getElementById('hud');
20
21const SPEED = 180;
22const PLAYER_W = 26;
23const PLAYER_H = 34;
24const GAP = 70;
25
26const keys = Object.create(null);
27addEventListener('keydown', e => { keys[e.code]=true; if(e.code.startsWith('Arrow')||e.code==='Space') e.preventDefault(); });
28addEventListener('keyup', e => { keys[e.code]=false; });
29
30const player = { x: 60, y: 40, w: PLAYER_W, h: PLAYER_H, vx: 0, vy: 0, grounded: false };
31const solids = [
32 { x: 0, y: H - 24, w: W, h: 24 },
33 { x: 80, y: H - 24 - GAP, w: 100, h: 14 },
34 { x: 240, y: H - 24 - GAP * 1.4, w: 90, h: 14 },
35 { x: 400, y: H - 24 - GAP * 0.9, w: 130, h: 14 },
36 { x: 300, y: H - 24 - GAP * 2.1, w: 80, h: 14 },
37 { x: 520, y: 80, w: 16, h: 160 },
38];
39
40function aabb(a, b) {
41 return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
42}
43
44function resolve(p, s) {
45 const overlapX = Math.min(p.x + p.w - s.x, s.x + s.w - p.x);
46 const overlapY = Math.min(p.y + p.h - s.y, s.y + s.h - p.y);
47 if (overlapX < overlapY) {
48 if (p.x + p.w/2 < s.x + s.w/2) p.x -= overlapX; else p.x += overlapX;
49 p.vx = 0;
50 } else {
51 if (p.y + p.h/2 < s.y + s.h/2) { p.y -= overlapY; p.vy = 0; p.grounded = true; }
52 else { p.y += overlapY; p.vy = 0; }
53 }
54}
55
56let last = performance.now();
57function loop(now) {
58 const dt = Math.min((now - last) / 1000, 0.05);
59 last = now;
60
61 let ax = 0;
62 if (keys['ArrowLeft']||keys['KeyA']) ax -= 1;
63 if (keys['ArrowRight']||keys['KeyD']) ax += 1;
64 player.vx = ax * SPEED;
65 // light gravity so you fall onto platforms
66 player.vy += 900 * dt;
67 if (player.vy > 600) player.vy = 600;
68
69 player.grounded = false;
70 player.x += player.vx * dt;
71 for (const s of solids) if (aabb(player, s)) resolve(player, s);
72 player.y += player.vy * dt;
73 for (const s of solids) if (aabb(player, s)) resolve(player, s);
74
75 if (player.y > H + 40) { player.x = 60; player.y = 40; player.vx = 0; player.vy = 0; }
76
77 ctx.fillStyle = '#0b1018';
78 ctx.fillRect(0, 0, W, H);
79
80 ctx.fillStyle = '#1a2535';
81 for (const s of solids) {
82 ctx.fillRect(s.x, s.y, s.w, s.h);
83 ctx.strokeStyle = 'rgba(94,234,212,0.2)';
84 ctx.strokeRect(s.x + 0.5, s.y + 0.5, s.w - 1, s.h - 1);
85 }
86
87 ctx.fillStyle = player.grounded ? '#5eead4' : '#7dd3fc';
88 ctx.fillRect(player.x, player.y, player.w, player.h);
89
90 hud.textContent = 'Chapter 6 · grounded=' + player.grounded + ' · solids=' + solids.length;
91 requestAnimationFrame(loop);
92}
93requestAnimationFrame(loop);
94
95})();
Your lineline 1
(function () {

Arrows/WASD — walk on platforms; collision keeps you solid. · Sliders update the play window after a short pause.

Chapter solution
Reference
Live-bound HTML
Play window
Live