junkers devlog 02: movement feel and swept collision
Phase 1: feel before art
The coordinate space is locked at 320x240 logical pixels on a 16px tile grid, scaled up by whole numbers to a resizable window (4x by default, so 1280x960). Every physics constant is tuned to that scale.
Swept AABB collision
A fast runner breaks the naive approach to collision. At 400px/s and 60Hz the player moves about 7px a tick, so a 4px one-way platform can be stepped straight over without the two boxes ever overlapping. That is tunnelling. The fix is to test the path the player takes, not just where it ends up. I expand the static target by the mover's size (a Minkowski difference) and cast a ray from the mover's corner through that expanded box, one axis at a time.
/* expand target by the mover's dimensions, so box-vs-box becomes ray-vs-box */
float ex = target.x - mover.w; float ew = target.w + mover.w;
float ey = target.y - mover.h; float eh = target.h + mover.h;
tx_near = (ex - ox) / dx; tx_far = (ex + ew - ox) / dx; /* x slab times */
ty_near = (ey - oy) / dy; ty_far = (ey + eh - oy) / dy; /* y slab times */
float t_entry = (tx_near > ty_near) ? tx_near : ty_near; /* enter = latest near */
float t_exit = (tx_far < ty_far ) ? tx_far : ty_far; /* exit = earliest far */
if (t_entry >= t_exit) return none; /* miss */
if (t_entry >= 1.0f) return none; /* contact is past this frame */
if (t_entry < 0.0f) return none; /* already overlapping */
The contact normal comes from whichever axis the ray entered last. If
tx_near is greater than ty_near the mover hit a vertical face, so
the normal is on x with the sign opposite the motion. Otherwise it is a horizontal face on
y.
Resolving and sliding
Resolution runs up to four times. Find the earliest impact across all colliders, move up to that contact, then take the leftover displacement and velocity and remove the part pointing into the surface. That is what makes the player slide along a floor or wall instead of stopping dead. Then re-sweep with what is left.
mover->x += rx * best_t; rx *= (1.0f - best_t);
mover->y += ry * best_t; ry *= (1.0f - best_t);
/* remove the surface-normal component so we slide instead of stick */
float dot = rx * best_nx + ry * best_ny;
rx -= dot * best_nx;
ry -= dot * best_ny;
One-way platforms use the same code but are ignored unless the player is moving down and
started the step with their feet at or above the platform top. Ground detection is just an
upward normal, best_ny < -0.5.
On top of the collision sits the feel layer. Coyote time gives a 0.10s window where a jump still works just after you walk off a ledge. Jump buffering fires a jump pressed up to 0.12s early the moment you land. Releasing the jump button early while still rising cuts the upward velocity to 0.3 of itself for a short hop. None of it shows up in a screenshot, but it is most of what makes the movement feel right.
← the loop and the arena all posts next: the parkour state machine →