80 lines
3.2 KiB
Markdown
80 lines
3.2 KiB
Markdown
# Step 10 — moveAndSlide (the whole engine, in one loop)
|
|
|
|
This is it. Every function you've written since step 01 gets tied together here
|
|
into the exact loop your real `nage` engine runs. It's the hardest step, so I'll
|
|
give you the skeleton — you write the code.
|
|
|
|
## The idea
|
|
|
|
A single collision doesn't end the frame (step 08's "leftover that matters").
|
|
You hit a wall at `t = 0.3`, slide, and **70% of the frame is still owed** —
|
|
during which you might hit _another_ wall, slide again, and so on. So moving is
|
|
a small **loop**: sweep → stop at the nearest hit → slide → repeat with the
|
|
leftover time.
|
|
|
|
We loop a **maximum of 4 times** (your engine's cap) — enough to handle a corner
|
|
(hit a wall, slide, hit the perpendicular wall, slide, stop) without ever
|
|
risking an infinite loop.
|
|
|
|
## The algorithm
|
|
|
|
You're given the moving `box`, its full-frame displacement `v`, and a list of
|
|
static `walls`. Track a running `pos`, a running `vel`, and `timeLeft` (fraction
|
|
of the frame remaining, starts at `1`).
|
|
|
|
```
|
|
pos = { box.x, box.y }
|
|
vel = { v.x, v.y }
|
|
timeLeft = 1
|
|
|
|
repeat up to 4 times, while timeLeft > 0:
|
|
move = vel * timeLeft // what's left to travel this frame
|
|
|
|
find the NEAREST hit: for each wall, sweptAABB(box-at-pos, move, wall);
|
|
keep the hit with the smallest .time
|
|
|
|
if no hit:
|
|
pos = pos + move // clear path: take the rest of the move
|
|
stop
|
|
|
|
else:
|
|
pos = pos + move * max(0, hit.time - EPSILON) // advance to just before contact
|
|
vel = slide(vel, hit.normal) // redirect along the wall
|
|
timeLeft = timeLeft * (1 - hit.time) // consume the used fraction
|
|
|
|
return pos
|
|
```
|
|
|
|
Two things worth understanding, not just copying:
|
|
|
|
- **`move = vel * timeLeft`.** `vel` is a _full-frame_ displacement (how far
|
|
you'd go in a whole frame at this velocity). You only have `timeLeft` of the
|
|
frame left, so the actual travel is `vel * timeLeft`. `sweptAABB`'s returned
|
|
`time` is then a fraction _of that sub-move_, which is why `pos + move * time`
|
|
is correct.
|
|
|
|
- **The `EPSILON` backoff** (`max(0, hit.time - EPSILON)`). Stop a hair _short_
|
|
of the wall. If you land exactly on it, floating-point error can leave you a
|
|
sliver inside — and next iteration's sweep would start already-overlapping,
|
|
reporting a garbage negative-time "collision" that makes you stick or jitter.
|
|
That tiny gap is exactly the `Math.max(0, time - EPSILON)` in your real
|
|
`moveAndSlide`. Now you know _why_ it's there. `EPSILON` is provided in
|
|
`given.ts`.
|
|
|
|
> **Moving-vs-moving (why your real engine has `velocity - otherVel`).** Here
|
|
> the walls are static, so we sweep with plain `vel`. When the _other_ body also
|
|
> moves, you sweep in its frame of reference by using the **relative** velocity
|
|
> `vel - otherVel` — then the exact same loop works, because from the other
|
|
> body's point of view it's standing still. That's the only difference between
|
|
> this kata and the full engine. The loop itself doesn't change.
|
|
|
|
## Task
|
|
|
|
Implement `moveAndSlide(box, v, walls)` in `moveAndSlide.ts`. Everything you
|
|
need — `sweptAABB`, `slide`, the vector ops, `EPSILON` — is finished in
|
|
`given.ts`.
|
|
|
|
```sh
|
|
bun test workshop/steps/10-move-and-slide
|
|
```
|