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.velis a full-frame displacement (how far you'd go in a whole frame at this velocity). You only havetimeLeftof the frame left, so the actual travel isvel * timeLeft.sweptAABB's returnedtimeis then a fraction of that sub-move, which is whypos + move * timeis correct. -
The
EPSILONbackoff (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 theMath.max(0, time - EPSILON)in your realmoveAndSlide. Now you know why it's there.EPSILONis provided ingiven.ts.
Moving-vs-moving (why your real engine has
velocity - otherVel). Here the walls are static, so we sweep with plainvel. When the other body also moves, you sweep in its frame of reference by using the relative velocityvel - 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.
bun test workshop/steps/10-move-and-slide