Quick answer: The speed difference on slopes is caused by move_and_slide() projecting the velocity vector onto the slope surface. Downhill projection lengthens the vector (faster), uphill projection shortens it (slower). Fix this by normalizing the slope-projected direction and multiplying by a constant speed, or by disabling slide_on_floor and handling slope movement manually.
Here is how to fix Godot CharacterBody3D slope speed inconsistent. Your character moves at a steady 5 m/s on flat ground. Hit a downhill slope and they accelerate to 7 m/s. Turn around to go uphill and they slow to 3 m/s. No code changes, same input, same speed variable — just a different slope direction. Players notice immediately because it feels like the character is ice-skating downhill and trudging through mud uphill. This is not a bug in Godot; it is how velocity projection onto angled surfaces works mathematically. But for most games, you want consistent speed regardless of terrain angle.
The Symptom
A CharacterBody3D moves at different speeds depending on the slope direction. Downhill movement is noticeably faster than flat ground. Uphill movement is noticeably slower. On steep slopes, the speed difference becomes extreme. The character’s horizontal movement speed — which you set in code — does not match the actual speed along the surface.
The issue is most visible in third-person games where the camera shows the character moving across terrain. In first-person games, players feel it as inconsistent movement response: pressing forward on a downhill slope covers more ground than pressing forward uphill, even though the input and code-side speed are identical.
If you log velocity.length() before and after move_and_slide(), you will see that the velocity magnitude changes after the call. The velocity you set is not the velocity that executes. move_and_slide() modifies the velocity as part of its collision response, and slope projection is the primary modifier.
What Causes This
Velocity projection onto the floor normal. When move_and_slide() detects that the character is on a floor (a surface within floor_max_angle of horizontal), it projects the velocity onto the floor plane. This projection preserves the velocity component that is parallel to the surface and removes the component that is perpendicular to it (into the floor).
On flat ground, the floor normal is straight up (0, 1, 0). A horizontal velocity of (5, 0, 0) projected onto this floor remains (5, 0, 0) — no change. On a downhill slope with a normal tilted away from the movement direction, the projection adds a downward component to the velocity. The horizontal distance covered per frame increases because the character is sliding along a longer diagonal path. The projected vector is literally longer than the input vector.
On an uphill slope, the projection removes some of the forward component because part of the velocity points into the slope surface. The remaining parallel component is shorter than the input, so the character moves slower. The steeper the slope, the greater the effect in both directions.
Gravity compounding the problem. If you apply gravity in your _physics_process() function (as most character controllers do), the downward velocity component adds to the downhill acceleration. Going downhill, gravity pulls the character in roughly the same direction as the slope projection, compounding the speed increase. Going uphill, gravity opposes the movement direction, further reducing speed. This makes the difference even more pronounced than pure projection alone.
slide_on_floor behavior. The slide_on_floor property (enabled by default) controls whether the character slides along the floor surface after a collision. When enabled, any remaining velocity after a floor collision is redirected along the surface. This is what causes the projection behavior. When disabled, the character stops on contact with the floor rather than sliding, which prevents slope speed changes but also prevents smooth floor movement entirely — not a useful default.
The Fix
Step 1: Normalize the slope-projected direction. Instead of setting velocity and letting move_and_slide() project it, calculate the slope-projected direction yourself, normalize it, and multiply by your desired constant speed.
extends CharacterBody3D
const SPEED := 5.0
const GRAVITY := 9.8
func _physics_process(delta: float) -> void:
var input_dir := Vector2(
Input.get_axis("move_left", "move_right"),
Input.get_axis("move_forward", "move_back")
).normalized()
var direction := (
transform.basis * Vector3(input_dir.x, 0, input_dir.y)
).normalized()
if direction != Vector3.ZERO and is_on_floor():
# Project direction onto the floor surface
var floor_normal := get_floor_normal()
var slope_dir := direction.slide(floor_normal).normalized()
# Apply constant speed to the slope-aligned direction
velocity.x = slope_dir.x * SPEED
velocity.z = slope_dir.z * SPEED
velocity.y = slope_dir.y * SPEED
elif direction != Vector3.ZERO:
# In the air, use flat direction
velocity.x = direction.x * SPEED
velocity.z = direction.z * SPEED
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
velocity.z = move_toward(velocity.z, 0, SPEED)
# Apply gravity only when not on floor
if !is_on_floor():
velocity.y -= GRAVITY * delta
move_and_slide()
The critical line is direction.slide(floor_normal).normalized(). The slide() method projects the direction vector onto the plane defined by the floor normal — this gives you the slope-aligned direction. Then normalized() scales it back to unit length, removing the magnitude change that causes the speed inconsistency. Multiplying by your constant SPEED ensures the same speed on any slope angle.
Step 2: Handle gravity separately from movement. A common mistake is to add gravity to the velocity before projecting onto the slope, which mixes gravitational acceleration with intentional movement. Keep them separate. Apply gravity only when the character is airborne. When on a floor, the floor normal and move_and_slide()’s floor snapping keep the character grounded without needing continuous gravity application.
# Alternative: use floor_snap to stay grounded on slopes
# without relying on gravity pulling the character down
func _ready() -> void:
floor_snap_length = 0.5 # Snap distance in meters
floor_max_angle = deg_to_rad(45.0)
floor_block_on_wall = true
The floor_snap_length property keeps the character attached to the floor when moving over small bumps and slope transitions. Without it, the character may briefly become airborne at the crest of a slope, and the gravity-based falling code kicks in, causing a small hop. With snapping, the character smoothly follows the terrain contour.
When to Allow Speed Variation
Not all games want constant slope speed. For platformers, the speed difference is part of the game feel — running downhill should feel fast and exhilarating, while running uphill should feel like effort. For these games, the default move_and_slide() behavior is correct. You might even want to amplify it by adding extra acceleration downhill.
For first-person shooters, action RPGs, and exploration games, constant speed is almost always preferred. Players expect their movement to be predictable and consistent regardless of terrain. The slope-projected-then-normalized approach gives you this consistency while still having the character follow the terrain surface naturally.
A middle ground is to allow slight speed variation but cap it. Clamp the projected velocity magnitude to a range like SPEED * 0.9 to SPEED * 1.1, allowing a 10% variation that adds subtle terrain feel without being disruptive to gameplay.
The floor_block_on_wall Interaction
The floor_block_on_wall property interacts with slope movement in a non-obvious way. When enabled, it prevents the character from being pushed up walls that are steeper than floor_max_angle. Without it, a character walking into a steep slope might slide upward along the surface. With it, the character stops at the base of the steep section.
This matters for slope speed because surfaces near the floor_max_angle threshold can alternate between being treated as “floor” and “wall” depending on the character’s approach angle. If a slope is 44 degrees and your floor_max_angle is 45, it is floor. At 46 degrees, it is wall. The transition between these states causes a sudden change in how velocity is handled, which players perceive as jerky movement on moderate slopes. Setting floor_max_angle well above the steepest traversable slope in your level (e.g., 50 degrees for a level with 30-degree slopes) avoids this threshold jitter.
“Slope speed inconsistency is not a Godot bug — it is vector math working as designed. The fix is to separate direction from speed: let the slope determine where you go, but not how fast you get there.”
Related Issues
If your character slides down slopes while standing still, see CharacterBody3D Sliding Down Slopes While Idle for floor snap and velocity zeroing techniques. If the character jitters when transitioning between flat ground and slopes, check Camera Jittering When Following Player for interpolation and physics process timing.
Normalize the slope-projected direction, then multiply by constant speed — never let projection change your magnitude.