Skip to content

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.