Quick answer: In Godot 4, move_and_slide reads from the velocity property of CharacterBody2D. If you never assign a value to velocity before calling move_and_slide, the body will not move. Unlike Godot 3, the method no longer accepts a velocity argument directly.

Here is how to fix Godot CharacterBody2D move and slide not moving. You have a CharacterBody2D in your scene, you are calling move_and_slide() every frame, and your character refuses to move a single pixel. No errors in the Output panel, no crashes — just a body frozen in place. This is one of the most reported issues when migrating to Godot 4, and it almost always traces back to how velocity assignment changed between engine versions. Let us fix it.

The Symptom

Your CharacterBody2D sits motionless in the scene despite your movement code running. You have confirmed that _physics_process is being called by adding a print statement. Input is registering correctly when you test with Input.is_action_pressed(). Yet the character simply does not move when you call move_and_slide().

This is especially confusing if you are coming from Godot 3, where move_and_slide() accepted a velocity vector as a parameter. In Godot 4, the API changed significantly: the method takes no arguments and instead reads from the node’s built-in velocity property. If you are passing velocity as an argument, your code may appear to compile (or you might get a subtle warning you missed), but the character will not move because the internal velocity remains at zero.

Another variant of this symptom is the character moving erratically or at wildly inconsistent speeds. This typically happens when movement code is placed in _process() instead of _physics_process(), causing the physics engine to receive updates at unpredictable intervals.

A third variant is the character moving horizontally but falling through the floor, or moving on flat ground but sliding uncontrollably on slopes. These point to floor detection configuration issues rather than a velocity assignment problem, but they often appear together when setting up a new character controller.

Why move_and_slide Ignores Your Input

In Godot 3, the standard pattern was to compute a velocity vector and pass it directly to move_and_slide():

# Godot 3 pattern - DOES NOT WORK in Godot 4
var velocity = Vector2.ZERO

func _physics_process(delta):
  velocity.x = Input.get_axis("move_left", "move_right") * speed
  velocity = move_and_slide(velocity, Vector2.UP)

In Godot 4, move_and_slide() no longer accepts parameters. It reads from the velocity property that is built into every CharacterBody2D node. If you declare a local variable called velocity, it shadows the built-in property, meaning your assignments go to the local variable while the node’s actual velocity stays at Vector2.ZERO. The method reads the node property, finds zero, and does nothing.

This is the single most common cause. The fix is straightforward: do not declare a local velocity variable. Assign directly to the inherited property.

The second most common cause is calling move_and_slide() from _process() instead of _physics_process(). While this will not prevent movement entirely, it causes the physics engine to process movement at variable frame rates. On fast machines the character may seem fine; on slower machines or during frame drops, movement becomes jittery, and collision detection can fail because the body moves too far in a single step.

The third cause involves the up_direction property. CharacterBody2D uses up_direction to determine what counts as a floor, wall, or ceiling. If up_direction is set incorrectly (for example, to Vector2.ZERO), floor detection breaks completely. The character may move but will never register as being on the floor, which breaks jump logic and gravity handling.

The Correct Pattern

Here is the standard Godot 4 character movement setup that works reliably:

extends CharacterBody2D

const SPEED = 300.0
const JUMP_VELOCITY = -400.0

# Get the gravity from the project settings
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")

func _physics_process(delta):
  # Apply gravity only when airborne
  if not is_on_floor():
    velocity.y += gravity * delta

  # Handle jump
  if Input.is_action_just_pressed("jump") and is_on_floor():
    velocity.y = JUMP_VELOCITY

  # Horizontal movement
  var direction = Input.get_axis("move_left", "move_right")
  if direction:
    velocity.x = direction * SPEED
  else:
    velocity.x = move_toward(velocity.x, 0, SPEED)

  move_and_slide()

Notice that there is no var velocity declaration at the top. The script extends CharacterBody2D, which already has a velocity property. Every assignment like velocity.x = direction * SPEED writes directly to the node’s property, and move_and_slide() reads from it automatically.

Also note that gravity is only applied when is_on_floor() returns false. If you apply gravity unconditionally, the downward velocity accumulates while the character is on the ground. This can cause the character to “snap” through thin platforms or behave strangely when walking off ledges.

The move_toward() call on the else branch provides smooth deceleration when the player releases the movement keys, rather than an abrupt stop. Adjust the third parameter to control how quickly the character decelerates.

Configuring Floor Detection

Even with correct velocity assignment, movement can feel broken if floor detection is not configured properly. CharacterBody2D has several properties that control how it interacts with surfaces:

# Configure floor detection in _ready or the Inspector
func _ready():
  up_direction = Vector2.UP          # (0, -1) - standard for 2D platformers
  floor_snap_length = 8.0            # Snap to floor within 8 pixels
  floor_max_angle = deg_to_rad(46)  # Slopes up to 46 degrees count as floor
  floor_stop_on_slope = true         # Prevent sliding down slopes when idle
  floor_constant_speed = true        # Maintain speed on slopes

The floor_snap_length property is particularly important. It controls how far below the character the engine will look for a floor surface. If your character briefly loses floor contact when walking over small bumps or at the edges of platforms, increasing this value usually solves the problem. A value of 4 to 16 pixels works well for most 2D platformers.

The floor_max_angle determines what the engine considers a “floor” versus a “wall.” The default is 45 degrees (about 0.785 radians). Surfaces steeper than this threshold are treated as walls, meaning is_on_floor() returns false even if the character is resting on them. If your game has steep ramps the player should walk on, increase this value.

The floor_stop_on_slope property, when true, prevents the character from sliding down slopes when no horizontal input is applied. Without this, gravity causes a slow downhill drift on any non-flat surface, which looks and feels wrong in most platformers.

The floor_constant_speed property ensures the character moves at the same speed regardless of slope angle. Without it, characters move slower going uphill and faster going downhill, which may or may not be desirable depending on your game.

Common Mistakes and Edge Cases

Beyond the velocity shadowing issue, there are several other mistakes that cause move_and_slide to behave unexpectedly:

Multiplying velocity by delta twice. The move_and_slide() method already accounts for the physics timestep internally. If you also multiply your velocity by delta, movement will be frame-rate dependent and much slower than expected. Only multiply acceleration or gravity by delta, not the final velocity you assign.

# WRONG - double delta application
velocity.x = direction * SPEED * delta  # Too slow!

# CORRECT - move_and_slide handles delta internally
velocity.x = direction * SPEED

Resetting velocity before move_and_slide. If your code sets velocity = Vector2.ZERO at the top of every _physics_process call, you discard any velocity that move_and_slide calculated on the previous frame. This breaks gravity accumulation and any momentum-based movement. Instead, only modify the components you need to change.

Using a kinematic body without a CollisionShape2D. A CharacterBody2D without a collision shape child can still have velocity applied, but move_and_slide will not detect any collisions. The body may appear to move but will pass through all geometry. Godot shows a yellow warning icon on the node when it has no collision shape, but this is easy to overlook.

Conflicting scripts on parent and child nodes. If both a parent node and the CharacterBody2D have scripts that call move_and_slide, or if a parent script moves the body by setting its position directly, the results will conflict with the physics engine’s calculations. Only one script should be responsible for calling move_and_slide, and you should avoid setting position directly on a CharacterBody2D during normal gameplay.

"The Godot 4 migration killed my character controller. Once I deleted the local velocity variable, everything started working. The move_and_slide API change is the single most common migration issue I see in the community."

Related Issues

If your character moves but jitters against walls, see our guide on CharacterBody2D jittering and wall sliding. For characters that slide down slopes when idle, check slope sliding fixes. If your character falls through the floor entirely, the problem is likely collision shape configuration — our collision layer and mask guide covers the setup in detail.

Delete the local velocity variable. That is the fix 90% of the time.