Guide

Collision detection in games explained

When a player jumps onto a platform, a bullet grazes an enemy, or a dice chip lands in a payout zone, the game must answer one question fast: do these shapes overlap? That is collision detection — geometric tests that run every frame (or every physics step) to decide what touched what. Get it wrong and characters fall through floors; get it slow and frame rates collapse. This guide walks through the two-phase pipeline most engines use, the classic tests (AABB, circles, SAT), spatial acceleration structures, continuous collision for fast movers, layer filtering, and how to keep collision work inside your frame budget.

Collision vs collision response

Detection answers whether overlap occurred. Collision response decides what happens next: bounce, slide along a wall, take damage, trigger a sound, or resolve a provably-fair outcome. Keep them separate. Detection should be pure geometry — no side effects — so you can unit-test shapes in isolation and replay the same inputs deterministically. Response belongs in gameplay systems that consume contact manifolds (normal vectors, penetration depth, contact points).

Many 2D browser games start with axis-aligned bounding boxes (AABBs) for detection and only add fancier shapes when gameplay demands it. That is a sound default: AABB tests are a handful of comparisons and cache-friendly.

Broad phase and narrow phase

Naively testing every object against every other object is O(n²). With 500 entities that is 124,750 pair checks per frame — unacceptable on a phone. Engines split work into two stages:

  1. Broad phase — cheaply reject pairs that cannot possibly touch. Output a short list of candidate pairs.
  2. Narrow phase — run precise tests only on candidates. Return contact data if shapes overlap.

The broad phase might use a uniform grid (spatial hash), quadtree, or sorted sweep along an axis. The narrow phase runs AABB-vs-AABB, circle-vs-circle, or polygon SAT only on pairs the broad phase flagged. A well-tuned broad phase can cut narrow-phase work by 90% or more in crowded scenes.

Static geometry (tilemaps, level meshes) often gets its own broad-phase structure built once at load time. Dynamic actors (players, projectiles) insert and remove themselves each frame as they move between grid cells.

Axis-aligned bounding boxes (AABB)

An AABB is the smallest rectangle aligned to the world axes that fully contains a sprite or mesh. Store minX, minY, maxX, maxY (or center plus half-extents). Two AABBs overlap when they overlap on both axes:

function aabbOverlap(a, b) {
  return a.minX <= b.maxX && a.maxX >= b.minX
      && a.minY <= b.maxY && a.maxY >= b.minY;
}

AABBs are fast and stable, but they are conservative: a rotated sword sprite wrapped in an AABB has empty corners that register false positives. Mitigations include tighter oriented bounding boxes (OBB), multiple smaller AABBs per object, or switching to circles/SAT for combat-critical hits.

For tile-based platformers, test the player AABB against only the tiles its bounding box touches (typically 4–9 cells) instead of the whole map. That is broad phase at tile granularity — constant work per character regardless of world size.

Circle-circle collision

Circles are ideal for top-down games, soft hitboxes, and projectiles. Two circles with centers (x1, y1) and (x2, y2) and radii r1, r2 overlap when the distance between centers is less than r1 + r2. Compare squared distances to avoid sqrt:

const dx = x2 - x1, dy = y2 - y1;
const rSum = r1 + r2;
if (dx * dx + dy * dy <= rSum * rSum) {
  // overlap — compute penetration for response
}

Circle-vs-AABB is also common: find the closest point on the rectangle to the circle center, then treat that point as a zero-radius circle. These hybrids give tighter fits than a single large AABB around a round character.

Separating axis theorem (SAT) for polygons

When you need rotated rectangles, triangles, or arbitrary convex polygons, the separating axis theorem is the workhorse. If you can find any axis onto which the projections of two convex shapes do not overlap, the shapes do not collide. For 2D polygons, test each edge normal of both shapes; if all axes show overlapping intervals, the polygons intersect.

SAT is more expensive than AABB or circle tests — typically reserve it for narrow-phase pairs already filtered by broad phase, and only for gameplay- critical geometry. Concave shapes are usually decomposed into convex pieces or approximated with compound AABBs. Physics libraries (Box2D, Matter.js, Rapier) implement SAT and contact generation internally; understanding the idea helps you debug ghost collisions and choose collider complexity.

Spatial hashing and quadtrees

A uniform spatial grid divides the world into fixed-size cells (e.g. 64×64 pixels). Each frame, hash each object into every cell its AABB touches. Collision candidates are objects sharing a cell — not the whole world. Cell size should be roughly the diameter of a typical actor; too small and objects span many cells; too large and cells hold too many entries.

Quadtrees recursively subdivide space where object density is high. They adapt to uneven distributions (many entities in one corner, empty elsewhere) but carry pointer-chasing overhead. For browser canvas games with hundreds of similar-sized sprites, spatial hashing is often simpler and faster than a quadtree. Either structure pairs naturally with the broad-phase / narrow-phase split described above.

Continuous collision detection (CCD)

Discrete tests sample positions once per frame. A thin wall and a fast bullet can tunnel through between samples — the bullet was on one side at t and the other at t + dt with no overlap detected in between. Fixes include:

  • Swept tests — cast the moving shape along its velocity segment and find the earliest time of impact (common for ray-vs-AABB in shooters).
  • Sub-stepping — run multiple physics steps per frame so displacement per step stays smaller than the thinnest collider (pairs well with fixed timestep loops).
  • Thicker colliders — pragmatic level-design fix for very fast objects when full CCD is overkill.

Casino and skill games with moving chips or balls often combine swept circle tests with deterministic fixed steps so outcomes match across devices — the same principle provably-fair dice uses when separating simulation from animation.

Layers, masks, and triggers

Not every overlap should block movement. Engines assign each collider a layer (what it is) and a mask (what it collides with). Example layers: Player, Enemy, Projectile, Pickup, Static. A pickup might overlap the player layer for collection but ignore enemy projectiles.

Trigger volumes detect overlap without physical response — checkpoint zones, damage auras, payout regions. Implement as sensors that fire events on enter/stay/exit rather than resolving impulses. Bitmask filters before any narrow-phase math keep trigger checks cheap.

Performance on web and mobile

  • Profile pair counts — log broad-phase output size; if it grows quadratically with entity count, your grid cell size or tree depth is wrong.
  • Avoid allocations in hot loops — reuse pair buffers and contact arrays; GC pauses show up as collision stutter (see Core Web Vitals for main-thread impact).
  • Compile hot paths to WASM when JavaScript becomes the bottleneck — spatial hashing and SAT inner loops are good candidates for WebAssembly modules.
  • Sleep inactive bodies — objects far from the action skip simulation until something wakes them (standard in Box2D-style engines).
  • Match collider complexity to gameplay — a background prop does not need a 16-vertex polygon; one AABB is enough.
  • Determinism for networked or verifiable games — use fixed-point or carefully ordered floating operations so collision results match across clients.

Common pitfalls

  • Updating AABBs after detection — stale bounds from last frame cause missed hits; sync bounds immediately when position changes.
  • Mixing world and local space — rotated OBB/SAT math in local space must transform consistently; a classic source of one-frame glitches.
  • One giant collider for animation frames — swap or resize hitboxes per animation state instead of wrapping the whole sprite sheet.
  • Ignoring resting contact — objects stacked on slopes need slight penetration tolerance or jitter appears; physics engines use slop and Baumgarte stabilization.
  • Testing static pairs every frame — cache static-static results or exclude them at broad phase.

Key takeaways

  • Split work into broad phase (cheap rejection) and narrow phase (precise tests) to avoid O(n²) pair explosions.
  • Start with AABB and circle tests; add SAT only where rotated polygons matter.
  • Use spatial hashing or quadtrees so collision cost scales with local density, not global entity count.
  • Fast movers need continuous collision (swept tests or sub-steps) to prevent tunneling.
  • Layer masks and trigger volumes keep unrelated overlaps out of the hot path.

Related reading