Quick answer: Wall jumps fail in Godot 4 because is_on_wall() requires the character to be actively pressing into the wall during move_and_slide(). Use side-facing RayCast2D nodes for reliable wall detection, read the collision normal to determine jump direction, and apply both horizontal and vertical velocity in the same frame.
Wall jumping is a staple mechanic in 2D platformers, but getting it to work reliably with Godot’s CharacterBody2D is surprisingly tricky. The most common issue is that is_on_wall() returns false even when the player visually touches a wall, or the jump fires but sends the character in the wrong direction. Here’s how to fix it.
Why is_on_wall() Is Unreliable for Wall Jumps
The is_on_wall() method only returns true after move_and_slide() has processed a collision with a surface whose angle exceeds floor_max_angle. This means two things must be true simultaneously: the character must be moving into the wall, and the collision normal must be steep enough to qualify as a “wall” rather than a slope.
In practice, this breaks wall jumping in several ways. If the player releases the horizontal movement key before pressing jump, is_on_wall() returns false because there’s no collision happening. If the wall has even a slight angle (common with hand-drawn or tile-based levels), it might register as a slope instead. And if the character is falling too fast, the collision may be classified differently based on the velocity vector.
The fix is to stop relying on is_on_wall() and use dedicated raycasts instead.
Setting Up Raycast-Based Wall Detection
Add two RayCast2D nodes as children of your CharacterBody2D. One points left, one points right. Set their target_position to extend just past the edge of your collision shape — typically 2–4 pixels beyond the character’s width.
# Scene tree:
# CharacterBody2D
# CollisionShape2D
# RayCast2D (WallCheckLeft) - target_position = Vector2(-12, 0)
# RayCast2D (WallCheckRight) - target_position = Vector2(12, 0)
@onready var wall_check_left: RayCast2D = $WallCheckLeft
@onready var wall_check_right: RayCast2D = $WallCheckRight
func is_touching_wall() -> bool:
return wall_check_left.is_colliding() or wall_check_right.is_colliding()
func get_wall_normal() -> Vector2:
if wall_check_left.is_colliding():
return wall_check_left.get_collision_normal()
if wall_check_right.is_colliding():
return wall_check_right.get_collision_normal()
return Vector2.ZERO
Make sure both raycasts are enabled and set to collide with your wall collision layer. If your walls are on a different layer than your floors, configure the raycast’s collision_mask accordingly.
Implementing the Wall Jump
With reliable wall detection in place, the wall jump itself is straightforward. When the player presses jump while touching a wall and not on the floor, apply both vertical and horizontal velocity:
const WALL_JUMP_VELOCITY_Y: float = -300.0
const WALL_JUMP_VELOCITY_X: float = 200.0
const WALL_JUMP_INPUT_LOCKOUT: float = 0.15
var input_locked: bool = false
var input_lock_timer: float = 0.0
var last_wall_normal: Vector2 = Vector2.ZERO
func _physics_process(delta: float) -> void:
# Handle input lockout timer
if input_locked:
input_lock_timer -= delta
if input_lock_timer <= 0.0:
input_locked = false
# Apply gravity
if not is_on_floor():
velocity.y += gravity * delta
# Ground jump
if is_on_floor() and Input.is_action_just_pressed("jump"):
velocity.y = JUMP_VELOCITY
last_wall_normal = Vector2.ZERO
# Wall jump
elif not is_on_floor() and is_touching_wall() and Input.is_action_just_pressed("jump"):
var wall_normal = get_wall_normal()
if wall_normal != last_wall_normal:
velocity.y = WALL_JUMP_VELOCITY_Y
velocity.x = wall_normal.x * WALL_JUMP_VELOCITY_X
last_wall_normal = wall_normal
input_locked = true
input_lock_timer = WALL_JUMP_INPUT_LOCKOUT
# Horizontal movement (skip if input locked)
if not input_locked:
var direction = Input.get_axis("move_left", "move_right")
velocity.x = direction * SPEED
move_and_slide()
The input_lock_timer is crucial. Without it, the player can hold the opposite direction and immediately cancel the horizontal push from the wall jump, making the character slide straight up the wall. A lockout of 100–200 milliseconds feels responsive without being sluggish.
Preventing Infinite Wall Climbing
Without a check, players can repeatedly jump off the same wall to climb infinitely. The last_wall_normal variable prevents this — it stores the normal from the most recent wall jump and blocks another jump in the same direction until the player either lands on the floor (which resets it to Vector2.ZERO) or touches a wall on the opposite side.
Reset the tracking when the player lands:
if is_on_floor():
last_wall_normal = Vector2.ZERO
If you want to allow multiple jumps on the same wall (like climbing a shaft by alternating between two walls), you can skip the normal check entirely and instead limit wall jumps with a counter or a short cooldown timer. The right approach depends on your game’s design — Celeste-style wall climbing is very different from Mega Man X-style wall jumps.
Test your wall jump with different wall materials, angles, and character speeds. Edge cases like jumping at the top edge of a wall, hitting a corner where two walls meet, or wall-jumping during a wall slide all need specific attention. Build a test level with various wall configurations and iterate until every case feels right.
Wall jumps are 90% about feel — get the physics right, then tweak the numbers until it’s fun.