2026-06-10

The Root That Slipped Between Frames — Collisions and Tunneling

Collision checks split into two kinds.
Point-shaped entities like carrots and rocks are approximated as spheres, so if the 3D distance to the player is less than the sum of the radii, I count it as a hit.
Simple, but it was enough.

A hit chips away a heart; run out and it's game over

The tricky one was the roots.
A root isn't a point — it's an arc-shaped obstacle stretching across the tunnel's circumference.
So it's a collision only when all three conditions hold: the depth is close, the rabbit's angle is within the root's arc range, and the jump height didn't clear the root.
Jump high enough and you pass over the root.

This is where the real bug showed up.
At high speed, the rabbit just punched straight through the roots.
The cause was the per-frame travel distance.
At a top speed of 30m/s, if the frame rate drops to 20fps, you skip 1.5m in a single frame.
But the root's depth check window was narrower than that (1.46m).
The rabbit is in front of the root one frame and behind it the next — and the moment they actually overlap slips between the two frames.
Classic tunneling.

The fix was to add this frame's travel distance (sweep) to the check window.

export function hitsRoot(playerDepth, p, rootDepth, root, sweep = 0): boolean {
  if (Math.abs(playerDepth - rootDepth) > SPAWN.ROOT_TUBE + PLAYER.RADIUS + sweep)
    return false;
  if (p.height >= SPAWN.ROOT_CLEARANCE) return false;
  return angularDist(p.angle, root.angle) < root.arc / 2 + PLAYER.RADIUS / TUNNEL.RADIUS;
}

This way, the faster you go the wider the check window grows, so you won't miss a root unless you jumped.

Near-misses (+50 points) had a trick too.
I judge them by the cross-section distance after passing, not as you approach.
The moment you didn't hit but barely grazed by, I measure it right after the pass and hand out the bonus.
That one little detail rewards the thrill of dodging with points.

Next time, the last one: synthesizing sound effects in code.

Next: Synthesizing SFX with Web Audio →