Quick answer: CharacterBody3D in Godot 4 does not have built-in step climbing. The move_and_slide() method treats steps as walls if the vertical face exceeds the floor_max_angle. Even small steps (0.1-0.
Here is how to fix Godot CharacterBody3D stairs climbing stuck. Your character walks across flat ground perfectly, but the moment they hit a staircase, they stop dead. CharacterBody3D in Godot 4 does not have built-in stair climbing, and move_and_slide() treats every step as a wall. This guide covers multiple approaches to implement smooth stair traversal, from simple margin tweaks to a full raycast-based step system.
Why CharacterBody3D Cannot Climb Stairs by Default
The move_and_slide() method on CharacterBody3D classifies collisions as either floor, wall, or ceiling based on the floor_max_angle property. A vertical step face is at 90 degrees to the ground—well beyond the default floor angle of 45 degrees. The physics system correctly identifies the step as a wall and stops the character.
This is intentional behavior. Unlike engines with built-in step-offset parameters (like the old Godot 3 KinematicBody step workaround or Unity’s CharacterController.stepOffset), Godot 4’s CharacterBody3D leaves step climbing up to the developer. This gives more control but requires more work.
The collision shape matters too. A capsule collider handles steps slightly better than a box because its curved bottom can slide up small ledges. But even with a capsule, anything over about 0.05 meters will stop the character.
Solution 1: The Raycast Step-Up Method
The most reliable stair climbing solution uses raycasts to detect steps ahead of the character and teleport the body up by the step height. This is the approach used by most production Godot games.
extends CharacterBody3D
@export var speed := 5.0
@export var max_step_height := 0.4
@export var gravity := 9.8
@onready var step_ray_top := $StepRayTop
@onready var step_ray_bottom := $StepRayBottom
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= gravity * delta
var input_dir := Input.get_vector(
"move_left", "move_right",
"move_forward", "move_back")
var direction := (transform.basis * Vector3(
input_dir.x, 0, input_dir.y)).normalized()
velocity.x = direction.x * speed
velocity.z = direction.z * speed
# Try stair stepping before move_and_slide
if direction.length() > 0.1 and is_on_floor():
_try_step_up(direction)
move_and_slide()
func _try_step_up(direction: Vector3) -> void:
# Point raycasts in movement direction
var ray_dir := direction * 0.5
step_ray_bottom.target_position = ray_dir
step_ray_top.target_position = ray_dir
step_ray_bottom.force_raycast_update()
step_ray_top.force_raycast_update()
# Bottom ray hits a wall, top ray is clear = step
if step_ray_bottom.is_colliding() and not step_ray_top.is_colliding():
var hit_point := step_ray_bottom.get_collision_point()
var step_height := hit_point.y - global_position.y
if step_height > 0.0 and step_height <= max_step_height:
global_position.y += step_height + 0.01
The scene tree setup requires two RayCast3D nodes as children of your CharacterBody3D. Position StepRayBottom at the character’s feet (Y = 0.05) and StepRayTop at the maximum step height (Y = max_step_height + 0.05). Both should cast forward in the movement direction.
Solution 2: ShapeCast3D Step Detection
For more accurate step detection, use a ShapeCast3D instead of individual raycasts. A shape cast sweeps the character’s full collision shape forward and can detect steps more reliably, especially with irregular geometry.
extends CharacterBody3D
@export var max_step_height := 0.4
@onready var shape_cast := $StepShapeCast
func _try_step_up_shapecast(direction: Vector3) -> void:
if not is_on_floor() or direction.length() < 0.1:
return
# First, move shape cast up by max step height
var test_position := global_position + Vector3.UP * max_step_height
# Then cast forward in movement direction
shape_cast.global_position = test_position
shape_cast.target_position = direction * 0.6
shape_cast.force_shapecast_update()
if not shape_cast.is_colliding():
# Space is clear at step height — now check floor below
var forward_pos := test_position + direction * 0.4
var space := get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(
forward_pos,
forward_pos + Vector3.DOWN * (max_step_height + 0.1))
query.exclude = [get_rid()]
var result := space.intersect_ray(query)
if result:
var step_y := result.position.y
if step_y > global_position.y:
global_position.y = step_y + 0.01
This method is more robust because the ShapeCast3D uses the same collision shape as the character, preventing false positives from narrow geometry that a single ray might miss.
Solution 3: Collision Margin and Snap Adjustments
For very small steps (under 0.1 meters), you can sometimes get away with adjusting the collision margin and floor snap settings without writing any step logic:
extends CharacterBody3D
func _ready() -> void:
# Increase floor snap to stick to uneven ground
floor_snap_length = 0.3
# Increase max floor angle to treat steeper surfaces as floor
floor_max_angle = deg_to_rad(55.0)
# Reduce safe margin for tighter collision response
safe_margin = 0.01
This approach is limited. It works for slight unevenness in terrain and very short steps, but it will not handle real staircases with visible step geometry. It can also cause the character to slide up walls at steep angles, which is usually worse than the original problem.
Smooth Camera During Step-Up
The biggest visual problem with stair climbing is camera jitter. When the character teleports up by the step height, the camera jumps with it, creating a jarring stutter. The fix is to decouple the camera from the physics body.
extends Node3D
# Attach this to the camera's parent node
@export var player: CharacterBody3D
@export var smooth_speed := 15.0
var target_y := 0.0
func _physics_process(delta: float) -> void:
target_y = player.global_position.y
# Smoothly interpolate camera Y position
global_position.x = player.global_position.x
global_position.z = player.global_position.z
global_position.y = lerpf(global_position.y, target_y,
smooth_speed * delta)
Place your Camera3D as a child of this smoothing node instead of directly under the CharacterBody3D. The camera will follow the character’s X and Z position exactly but smoothly interpolate the Y position, eliminating stair-step jitter.
Level Design Considerations
Beyond code fixes, your level geometry has a major impact on stair traversal:
Use ramps for collision. Even if your stairs look like individual steps visually, use a single angled collision shape (a ramp) for the physics layer. This is the most common solution in commercial games and eliminates stair climbing issues entirely.
Keep step heights consistent. If you do use per-step collision, ensure every step is the same height. Mixed step heights cause unpredictable behavior with any step-climbing implementation.
Add collision bevels. Slightly beveling the top edge of each step (even by 0.02 meters) can help the character slide up steps with a capsule collider without any code changes.
“The ramp collider approach is the industry standard for a reason. Let the visual mesh be detailed stairs, but make the collision shape a smooth ramp. Your step-climbing code becomes zero lines.”
Related Issues
For other CharacterBody3D movement problems, see Fix: Godot HTTPRequest Not Completing or Timing Out if you are also dealing with multiplayer networking. To automatically collect player movement bugs from testers, check out How to Add a Bug Reporter to a Godot Game.
Stair climbing is a solved problem in game design—the solution is usually a ramp collider, not a code workaround. But when you need code, the raycast step-up method handles every edge case.