This commit is contained in:
Schluffe
2026-09-04 13:57:52 +02:00
parent 71cfa64793
commit e961887953
24 changed files with 340 additions and 360 deletions
+22 -21
View File
@@ -2,21 +2,21 @@
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
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
> `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
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:
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.
@@ -38,33 +38,34 @@ 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
_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*?
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?
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,
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.
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 —
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:
```
@@ -79,9 +80,9 @@ 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
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
`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
@@ -92,7 +93,7 @@ Two functions in `overlap.ts`:
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
(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.
@@ -105,4 +106,4 @@ bun test workshop/steps/12-overlap
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?
about: does your _real_ engine survive the same experiment?