time for a snapshot
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# Step 07 — Swept AABB (the Minkowski trick)
|
||||
|
||||
Step 06 handled a moving **point** vs a box. But in a real game the thing that
|
||||
moves is a **box** (the player), not a point. This step turns "moving box vs box"
|
||||
into "moving point vs box" so you can reuse step 06 *unchanged*. That conversion
|
||||
is the single cleverest idea in the whole engine.
|
||||
|
||||
## The problem
|
||||
|
||||
Box A (the player) sits at corner `(a.x, a.y)` with size `a.w × a.h`, and moves by
|
||||
`v` this frame. Box B (a wall) is static. When do they touch?
|
||||
|
||||
It's fiddly because *both* shapes have size. You'd have to track four edges of A
|
||||
against four edges of B. Ugh.
|
||||
|
||||
## The trick: grow B, shrink A to a point
|
||||
|
||||
Watch what "just touching" means on the x-axis. A spans `[a.x, a.x + a.w]`, B
|
||||
spans `[b.x, b.x + b.w]`. They overlap when:
|
||||
|
||||
```
|
||||
a.x < b.x + b.w AND b.x < a.x + a.w
|
||||
```
|
||||
|
||||
Rearrange the second one (`b.x - a.w < a.x`) and you get a statement purely about
|
||||
**`a.x`**, the corner of A:
|
||||
|
||||
```
|
||||
b.x - a.w < a.x < b.x + b.w
|
||||
```
|
||||
|
||||
Read that: A's *corner* `a.x` behaves exactly like a **point** sliding inside a
|
||||
**wider interval** — one that starts `a.w` earlier and is `a.w` longer than B.
|
||||
The same happens on y with `a.h`.
|
||||
|
||||
So: **dump all of A's size onto B, and A collapses to just its corner point.**
|
||||
|
||||
```
|
||||
inflated = {
|
||||
x: b.x - a.w, // push the left edge out by A's width
|
||||
y: b.y - a.h, // push the top edge out by A's height
|
||||
w: b.w + a.w, // grow width by A's width
|
||||
h: b.h + a.h, // grow height by A's height
|
||||
}
|
||||
point = { x: a.x, y: a.y } // A is now just its corner
|
||||
```
|
||||
|
||||
This grown box is the **Minkowski sum** of B with A. And "does this point, moving
|
||||
by `v`, hit `inflated`?" is *exactly* `rayVsAABB` from step 06. You're done in
|
||||
three lines.
|
||||
|
||||
> Sanity picture: player box 2 wide with its right edge at x=2, wall left edge at
|
||||
> x=5 → real gap is 3. Inflate: `inflated.x = 5 - 2 = 3`, and the player's corner
|
||||
> sits at x=0, so the corner-to-inflated-edge gap is also 3. Same answer, simpler
|
||||
> shape. The inflation *bakes A's size into the wall* so the corner can pretend to
|
||||
> be a point.
|
||||
|
||||
This is the heart of your real engine's `sweptAABB` — the `inflAABB` it builds is
|
||||
this very inflated box, and `(ax, ay)` is this corner point.
|
||||
|
||||
## Task
|
||||
|
||||
Implement `sweptAABB(a, v, b)` in `swept.ts`: build the inflated box, then call
|
||||
the provided `rayVsAABB` (finished, in `given.ts`). Return its `Hit | null`.
|
||||
|
||||
```sh
|
||||
bun test workshop/steps/07-swept-aabb
|
||||
```
|
||||
@@ -0,0 +1,54 @@
|
||||
// Finished in earlier steps — reuse, don't rewrite.
|
||||
export type Vec = { x: number; y: number };
|
||||
export const vec = (x: number, y: number): Vec => ({ x, y });
|
||||
|
||||
export type AABB = { x: number; y: number; w: number; h: number };
|
||||
export type Span = { entry: number; exit: number };
|
||||
export type Hit = { time: number; normal: Vec };
|
||||
|
||||
/** Step 05, finished. */
|
||||
export function sweepInterval(
|
||||
p: number,
|
||||
v: number,
|
||||
min: number,
|
||||
max: number,
|
||||
): Span | null {
|
||||
if (v === 0) {
|
||||
return min <= p && p <= max
|
||||
? { entry: -Infinity, exit: Infinity }
|
||||
: null;
|
||||
}
|
||||
|
||||
let t1 = (min - p) / v;
|
||||
let t2 = (max - p) / v;
|
||||
if (t1 > t2) {
|
||||
[t1, t2] = [t2, t1];
|
||||
}
|
||||
|
||||
return { entry: t1, exit: t2 };
|
||||
}
|
||||
|
||||
/** Step 06, finished. A moving point vs a static box. */
|
||||
export function rayVsAABB(p: Vec, v: Vec, box: AABB): Hit | null {
|
||||
const spanX = sweepInterval(p.x, v.x, box.x, box.x + box.w);
|
||||
const spanY = sweepInterval(p.y, v.y, box.y, box.y + box.h);
|
||||
if (spanX === null || spanY === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entry = Math.max(spanX.entry, spanY.entry);
|
||||
const exit = Math.min(spanX.exit, spanY.exit);
|
||||
if (entry > exit) {
|
||||
return null;
|
||||
}
|
||||
if (entry >= 1 || exit <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normal =
|
||||
spanX.entry > spanY.entry
|
||||
? { x: v.x > 0 ? -1 : 1, y: 0 }
|
||||
: { x: 0, y: v.y > 0 ? -1 : 1 };
|
||||
|
||||
return { time: entry, normal };
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { type AABB, vec } from "./given.ts";
|
||||
import { sweptAABB } from "./swept.ts";
|
||||
|
||||
test("box moving right into a wall -> hits at the real gap", () => {
|
||||
// A right edge at x=2, B left edge at x=5 -> gap 3, speed 10 -> t=0.3
|
||||
const a: AABB = { x: 0, y: 0, w: 2, h: 2 };
|
||||
const b: AABB = { x: 5, y: 0, w: 2, h: 2 };
|
||||
const hit = sweptAABB(a, vec(10, 0), b)!;
|
||||
expect(hit.time).toBeCloseTo(0.3);
|
||||
expect(hit.normal).toEqual({ x: -1, y: 0 });
|
||||
});
|
||||
|
||||
test("box passes below the wall -> null", () => {
|
||||
const a: AABB = { x: 0, y: 10, w: 2, h: 2 };
|
||||
const b: AABB = { x: 5, y: 0, w: 2, h: 2 };
|
||||
expect(sweptAABB(a, vec(10, 0), b)).toBeNull();
|
||||
});
|
||||
|
||||
test("box moving left into a wall -> normal points right", () => {
|
||||
// A left edge at x=10, B right edge at x=2 -> gap 8, speed 10 -> t=0.8
|
||||
const a: AABB = { x: 10, y: 0, w: 2, h: 2 };
|
||||
const b: AABB = { x: 0, y: 0, w: 2, h: 2 };
|
||||
const hit = sweptAABB(a, vec(-10, 0), b)!;
|
||||
expect(hit.time).toBeCloseTo(0.8);
|
||||
expect(hit.normal).toEqual({ x: 1, y: 0 });
|
||||
});
|
||||
|
||||
test("diagonal where y blocks first -> horizontal normal", () => {
|
||||
const a: AABB = { x: 0, y: 0, w: 2, h: 2 };
|
||||
const b: AABB = { x: 5, y: 8, w: 2, h: 2 };
|
||||
const hit = sweptAABB(a, vec(10, 10), b)!;
|
||||
expect(hit.time).toBeCloseTo(0.6);
|
||||
expect(hit.normal).toEqual({ x: 0, y: -1 });
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { type AABB, type Hit, rayVsAABB, type Vec } from "./given.ts";
|
||||
|
||||
/**
|
||||
* A moving box `a` (velocity `v` over this frame) vs a static box `b`.
|
||||
*
|
||||
* Inflate `b` by `a`'s size (Minkowski sum), collapse `a` to its corner point,
|
||||
* then reuse rayVsAABB. Return its Hit, or null for no collision this frame.
|
||||
*/
|
||||
export function sweptAABB(a: AABB, v: Vec, b: AABB): Hit | null {
|
||||
const inflated: AABB = {
|
||||
x: b.x - a.w,
|
||||
y: b.y - a.h,
|
||||
w: b.w + a.w,
|
||||
h: b.h + a.h,
|
||||
};
|
||||
|
||||
const point: Vec = { x: a.x, y: a.y };
|
||||
|
||||
return rayVsAABB(point, v, inflated);
|
||||
}
|
||||
Reference in New Issue
Block a user