ehm
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# Step 12 — The overlap trap (when the sweep has no answer)
|
||||
|
||||
You found this one yourself, in the capstone: start a box **inside** another
|
||||
collider, press a key, and the position turns into `NaN`. That's not a typo in
|
||||
your code from steps 01–10 — the kernel is *correct* and still does this. It's a
|
||||
**blind spot in the whole approach**, and every real engine has to patch it.
|
||||
|
||||
> This is a genuine hole in the finished kernel, shared by the capstone's
|
||||
> `game.js`. It is a *third* thing, separate from the two bugs you're hunting in
|
||||
> `engine/system/physics.ts` — no spoilers here.
|
||||
|
||||
## Why the sweep can't see it
|
||||
|
||||
Everything since step 05 answers one question: *"when, during this frame, will I
|
||||
**enter** the box?"* The whole ladder quietly assumes the answer lies in the
|
||||
future — that you start the frame **outside**.
|
||||
|
||||
Start inside, and "when will I enter?" has no sane answer. The math doesn't
|
||||
refuse — it cheerfully reports that you entered *in the past*. Remember step 05:
|
||||
what sign does `entry` have when `p` is already between `min` and `max`? Every
|
||||
function above `sweepInterval` trusts that number without checking it.
|
||||
|
||||
## The autopsy (do this before reading on)
|
||||
|
||||
The `NaN` isn't born where you see it — it's the end of a chain, and each link
|
||||
is one line you wrote. Reproduce it in a scratch file and trace it:
|
||||
|
||||
```ts
|
||||
import { moveAndSlide, sweptAABB } from "./given.ts";
|
||||
|
||||
const box = { x: 10, y: 10, w: 2, h: 2 };
|
||||
const wall = { x: 0, y: 0, w: 32, h: 32 };
|
||||
console.log(sweptAABB(box, { x: 5, y: 0 }, wall));
|
||||
console.log(moveAndSlide(box, { x: 5, y: 0 }, [wall]));
|
||||
```
|
||||
|
||||
Work through these **in order, predicting each answer before checking** (add
|
||||
`console.log`s inside your step-10 loop — it's your code, instrument it):
|
||||
|
||||
1. What `time` does `sweptAABB` report? Now flip the velocity so the box moves
|
||||
*away* from the wall — why do you *still* get a hit? (This is why you can't
|
||||
even walk out of a wall you're stuck in.)
|
||||
2. Follow that `time` into the `else` branch of `moveAndSlide`. Three lines use
|
||||
it. Which line is saved by the `Math.max(0, …)`? What happens to `vel` when
|
||||
you `slide` against that normal? And what does `timeLeft = timeLeft * (1 - time)`
|
||||
do when `time` is negative — shrink, or *grow*?
|
||||
3. Next iteration: `vel` is now `(0, 0)` but the loop keeps going. What does
|
||||
`sweepInterval` return for `v = 0` while inside the interval (look at the
|
||||
first branch — you wrote it in step 05)? So what is `entry` now, and what
|
||||
does `timeLeft` become after multiplying by `(1 - entry)`?
|
||||
4. Last link. In JavaScript, what is `0 * Infinity`? That's `scale(vel, timeLeft)`
|
||||
on iteration three. And once one `NaN` exists, every comparison against it is
|
||||
`false` — so which branch of the loop does the poisoned move fall into, and
|
||||
what does `pos = add(pos, move)` do then?
|
||||
|
||||
Four links: **overlap → a hit in the past → dead velocity + growing time debt →
|
||||
`0 × ∞`**. When you can retell that chain from memory, you own it.
|
||||
|
||||
## The fix: measure the overlap, push out
|
||||
|
||||
The sweep is *continuous* detection — it prevents overlap but can't recover from
|
||||
it. So real engines pair it with a *discrete* partner: if you're already inside,
|
||||
don't ask "when do I enter?" — ask **"how deep am I, and what's the shortest way
|
||||
out?"**, then teleport that far and *only then* sweep.
|
||||
|
||||
That shortest-way-out is the **penetration vector** (the famous *minimum
|
||||
translation vector*). For two overlapping AABBs there are exactly four escapes —
|
||||
push `a` left, right, up, or down until the boxes just separate:
|
||||
|
||||
```
|
||||
outLeft = (a.x + a.w) - b.x // slide a left this far -> separated
|
||||
outRight = (b.x + b.w) - a.x // slide a right this far -> separated
|
||||
outUp = (a.y + a.h) - b.y
|
||||
outDown = (b.y + b.h) - a.y
|
||||
```
|
||||
|
||||
Two things to convince yourself of (don't skip — the tests check both):
|
||||
|
||||
- The boxes strictly overlap **iff all four distances are positive**. (What is
|
||||
`outLeft` when `a` sits fully to the right of `b`? When they merely touch?)
|
||||
- The answer is the **smallest** of the four, as a vector, with the sign that
|
||||
moves `a` *away*. Smallest, because depenetration is a teleport the player can
|
||||
see — one pixel of pop beats being flung across the room. Note this handles
|
||||
`a` fully *swallowed* by `b` too, where "the overlap of the intervals" would
|
||||
lie to you — one of the tests is exactly that case.
|
||||
|
||||
## Task
|
||||
|
||||
Two functions in `overlap.ts`:
|
||||
|
||||
1. `penetrationVector(a, b)` — the smallest single-axis push that separates the
|
||||
boxes, or `null` if they don't strictly overlap.
|
||||
2. `safeMoveAndSlide(box, v, walls)` — check every wall; if the box is inside
|
||||
one, apply the push **plus an `EPSILON` of slack in the push direction**
|
||||
(same idea as the backoff in the loop: land *flush* on the wall and next
|
||||
frame's sweep starts half-trapped again). Then run the given `moveAndSlide`
|
||||
from the safe position.
|
||||
|
||||
```sh
|
||||
bun test workshop/steps/12-overlap
|
||||
```
|
||||
|
||||
## Make the capstone unbreakable (optional)
|
||||
|
||||
Port both functions into `11-capstone/game.js`, swap the `moveAndSlide` call for
|
||||
`safeMoveAndSlide`, and set the player's spawn inside the pillar. It should pop
|
||||
out and play on like nothing happened. Then the question you actually care
|
||||
about: does your *real* engine survive the same experiment?
|
||||
Reference in New Issue
Block a user