Skip to main content
Back to module
Game Prototyping & Systems · Mechanics

Collision detection

Reading25mLesson 1 of 3 in this module

Detect overlaps between axis-aligned boxes and points, and find every object a player touches.

Why this matters

Almost every mechanic is a collision question in disguise. Did the bullet hit the enemy? Is the player standing on a platform? Did the mouse click a button? Is the coin inside the pickup radius? You need an answer that is correct, fast enough to run hundreds of times per frame, and simple enough to debug at 2 a.m. during a jam. For 2D prototypes the answer is usually the axis-aligned bounding box, or AABB.

What an AABB is

An AABB is a rectangle whose sides line up with the x and y axes (it is never rotated). Store it the same way you draw it:

const player = { x: 10, y: 20, w: 16, h: 24 };   // x, y = top-left corner

Its edges are:

  • left = x, right = x + w
  • top = y, bottom = y + h

Even when the art is a circle or a character with flowing hair, a box that roughly fits is usually good enough. Many games make the hitbox slightly smaller than the sprite so near-misses feel fair to the player.

The overlap test

Two boxes overlap when they overlap on both axes at once. Thinking about the opposite is easier: they are apart if one is completely to the left, right, above or below the other. Flip that and you get the classic test:

function aabbOverlap(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
}

Worked example. a = {x: 0, y: 0, w: 10, h: 10} and b = {x: 5, y: 5, w: 10, h: 10}:

  • 0 < 15, 10 > 5, 0 < 15, 10 > 5: all true, so they overlap (a 5×5 square is shared).

Move b to x: 10. Now a.x + a.w > b.x is 10 > 10, which is false. The boxes touch along an edge but do not overlap. Using strict < and > means touching is not a hit, which is what you want for a player standing exactly on top of a floor: they rest on it without "colliding into" it every frame. Whichever rule you pick, pick it deliberately and use it everywhere.

Point in rectangle

Clicks, cursor hover and small projectiles are points. A point is inside a box if it lies within both ranges:

function pointInRect(px, py, r) {
  return px >= r.x && px < r.x + r.w &&
         py >= r.y && py < r.y + r.h;
}

Here the left/top edge counts as inside and the right/bottom edge does not. That "half-open" convention means a point on the shared border of two touching tiles belongs to exactly one of them, never both or neither.

Checking many objects

A simple loop is fine for prototypes:

function findHits(player, others) {
  const hits = [];
  others.forEach((o, i) => {
    if (aabbOverlap(player, o)) hits.push(i);
  });
  return hits;
}

Returning indices (or the objects) and letting the caller decide what to do keeps the collision code pure: it answers a question and changes nothing. The game then decides that touching a coin adds score, touching spikes costs a life, and so on. Mixing detection with response makes bugs hard to find.

With a few hundred objects, checking every pair is still cheap. When you have thousands, games use a broad phase such as a uniform grid: only test objects in the same or neighbouring cells. Do not build that until profiling says you need it.

Tunnelling

Collision is checked at discrete moments. A fast bullet moving 40 pixels per frame can be on one side of a 10-pixel wall in one frame and on the other side in the next, never overlapping it. Fixes, roughly from simplest:

  • cap the speed or make thin walls thicker,
  • split the movement into smaller sub-steps,
  • use a swept test or a ray cast along the path.

This is also why the previous lesson capped delta time.

Beyond boxes

  • Circles: two circles overlap when the distance between centres is less than the sum of radii. Compare squared distances to avoid Math.sqrt.
  • Rotated shapes: use the Separating Axis Theorem (SAT), covered in the Nybbit video. Engines like Godot do this for you.

Common mistakes

  • Using <= in one place and < in another, so touching counts as a hit sometimes.
  • Treating x, y as the centre in one function and the corner in another.
  • Forgetting width/height and comparing only x values.
  • Changing state inside the detection function, which makes the order of checks matter.
  • Hitboxes equal to sprite bounds, which makes near-misses feel unfair.

Quick recap

  • An AABB is {x, y, w, h} with sides along the axes.
  • Boxes overlap when they overlap on both axes; strict comparisons mean touching is not a hit.
  • Points use a half-open range so shared borders belong to one box.
  • Keep detection pure; let the game decide the response.
  • Watch out for tunnelling with fast objects.

Try it

Open the Do tab to write aabbOverlap, pointInRect and findHits as pure functions.

Also in this lesson: