Wall-slide and wall-jump
const WALL_SLIDE_MAX_FALL_SPEED = 120.0 // clamped fall speed while sliding
const WALL_JUMP_VELOCITY = Vector2(350.0, -420.0) // away-x, up-y magnitudes
const WALL_JUMP_LOCKOUT_TIME = 0.15 // seconds of suppressed air control after wall jump
const WALL_COYOTE_TIME = 0.10 // grace period after leaving a wall
state wall_contact_dir = 0 // -1 left wall, 1 right wall, 0 none
state wall_coyote_timer = 0.0
state wall_jump_lockout = 0.0
state is_wall_sliding = false
function check_wall_contact():
if cast_side(direction = -1, distance = 2.0) != null:
return -1
if cast_side(direction = 1, distance = 2.0) != null:
return 1
return 0
function update_wall_slide_state(dt):
wall_contact_dir = check_wall_contact()
pressing_into_wall = (wall_contact_dir != 0)
and (get_input_direction() == wall_contact_dir)
is_wall_sliding = (not is_grounded)
and (wall_contact_dir != 0)
and pressing_into_wall
and velocity.y > 0 // only while falling, not while rising past a wall
if wall_contact_dir != 0 and not is_grounded:
wall_coyote_timer = WALL_COYOTE_TIME
else:
wall_coyote_timer = max(0.0, wall_coyote_timer - dt)
function apply_wall_slide_clamp():
if is_wall_sliding:
velocity.y = min(velocity.y, WALL_SLIDE_MAX_FALL_SPEED)
function handle_wall_jump_input():
if input.was_pressed("jump") and wall_coyote_timer > 0.0 and not is_grounded:
jump_away_dir = -wall_contact_dir // push off, away from the wall
velocity.x = WALL_JUMP_VELOCITY.x * jump_away_dir
velocity.y = WALL_JUMP_VELOCITY.y
wall_coyote_timer = 0.0
wall_jump_lockout = WALL_JUMP_LOCKOUT_TIME
function update_wall_jump_lockout(dt):
wall_jump_lockout = max(0.0, wall_jump_lockout - dt)
function apply_horizontal_acceleration_with_lockout(dt):
if wall_jump_lockout > 0.0:
return // ignore horizontal input entirely during lockout
apply_horizontal_acceleration(dt) // normal section-1 behavior otherwise
function update(dt):
update_wall_slide_state(dt)
handle_wall_jump_input()
update_wall_jump_lockout(dt)
apply_horizontal_acceleration_with_lockout(dt)
handle_jump_input() // ground/coyote jump still applies when grounded
apply_gravity(dt)
apply_wall_slide_clamp()
integrate_position(dt)
integrate_vertical(dt)
How it works¶
Wall-slide is really just a second, situational terminal velocity: while airborne, touching a wall, holding input into that wall, and already falling, gravity's normal clamp (MAX_FALL_SPEED) is replaced with a much lower one (WALL_SLIDE_MAX_FALL_SPEED). The "already falling" (velocity.y > 0) condition matters — without it, jumping up alongside a wall would immediately clamp your rising velocity down to the slide speed, which feels wrong.
Wall-jump imparts velocity on both axes at once: a horizontal push away from the wall and a vertical impulse upward, both set directly (not accelerated into, same reasoning as the dash). The near-universal gotcha with wall-jump is that the very next frame, if the player is still holding the stick toward the wall (which they usually are, since that's how they were sliding), normal air control immediately cancels the outward push and the character just falls back down the same wall. WALL_JUMP_LOCKOUT_TIME solves this by suppressing horizontal input entirely for a short window right after the jump, guaranteeing the outward arc actually plays out before the player regains steering.
WALL_COYOTE_TIME mirrors ground Coyote Time for the same reason: players often press jump a beat after their character has already peeled off the wall, and without a grace window that input is silently dropped.