Advanced

Collision Detection

Does this rectangle overlap that one? Learn the AABB test that drives every coin, hazard, and platform, the same four-line function the game ships.

What you will see

coin: true | pit: false

About 5 min of reading

Quick check

Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.

Try It

The collision lesson shows code turning into game mechanics. I run small snippets and watch the exact output, then copy the same pattern the finished game ships.

1

The AABB test checks two axis-aligned rectangles by comparing their edges: left of one is left of right of the other, and so on all four sides. I run the test against two rectangles that clearly overlap and two clearly apart so the four comparisons produce both true and false.

function overlaps(a, b) { 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 } console.log(overlaps({x:100,y:400,w:40,h:56}, {x:130,y:420,w:24,h:24})) console.log(overlaps({x:100,y:400,w:40,h:56}, {x:200,y:420,w:24,h:24}))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
true false
A rectangle at x=100..140 overlaps one at x=130..154 on all four edges, so it is true. One at x=200..224 does not, 140 is not > 200, so it is false. The test is four edge comparisons ANDed together; missing one makes holes in the collision box.
2

The test is sensitive to exact edges. Two rectangles that touch but do not cross report false, and one offset vertically by the same amount also reports false. I print the four edge comparisons side by side so it is clear which comparison flips the result.

function overlaps(a, b) { 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 } console.log(overlaps({x:100,y:400,w:40,h:56}, {x:140,y:420,w:24,h:24})) // edges touch console.log(overlaps({x:100,y:400,w:40,h:56}, {x:110,y:300,w:40,h:40})) // vertically apart

Before you run it, say out loud what it will print. Then open the answer.

What it prints
false false
When the right edge of a (140) exactly meets the left edge of b (140), strict < and > make it false, no overlap. A rectangle 100 px higher shares the x range but not the y range, so it is false too. Overlap requires ALL FOUR comparisons to be true.
3

The game walks every active object and runs the AABB test against the player. On a true it does the right thing: coins are taken, hazards cost a life. I loop a small world, run the test, and count how many coins are collected so the collision-driven score is real.

function overlaps(a, b) { 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 } let coins = [ {x:130,y:420,w:24,h:24,taken:false}, {x:500,y:420,w:24,h:24,taken:false} ] let score = 0 for (const c of coins) { if (overlaps({x:100,y:400,w:40,h:56}, c) && !c.taken) { c.taken = true; score++ } } console.log(score)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
1
Only the coin at x=130 overlaps the player at x=100, so exactly one is collected. The coins array is mutated in place (taken = true) so the same coin cannot score twice. The collision test drives the score, that is how pickups work.
4

The full collision pass loops obstacles too. A hazard that overlaps the player costs a life; a coin that overlaps grants points. I trace the decision tree: for each object test overlap, then branch on type so the collision handler reads like the game's.

function overlaps(a,b){return a.xb.x&&a.yb.y} let player={x:100,y:400,w:40,h:56} let score=0, lives=3 for (const o of [{type:'coin',x:130,w:24,y:420,h:24},{type:'spike',x:105,w:24,y:420,h:24}]) { if (overlaps(player, o)) { if (o.type==='coin') score++; else lives-- } } console.log('score='+score, 'lives='+lives)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
score=1 lives=2
The coin overlaps so score becomes 1; the spike overlaps so lives drop to 2. The same one-line AABB test routes to two outcomes by type. This four-line function runs against every object every frame, the core of all game logic.
5

Collision is how every interaction in a game is decided: coin pickup, hazard hit, platform landing. It is always the same four comparisons, wrapped in a loop. I print the decision table for one frame so the pattern 'loop -> test -> branch' is unmistakable.

function overlaps(a,b){return a.xb.x&&a.yb.y} let p={x:100,y:400,w:40,h:56} let results=[] for (const o of [{t:'coin',x:130,w:24,y:420,h:24},{t:'pit',x:500,w:40,y:420,h:40}]) { results.push(o.t + ': ' + overlaps(p, o)) } console.log(results.join(' | '))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
coin: true | pit: false
One frame tests every object against the player. The coin overlaps (true) so it scores; the pit 400 px away does not (false) so nothing happens. Loop, test, branch, that is the entire collision system, and it is the same four-line function everywhere.

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

Every frame the game compares the player's rectangle against each active object with the four-edge AABB test, then acts on overlap.
1 Build a Rect for the player and each object
↓
2 Run aabb(player, obj), four edge comparisons
↓
3 All four true means the rectangles overlap
↓
4 A coin overlap collects it; a hazard overlap ends the run
↓
5 Repeat for every object next frame
Why this works
Why both axes must overlap

Two rectangles can be at the same height but far apart, or stacked but far apart, either way they don't touch. The test catches "far away" in a single line because it requires overlap on both axes.

// Same height, separated horizontally → X fails
A = {x:100,y:100,w:40,h:56}, B = {x:500,y:110,w:20,h:20}
// A.x + A.w > B.x  →  140 > 500  → false

// Stacked vertically, separated → Y fails
A = {x:100,y:100,w:40,h:56}, B = {x:110,y:500,w:20,h:20}
// A.y + A.h > B.y  →  156 > 500  → false

This is the whole reason the game has no rotation: an axis-aligned rectangle has clean left/right/top/bottom edges, so overlap reduces to comparing four edges. Rotated objects would need real geometry, which a platformer never wants.

Fairness in the hitbox

Collision isn't just correct, it's fair. The game gives the player a smaller hurtRect for hazards than the full rect for coins. A sliding player's hurtbox shrinks even further, so a narrowly-grazing spike doesn't kill you.

// Coins use the full body;
// hazards use a smaller, more forgiving hurt box.
player.hurtRect = { x: x+6, y: y+8, w: w-12, h: h-16 };

This is the same spirit as the difficulty caps: the game is hard but winnable, and a death should be the player's fault, never the game's. We prove exactly this with tests in the Master tier.

A classic beginner mistake is to make the player's hitbox exactly their drawn body while the obstacle's is larger, that biases collisions against the player and feels unfair.

Examples

Everything is a rectangle

Before we can detect collisions we need a rectangle type. Every game object, player, coin, spike, ground segment, is drawn as a box, so we represent each as a rectangle:

class Rect {
  constructor(x, y, w, h) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
  }
}

x, y is the top-left corner (on a canvas y grows downward). w, h are width and height. The right edge is x + w; the bottom edge is y + h.

The whole question of collision comes down to one deceptively simple thing:

Do these two rectangles overlap?

The overlap test (AABB)

AABB stands for Axis-Aligned Bounding Box, a rectangle that isn't rotated. Our game never rotates objects, so this is perfect. Two rectangles overlap only if they overlap on both the X axis AND the Y axis at the same time.

function intersects(a, b) {
  return a.x < b.x + b.w &&          // A's left is left of B's right
         a.x + a.w > b.x &&          // A's right is right of B's left
         a.y < b.y + b.h &&          // A's top is above B's bottom
         a.y + a.h > b.y;            // A's bottom is below B's top
}

That is exactly the game's aabb() function from utils/math.js:

export function aabb(a, b) {
  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;
}

Each of the four comparisons is one side of one rectangle reaching toward the opposite side of the other. If they miss on either axis, they don't collide.

How the real game uses it

In PlayState.update(), the game checks collisions every frame against every active object:

// Coin collisions.
for (const c of this.spawn.coins) {
  if (c.active && !c.collected && collide(this.player, c)) {
    c.collect();
    this.score.addCoin();
  }
}

// Hazard collisions.
for (const h of this.spawn.hazards) {
  if (h.active && collide(this.player.hurtRect, h)) {
    this.die();   // touched a spike → game over
  }
}

collide(a, b) just calls aabb(a.rect, b.rect): the same test you wrote. Two tiny loops turn "did the player touch this?" into "collect a coin" or "game over."

Two subtle things to notice:

  • The player has two hitboxes. For coins it uses the full player.rect. For hazards it uses a smaller player.hurtRect (shrunk when sliding). Smaller hitbox = fairer, easier gameplay.
  • **!c.collected*: once a coin is grabbed it's removed from the check. Without it you'd score the same coin over and over.

Walk it by hand: with A = {x:100,y:100,w:40,h:56} and B = {x:120,y:130,w:22,h:22}, all four conditions are true → OVERLAP. Move B to x:500 and A.x + A.w > B.x becomes 140 > 500 → false → no collision.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Vectors and Physics
Previous Lesson
Vectors and Physics
Next Lesson
Procedural Generation
Procedural Generation →