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 and along . The step from point to point is the vector
The dot product of two vectors and multiplies them coordinate by coordinate and adds the results:
It’s also equal to , where 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 has length , is how far reaches along : the length of the shadow casts onto .
// 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;
}// 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;
}# 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.ysubtract- time O(1) space O(1)
dot- time O(1) space O(1)
Projecting a point onto a segment
Take a segment from to , its direction , and a point . The point of the whole line through and that’s closest to is , where
The top of the fraction is how far reaches along , and dividing by measures that in lengths of the segment. So says where along the line the closest point is: at , at , and in between for points between them. Beyond the ends, drops below or rises above , so for the segment, clamp into , just like we clamped the circle’s centre into the rectangle. Then the closest point sits at an end whenever is past it.
Drag past either end of the segment and watch leave : the hollow circle is where the projection lands on the whole line, and is where clamping puts it back on the segment.
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 };
}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 };
}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 of the segment, it’s a point-in-circle test once more: the circle touches the segment when .
function circleSegmentCollide(c: Circle, a: Point, b: Point): boolean {
return pointInCircle(closestPointOnSegment(c, a, b), c);
}function circleSegmentCollide(c, a, b) {
return pointInCircle(closestPointOnSegment(c, a, b), c);
}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 , and if the two results have different signs, or one of them is , the segment crosses or touches the line. Multiplying them makes that one comparison: the product is negative or zero exactly then.
function lineSegmentCollide(line: Line, a: Point, b: Point): boolean {
return side(a, line) * side(b, line) <= 0;
}function lineSegmentCollide(line, a, b) {
return side(a, line) * side(b, line) <= 0;
}def line_segment_collide(line: Line, a: Point, b: Point) -> bool:
return side(a, line) * side(b, line) <= 0lineSegmentCollide- time O(1) space O(1)
Segment vs segment
Two segments and cross when and are on opposite sides of the line through and , and and are on opposite sides of the line through and . One of the two isn’t enough: and can straddle the line through far past the end of , 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 , and the question becomes whether the two segments overlap along that line. That’s the interval test from the rectangles chapter, on and on .
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;
}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;
}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) <= 0segmentsIntersect- 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 , the side value against changes evenly from at to at , so it reaches at , and the crossing point is .
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.