WIP
This commit is contained in:
@@ -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?
|
||||
|
||||
Reference in New Issue
Block a user