Game Development 19 min read

Mastering 2D Collision Detection: Vectors, AABB, OBB, and JavaScript Implementations

This article explains how to use simple shapes like circles and rectangles for fast 2D collision detection, reviews vector mathematics, provides a reusable Vector2d class with operations such as addition, subtraction, length, dot product and rotation, and shows concrete JavaScript code for circle‑circle, circle‑rectangle (both axis‑aligned and rotated) and rectangle‑rectangle collisions using AABB and OBB techniques.

WecTeam
WecTeam
WecTeam
Mastering 2D Collision Detection: Vectors, AABB, OBB, and JavaScript Implementations

Introduction

In 2D games, rectangles and circles are often used to replace complex shapes for intersection tests because they are the fastest to compute. A rectangle can be an Axis Aligned Bounding Box (AABB) when its sides are parallel to the axes, or an Oriented Bounding Box (OBB) when it is rotated. Different scenarios call for different solutions.

For example, a Pokémon sprite (a rectangular bounding box) and a Poké Ball (a bounding sphere) illustrate the choice of shape.

Vector

Vectors are a fundamental mathematical tool in collision detection; all subsequent calculations are performed using vectors, so we first review them.

Algebraic Representation of Vectors

A vector can be represented by its coordinates in a chosen coordinate system. For a free vector, moving its start point to the origin lets us describe it with a single point whose coordinates are the vector’s components.

<ol><li><code class="language-javascript">class Vector2d {
  constructor(vx = 1, vy = 1) {
    this.vx = vx;
    this.vy = vy;
  }
}

const vecA = new Vector2d(1, 2);
const vecB = new Vector2d(3, 1);
</code></li></ol>

Vector Operations

Addition follows the parallelogram rule: the resulting vector’s x and y components are the sums of the operands’ components.

<ol><li><code class="language-javascript">static add(vec, vec2) {
  const vx = vec.vx + vec2.vx;
  const vy = vec.vy + vec2.vy;
  return new Vector2d(vx, vy);
}
</code></li></ol>

Subtraction yields a vector from the end of the second operand to the end of the first.

<ol><li><code class="language-javascript">static sub(vec, vec2) {
  const vx = vec.vx - vec2.vx;
  const vy = vec.vy - vec2.vy;
  return new Vector2d(vx, vy);
}
</code></li></ol>

The length (magnitude) is the square root of the sum of squares of its components.

<ol><li><code class="language-javascript">length() {
  return Math.sqrt(this.vx * this.vx + this.vy * this.vy);
}
</code></li></ol>

The dot (scalar) product multiplies corresponding components and sums them.

<ol><li><code class="language-javascript">static dot(vec, vec2) {
  return vec.vx * vec2.vx + vec.vy * vec2.vy;
}
</code></li></ol>

Rotation uses a rotation matrix.

<ol><li><code class="language-javascript">static rotate(vec, angle) {
  const cosVal = Math.cos(angle);
  const sinVal = Math.sin(angle);
  const vx = vec.vx * cosVal - vec.vy * sinVal;
  const vy = vec.vx * sinVal + vec.vy * cosVal;
  return new Vector2d(vx, vy);
}
</code></li></ol>

Circle

A circle is defined by its center (x, y) and radius r.

<ol><li><code class="language-javascript">class Circle {
  constructor(x = 0, y = 0, r = 1) {
    this.x = x;
    this.y = y;
    this.r = r;
  }
  get P() {
    return new Vector2d(this.x, this.y); // center vector
  }
}
</code></li></ol>

Rectangle

A rectangle needs a center (x, y), width w, height h, and a rotation angle (degrees).

<ol><li><code class="language-javascript">export class Rect {
  constructor(x = 0, y = 0, w = 1, h = 1, rotation = 0) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.rotation = rotation;
  }
  get C() {
    return new Vector2d(this.x, this.y); // center vector
  }
  get A3() {
    return new Vector2d(this.x + this.w / 2, this.y + this.h / 2); // top‑right vertex
  }
  get _rotation() {
    return this.rotation / 180 * Math.PI; // convert to radians
  }
}
</code></li></ol>

Circle‑Circle Intersection

Two circles intersect if the distance between their centers is less than or equal to the sum of their radii.

<ol><li><code class="language-javascript">function circleCircleIntersect(circle1, circle2) {
  const P1 = circle1.P;
  const P2 = circle2.P;
  const r1 = circle1.r;
  const r2 = circle2.r;
  const u = Vector2d.sub(P1, P2);
  return u.length() <= r1 + r2;
}
</code></li></ol>

Circle‑Rectangle Intersection

First handle the axis‑aligned case (AABB). The shortest distance from the circle centre to the rectangle is computed by clamping the difference vector to the rectangle’s half‑sizes.

<ol><li><code class="language-javascript">function rectCircleIntersect(rect, circle) {
  const C = rect.C;
  const P = circle.P;
  const h = Vector2d.sub(P, C);
  const v = new Vector2d(
    Math.abs(h.vx),
    Math.abs(h.vy)
  );
  const u = new Vector2d(
    Math.max(v.vx - rect.w / 2, 0),
    Math.max(v.vy - rect.h / 2, 0)
  );
  return u.lengthSquared() <= circle.r * circle.r;
}
</code></li></ol>

If the rectangle is rotated, we rotate the circle centre in the opposite direction, turning the problem back into the axis‑aligned case.

<ol><li><code class="language-javascript">function p(rect, circle) {
  if (rect.rotation % 360 === 0) return circle.P;
  return Vector2d.add(
    rect.C,
    Vector2d.rotate(
      Vector2d.sub(circle.P, rect.C),
      -rect._rotation
    )
  );
}

function rectCircleIntersect(rect, circle) {
  const rotation = rect.rotation;
  const C = rect.C;
  const r = circle.r;
  const P = p(rect, circle);
  const h = Vector2d.sub(P, C);
  const v = new Vector2d(
    Math.abs(h.vx),
    Math.abs(h.vy)
  );
  const u = new Vector2d(
    Math.max(v.vx - rect.w / 2, 0),
    Math.max(v.vy - rect.h / 2, 0)
  );
  return u.lengthSquared() <= r * r;
}
</code></li></ol>

Rectangle‑Rectangle Intersection (AABB)

Two axis‑aligned rectangles can be reduced to a point‑vs‑rectangle test by expanding one rectangle by the half‑sizes of the other.

<ol><li><code class="language-javascript">function AABBrectRectIntersect(rect1, rect2) {
  const P = rect2.C;
  const w2 = rect2.w;
  const h2 = rect2.h;
  const { w, h, x, y } = rect1;
  const C = rect1.C;
  const A3 = new Vector2d(
    x + w / 2 + w2 / 2,
    y + h / 2 + h2 / 2
  ); // new rectangle half‑size
  const H = Vector2d.sub(A3, C);
  const v = new Vector2d(
    Math.abs(P.vx - C.vx),
    Math.abs(P.vy - C.vy)
  );
  const u = new Vector2d(
    Math.max(v.vx - H.vx, 0),
    Math.max(v.vy - H.vy, 0)
  );
  return u.lengthSquared() === 0; // point inside expanded rectangle
}
</code></li></ol>

Rectangle‑Rectangle Intersection (OBB – Separating Axis Theorem)

For oriented rectangles we use the Separating Axis Theorem (SAT). The four potential separating axes are the normals of each rectangle’s edges (two per rectangle). If a gap exists on any axis, the rectangles do not intersect.

<ol><li><code class="language-javascript">class Rect {
  // ... same as before, plus vertex getters
  get _A1() { return new Vector2d(this.x - this.w/2, this.y - this.h/2); }
  get _A2() { return new Vector2d(this.x + this.w/2, this.y - this.h/2); }
  get _A3() { return new Vector2d(this.x + this.w/2, this.y + this.h/2); }
  get _A4() { return new Vector2d(this.x - this.w/2, this.y + this.h/2); }
  get _axisX() { return new Vector2d(1, 0); }
  get _axisY() { return new Vector2d(0, 1); }
  get _rotation() { return this.rotation / 180 * Math.PI; }
  get A1() { return this.rotation % 360 === 0 ? this._A1 : Vector2d.add(this.C, Vector2d.rotate(this._CA1, this._rotation)); }
  // similarly A2, A3, A4 using rotated corner vectors
  get axisX() { return this.rotation % 360 === 0 ? this._axisX : Vector2d.rotate(this._axisX, this._rotation); }
  get axisY() { return this.rotation % 360 === 0 ? this._axisY : Vector2d.rotate(this._axisY, this._rotation); }
  get vertexs() { return [this.A1, this.A2, this.A3, this.A4]; }
}

function OBBrectRectIntersect(rect1, rect2) {
  const axes = [rect1.axisX, rect1.axisY, rect2.axisX, rect2.axisY];
  for (const axis of axes) {
    if (!cross(rect1, rect2, axis)) return false;
  }
  return true;
}

function cross(rect1, rect2, axis) {
  const proj1 = rect1.vertexs.map(v => Vector2d.dot(v, axis)).sort((a,b)=>a-b);
  const proj2 = rect2.vertexs.map(v => Vector2d.dot(v, axis)).sort((a,b)=>a-b);
  const rect1Min = proj1[0];
  const rect1Max = proj1[proj1.length-1];
  const rect2Min = proj2[0];
  const rect2Max = proj2[proj2.length-1];
  return rect1Max >= rect2Min && rect2Max >= rect1Min;
}
</code></li></ol>

Demos

Demo 1 (basic tests): https://rococolate.github.io/blog/gom/test1.html

Demo 2 (interactive drag‑and‑drop): https://rococolate.github.io/blog/gom/test2.html

References

Chapter 15: Collision Detection – http://blog.jmecn.net/chapter-15-collision-detection/

Fighting Game Essentials – http://daily.zhihu.com/story/4761397

How to determine overlap between a rectangle and a circle – https://www.zhihu.com/question/24251545

Common 2D Collision Detection – https://aotu.io/notes/2017/02/16/2d-collision-detection/index.html

OBB Collision Detection – https://www.cnblogs.com/iamzhanglei/archive/2012/06/07/2539751.html

Rotation matrix – https://en.wikipedia.org/wiki/Rotation_matrix

Dot product – https://zh.wikipedia.org/wiki/%E7%82%B9%E7%A7%AF

Vector – https://zh.wikipedia.org/wiki/%E5%90%91%E9%87%8F

JavaScriptcollision detectionvector math2D GameAABBOBB
WecTeam
Written by

WecTeam

WecTeam (维C团) is the front‑end technology team of JD.com’s Jingxi business unit, focusing on front‑end engineering, web performance optimization, mini‑program and app development, serverless, multi‑platform reuse, and visual building.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.