← Collision detection for beginners

[Chapter 7 · Part I · 2D]

Line segments

Vectors and the dot product, projecting a point onto a segment, circle vs segment, line vs segment and segment vs segment, the tests behind walls, platforms and polygon edges.

A wall in a game isn’t an endless line. It starts somewhere and ends somewhere: it’s a line segment. To test against one, we need one new tool, the dot product.

Vectors and the dot product

A vector is a step: how far to move along xx and along yy. The step from point AA to point BB is the vector

AB=BA=(xBxA, yByA)\overrightarrow{AB} = B - A = (x_{B} - x_{A},\ y_{B} - y_{A})

The dot product of two vectors u\mathbf{u} and v\mathbf{v} multiplies them coordinate by coordinate and adds the results:

uv=uxvx+uyvy\mathbf{u} \cdot \mathbf{v} = u_{x}v_{x} + u_{y}v_{y}

It’s also equal to uvcosθ|\mathbf{u}|\,|\mathbf{v}| \cos\theta, where θ\theta is the angle between the two vectors. That tells us what it measures:

  • It’s positive when the vectors point roughly the same way, zero when they’re perpendicular, and negative when they point roughly opposite ways.
  • When v\mathbf{v} has length 11, uv\mathbf{u} \cdot \mathbf{v} is how far u\mathbf{u} reaches along v\mathbf{v}: the length of the shadow u\mathbf{u} casts onto v\mathbf{v}.
TypeScript
// A vector is stored like a point: { x, y } is the step, not a position.
function subtract(a: Point, b: Point): Point {
	return { x: a.x - b.x, y: a.y - b.y };
}

function dot(u: Point, v: Point): number {
	return u.x * v.x + u.y * v.y;
}
JavaScript
// A vector is stored like a point: { x, y } is the step, not a position.
function subtract(a, b) {
	return { x: a.x - b.x, y: a.y - b.y };
}

function dot(u, v) {
	return u.x * v.x + u.y * v.y;
}
Python
# A vector is stored like a point: (x, y) is the step, not a position.
def subtract(a: Point, b: Point) -> Point:
    return Point(a.x - b.x, a.y - b.y)


def dot(u: Point, v: Point) -> float:
    return u.x * v.x + u.y * v.y
subtract
time O(1) space O(1)
dot
time O(1) space O(1)

Projecting a point onto a segment

Take a segment from AA to BB, its direction d=BA\mathbf{d} = B - A, and a point PP. The point of the whole line through AA and BB that’s closest to PP is A+tdA + t\,\mathbf{d}, where

t=(PA)dddt = \frac{(P - A) \cdot \mathbf{d}}{\mathbf{d} \cdot \mathbf{d}}

The top of the fraction is how far PAP - A reaches along d\mathbf{d}, and dividing by dd\mathbf{d} \cdot \mathbf{d} measures that in lengths of the segment. So tt says where along the line the closest point is: 00 at AA, 11 at BB, and in between for points between them. Beyond the ends, tt drops below 00 or rises above 11, so for the segment, clamp tt into [0,1][0, 1], just like we clamped the circle’s centre into the rectangle. Then the closest point sits at an end whenever PP is past it.

Drag PP past either end of the segment and watch tt leave [0,1][0, 1]: the hollow circle is where the projection lands on the whole line, and QQ is where clamping puts it back on the segment.

0½1t = 1.21QABP
Drag A, B or P; one square is one unit. Past either end, t leaves [0, 1] and gets clamped. d = B − A = (10, −4) · t = (P − A)·d / (d·d) = 140 / 116 = 1.21 → clamped to 1 · Q = (13, 3)
TypeScript
function closestPointOnSegment(p: Point, a: Point, b: Point): Point {
	const d = subtract(b, a);
	const lengthSquared = dot(d, d);
	if (lengthSquared === 0) return a; // A and B are the same point
	const t = clamp(dot(subtract(p, a), d) / lengthSquared, 0, 1);
	return { x: a.x + t * d.x, y: a.y + t * d.y };
}
JavaScript
function closestPointOnSegment(p, a, b) {
	const d = subtract(b, a);
	const lengthSquared = dot(d, d);
	if (lengthSquared === 0) return a; // A and B are the same point
	const t = clamp(dot(subtract(p, a), d) / lengthSquared, 0, 1);
	return { x: a.x + t * d.x, y: a.y + t * d.y };
}
Python
def closest_point_on_segment(p: Point, a: Point, b: Point) -> Point:
    d = subtract(b, a)
    length_squared = dot(d, d)
    if length_squared == 0:  # A and B are the same point
        return a
    t = clamp(dot(subtract(p, a), d) / length_squared, 0, 1)
    return Point(a.x + t * d.x, a.y + t * d.y)
closestPointOnSegment
time O(1) space O(1)

Circle vs line segment

With the closest point QQ of the segment, it’s a point-in-circle test once more: the circle touches the segment when CQr|CQ| \le r.

QABC
Drag A, B or the circle. t = 1.26 → clamped to 1 · |CQ| = 144 > r = 45 → apart
TypeScript
function circleSegmentCollide(c: Circle, a: Point, b: Point): boolean {
	return pointInCircle(closestPointOnSegment(c, a, b), c);
}
JavaScript
function circleSegmentCollide(c, a, b) {
	return pointInCircle(closestPointOnSegment(c, a, b), c);
}
Python
def circle_segment_collide(c: Circle, a: Point, b: Point) -> bool:
    return point_in_circle(closest_point_on_segment(c, a, b), c)
circleSegmentCollide
time O(1) space O(1)

This is the test for a ball against a wall, a platform or a paddle, however they’re rotated. The set of all points within some distance of a segment even has a name of its own, a capsule, and it’s a popular shape for game characters for exactly that reason: testing a circle against a capsule is this same test, with the two radii added together.

Line vs line segment

An endless line and a segment touch when the segment’s ends aren’t both on the same side of the line. That’s the side value from the lines chapter: put both ends into ax+by+cax + by + c, and if the two results have different signs, or one of them is 00, the segment crosses or touches the line. Multiplying them makes that one comparison: the product is negative or zero exactly then.

TypeScript
function lineSegmentCollide(line: Line, a: Point, b: Point): boolean {
	return side(a, line) * side(b, line) <= 0;
}
JavaScript
function lineSegmentCollide(line, a, b) {
	return side(a, line) * side(b, line) <= 0;
}
Python
def line_segment_collide(line: Line, a: Point, b: Point) -> bool:
    return side(a, line) * side(b, line) <= 0
lineSegmentCollide
time O(1) space O(1)

Segment vs segment

Two segments ABAB and CDCD cross when CC and DD are on opposite sides of the line through AA and BB, and AA and BB are on opposite sides of the line through CC and DD. One of the two isn’t enough: CC and DD can straddle the line through ABAB far past the end of ABAB, and the second check is what catches that.

There’s one special case. When all four ends lie on one line, every side value is 00, and the question becomes whether the two segments overlap along that line. That’s the interval test from the rectangles chapter, on xx and on yy.

ABCD
Drag any of the four ends; one square is one unit. The numbers are each end's ax + by + c against the other segment's line. C, D against AB: 30, −96 (opposite sides) · A, B against CD: −96, 30 (opposite sides) → they cross at (9.62, 3.43)
TypeScript
function segmentsIntersect(a: Point, b: Point, c: Point, d: Point): boolean {
	const ab = lineThrough(a, b);
	const sc = side(c, ab);
	const sd = side(d, ab);
	if (sc === 0 && sd === 0) {
		// All four ends on one line: they meet only if they overlap along it.
		return (
			overlap(Math.min(a.x, b.x), Math.max(a.x, b.x), Math.min(c.x, d.x), Math.max(c.x, d.x)) &&
			overlap(Math.min(a.y, b.y), Math.max(a.y, b.y), Math.min(c.y, d.y), Math.max(c.y, d.y))
		);
	}
	const cd = lineThrough(c, d);
	return sc * sd <= 0 && side(a, cd) * side(b, cd) <= 0;
}
JavaScript
function segmentsIntersect(a, b, c, d) {
	const ab = lineThrough(a, b);
	const sc = side(c, ab);
	const sd = side(d, ab);
	if (sc === 0 && sd === 0) {
		// All four ends on one line: they meet only if they overlap along it.
		return (
			overlap(Math.min(a.x, b.x), Math.max(a.x, b.x), Math.min(c.x, d.x), Math.max(c.x, d.x)) &&
			overlap(Math.min(a.y, b.y), Math.max(a.y, b.y), Math.min(c.y, d.y), Math.max(c.y, d.y))
		);
	}
	const cd = lineThrough(c, d);
	return sc * sd <= 0 && side(a, cd) * side(b, cd) <= 0;
}
Python
def segments_intersect(a: Point, b: Point, c: Point, d: Point) -> bool:
    ab = line_through(a, b)
    sc = side(c, ab)
    sd = side(d, ab)
    if sc == 0 and sd == 0:
        # All four ends on one line: they meet only if they overlap along it.
        return overlap(
            min(a.x, b.x), max(a.x, b.x), min(c.x, d.x), max(c.x, d.x)
        ) and overlap(min(a.y, b.y), max(a.y, b.y), min(c.y, d.y), max(c.y, d.y))
    cd = line_through(c, d)
    return sc * sd <= 0 and side(a, cd) * side(b, cd) <= 0
segmentsIntersect
time O(1) space O(1)

If you also need where they cross, for the point a bullet hits a wall, say, the side values give that away too. Along ABAB, the side value against CDCD changes evenly from sAs_{A} at AA to sBs_{B} at BB, so it reaches 00 at t=sAsAsBt = \frac{s_{A}}{s_{A} - s_{B}}, and the crossing point is A+t(BA)A + t\,(B - A).

These last two tests are exactly what the next two chapters need: a polygon’s edges are segments.

Comments

No comments yet. Questions and corrections are welcome.

Plain text, line breaks kept. Your IP address is stored only as a one-way hash, to limit spam.