junkers devlog 03: parkour as a state machine
Phase 2: the move set
Wall-slide, wall-jump, an eight-direction dash, ledge-grab into a mantle, and a vault all
became states in a plain enum and switch state machine. I went with that over function
pointers for practical reasons. Every transition shows up in the debug overlay as text
like state:WALL_SLIDE, it steps in gdb without a symbol lookup, and all the
logic stays in one block I can read top to bottom.
The state machine and its enter hooks
enter_state is the one place that resets the timers and flags a state owns,
so a new way into a state cannot forget to set them up.
static void enter_state(Player *p, PlayerState ns) {
p->state = ns;
switch (ns) {
case PS_DASH:
p->dash_available = false;
p->dash_timer = PLR_DASH_DURATION;
p->dash_buffer_timer = 0.0f;
break;
case PS_MANTLE: p->mantle_timer = PLR_MANTLE_DURATION; break;
case PS_VAULT: p->vault_timer = PLR_VAULT_DURATION; break;
/* ... grounded / wall-slide / ledge-hang ... */
}
}
Velocity is not set in the hook. It is worked out by the caller from context (the dash direction, the wall-jump kick, the vault arc), not from the destination state on its own.
Normalising the diagonal dash
The dash aims from the movement input. A raw diagonal with both axes at 1 would be about 1.41 times faster than a straight dash, so diagonals get scaled by 1/sqrt(2), about 0.707.
float dh = h, dv = v;
if (dh == 0.0f && dv == 0.0f) dh = (float)p->facing; /* no aim, use facing */
if (dh != 0.0f && dv != 0.0f) { dh *= 0.707f; dv *= 0.707f; } /* normalise diagonal */
p->vel_x = dh * PLR_DASH_SPEED;
p->vel_y = dv * PLR_DASH_SPEED;
The wall-jump applies a 0.13s input lockout (around 8 ticks) after the transition runs, zeroing the horizontal input long enough for the sideways kick to set up a trajectory. Without it, holding toward the wall cancels the jump and it feels sticky.
The vault arc
The vault is a scripted parabola. X moves linearly across the obstacle while Y follows a quadratic that peaks in the middle.
box.y = vault_start_y - vault_arc * 4 * frac * (1 - frac); /* frac in [0,1] */
At frac = 0.5 the term 4 * 0.25 = 1.0, so the rise is exactly
vault_arc px, and it is zero at both ends. Setting vault_arc to
the obstacle height plus 8 leaves 8px of clearance over the lip.
The art so far
The player is still a debug rectangle in the running game. Sprite animation is the next real milestone (Phase 3b), but the animation itself is being worked out on paper.
Asuka, sprite work.

Working out the running animation.

Standing animation, goggles activating.
← movement and collision all posts next: camera and a bigger world →