Quick answer: Set floor_snap_length to ~0.5 (stair height). Set floor_max_angle to ~45°. Build stairs as a sloped collider (Box rotated, or a single ramp mesh) for the smoothest experience. Per-step colliders trigger floor-loss snap each step.

Player runs down stairs. Each step launches them into the air for a frame. Sound effect for landing fires repeatedly. CharacterBody3D’s default snap distance doesn’t reach the next step.

The Symptom

Going down stairs: bouncing or stuttering. Going up: bumping into the riser, then either step-up or stop. Audio events tied to grounded state fire repeatedly.

What Causes This

CharacterBody3D’s move_and_slide checks for a floor below within floor_snap_length each tick. When the ground drops away by more than the snap distance (the next step in a staircase), the body becomes airborne, gravity applies, then re-touches the next step the frame after.

The Fix

Step 1: Set floor_snap_length.

extends CharacterBody3D

@export var speed := 5.0
@export var jump_velocity := 5.0

func _ready() -> void:
    floor_snap_length = 0.5            # up to 0.5m drop holds the body to ground
    floor_max_angle = deg_to_rad(45)   # anything steeper is a wall
    floor_constant_speed = true       # constant horizontal on slopes
    floor_stop_on_slope = false        # keep momentum on hills

Step 2: Build stairs as a slope. Easiest. Replace step geometry with a single ramp mesh (or a Box collider rotated to ~30°). The character slides up/down without per-step snapping.

Step 3: Or, write step detection. When a horizontal collision is below step_height, raycast for a higher floor and snap.

func _physics_process(delta: float) -> void:
    velocity.y -= GRAVITY * delta
    velocity.x = input_dir.x * speed
    velocity.z = input_dir.z * speed
    move_and_slide()

    # Step-up after a wall hit
    if get_slide_collision_count() > 0:
        var col = get_slide_collision(0)
        if col.get_normal().y < 0.1:   # a wall
            var step = try_step_up(col.get_position())
            if step:
                global_position.y += STEP_HEIGHT

Verifying

Walk up and down a staircase. Position.y should change smoothly. is_on_floor() should stay true. Tracked landing/grounding signals fire only when actually leaving the floor (jump, fall).

“Snap length covers the step. Slope collider over per-step. Constant speed on slopes. Stairs feel solid.”

Related Issues

For physics interpolation jitter, see interpolation jitter. For input deadzone, see deadzone.

Snap length. Slope. Smooth.