Can you write this from memory?
Define the lifecycle callback that runs at a fixed rate for physics.
Godot 4 changed movement. velocity is a property, not a parameter. move_and_slide() takes no arguments. Input.get_axis replaces manual action checks. For the math behind directions and targeting, see vector math.
Practice the Godot 4 movement patterns and your player moves on the first try.
- Diagonal speed too fast → use
Input.get_vector()(returns length ≤ 1, already clamped) - Move stutters / inconsistent → use
_physics_processformove_and_slide() - Micro-jitter on high refresh → enable Physics Interpolation (Project Settings → Physics → Common)
- Slope "flying off" → set
floor_snap_length(+ optionalfloor_stop_on_slope) - is_on_floor() always false → check it after
move_and_slide()(only valid after the call) - Need post-collision velocity → use
get_real_velocity()/get_position_delta()for what actually happened
For a printable reference of all input patterns, see the GDScript input handling cheat sheet. Once movement feels right, layer in collision detection for walls and enemies, and scene instancing for spawning projectiles or level pieces at runtime.
Standard Top-Down Movement
extends CharacterBody2D
@export var speed: float = 300.0
func _ready() -> void:
motion_mode = CharacterBody2D.MOTION_MODE_FLOATING
func _physics_process(_delta: float) -> void:
var direction := Input.get_vector("left", "right", "up", "down")
velocity = direction * speed
move_and_slide()
Why MOTION_MODE_FLOATING? CharacterBody2D defaults to MOTION_MODE_GROUNDED (for platformers), which has floor/ceiling/wall concepts and slope behavior. For top-down games, FLOATING disables floor logic—collisions are all treated as walls, and slide speed stays constant.
Standard Platformer Movement
extends CharacterBody2D
@export var speed: float = 200.0
@export var jump_force: float = -400.0
var gravity := float(ProjectSettings.get_setting("physics/2d/default_gravity"))
func _physics_process(delta: float) -> void:
# Gravity
if not is_on_floor():
velocity.y += gravity * delta
# Jump
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_force
# Horizontal movement
var direction := Input.get_axis("left", "right")
velocity.x = direction * speed
move_and_slide()
This is the most common movement bug in Godot:
| Function | Runs | Use For |
|---|---|---|
_process(delta) | Every frame (variable rate) | Visuals, UI, non-physics animation |
_physics_process(delta) | Fixed tick (default 60 TPS; configurable in Project Settings) | ALL movement and collision code |
If your character stutters or moves differently on fast/slow machines, you're probably using _process for movement. move_and_slide() uses the physics step's delta automatically and should be called from _physics_process (or a function called by it).
If you're using _physics_process correctly but still see micro-stutter on 120Hz+ monitors, enable Physics Interpolation in Project Settings → Physics → Common → Physics Interpolation. This smooths the visual position between physics ticks.
2D physics interpolation was added in Godot 4.3. 3D interpolation arrived in Godot 4.4. Without it, objects only update visually at your physics tick rate (default 60 TPS) even on faster displays.
Input.get_vector("left", "right", "up", "down") is the preferred way to read directional input:
- Returns a Vector2 whose length is limited to 1 — so diagonal keyboard input is normalized automatically (no ~1.41x faster diagonals)
- Uses a circular deadzone — behaves well for analog sticks (half-push = ~0.5 length)
- Replaces manual
Input.get_axis()pairs + normalize calls
For single-axis input (platformer horizontal), Input.get_axis("left", "right") returns a float from -1 to 1.
Velocity is "pixels per second." When you set velocity = direction * speed, that's already a rate. move_and_slide() handles the frame timing internally.
Acceleration integrates with delta. When you're adding to velocity (gravity, acceleration, friction), multiply by delta:
# WRONG: Gravity gets faster at higher FPS
velocity.y += gravity
# RIGHT: Gravity is frame-rate independent
velocity.y += gravity * delta
Never multiply velocity by delta before move_and_slide():
# WRONG: Double-applies delta
velocity = direction * speed * delta
move_and_slide()
# RIGHT: velocity is already per-second
velocity = direction * speed
move_and_slide()
Walking down slopes can make your character "fly" off because gravity takes a frame to catch up. Fix this with floor_snap_length:
func _ready() -> void:
floor_snap_length = 8.0 # Snap to floor within 8 pixels
floor_stop_on_slope = true # Don't slide when standing still
floor_max_angle = deg_to_rad(50) # Max walkable slope angle
These properties turn a "basic" platformer controller into a "good" one.
When movement feels wrong:
print(velocity)to see actual values- Confirm you're in
_physics_process, not_process - Check
is_on_floor()/is_on_wall()aftermove_and_slide() - Use
get_real_velocity()to see post-collision velocity
This is a common pitfall: is_on_floor(), is_on_wall(), and is_on_ceiling() only return valid values after calling move_and_slide(). They report the result of the last collision check.
# WRONG: Checking before move_and_slide
if is_on_floor():
jump()
move_and_slide()
# RIGHT: Check after move_and_slide
move_and_slide()
if is_on_floor():
can_jump = true
In Godot 3, move_and_slide() returned the resulting velocity. In Godot 4, it returns a bool indicating whether a collision occurred. To get the post-collision velocity, read velocity directly or use get_real_velocity().
Coyote Time + Jump Buffer
Coyote time lets the player jump briefly after leaving a platform. Jump buffer queues a jump if pressed slightly before landing. Both make platformers feel responsive.
extends CharacterBody2D
@export var speed: float = 200.0
@export var jump_force: float = -400.0
@export var coyote_time: float = 0.1
@export var jump_buffer_time: float = 0.1
var gravity := float(ProjectSettings.get_setting("physics/2d/default_gravity"))
var coyote_timer: float = 0.0
var jump_buffer_timer: float = 0.0
func _physics_process(delta: float) -> void:
# Gravity
if not is_on_floor():
velocity.y += gravity * delta
coyote_timer -= delta
else:
coyote_timer = coyote_time
# Jump buffer
if Input.is_action_just_pressed("jump"):
jump_buffer_timer = jump_buffer_time
else:
jump_buffer_timer -= delta
# Jump with coyote time and buffer
if jump_buffer_timer > 0 and coyote_timer > 0:
velocity.y = jump_force
coyote_timer = 0 # Consume coyote time
jump_buffer_timer = 0 # Consume buffer
# Horizontal
var direction := Input.get_axis("left", "right")
velocity.x = direction * speed
move_and_slide()
Dash with Cooldown
A dash that doesn't break normal movement control:
extends CharacterBody2D
@export var speed: float = 200.0
@export var dash_speed: float = 600.0
@export var dash_duration: float = 0.15
@export var dash_cooldown: float = 0.5
var gravity := float(ProjectSettings.get_setting("physics/2d/default_gravity"))
var dash_timer: float = 0.0
var dash_cooldown_timer: float = 0.0
var dash_direction := Vector2.ZERO
func _physics_process(delta: float) -> void:
dash_timer -= delta
dash_cooldown_timer -= delta
var direction := Input.get_axis("left", "right")
# Start dash
if Input.is_action_just_pressed("dash") and dash_cooldown_timer <= 0 and direction != 0:
dash_timer = dash_duration
dash_cooldown_timer = dash_cooldown
dash_direction = Vector2(sign(direction), 0)
# Apply movement
if dash_timer > 0:
velocity.x = dash_direction.x * dash_speed
velocity.y = 0 # Optional: freeze Y during dash
else:
if not is_on_floor():
velocity.y += gravity * delta
velocity.x = direction * speed
move_and_slide()
When to Use GDScript Movement
- Implementing player or NPC movement that respects physics collisions via CharacterBody2D
- Reading directional input with Input.get_axis or Input.get_vector for smooth 2D movement
- Applying gravity and jump mechanics in a platformer using _physics_process and delta
Check Your Understanding: GDScript Movement
Check Your Understanding: Why does move_and_slide() take no arguments in Godot 4, and how does velocity work on CharacterBody2D?
In Godot 4, CharacterBody2D has a built-in velocity property. You set velocity directly, then call move_and_slide() with no arguments. It reads velocity automatically and updates it after resolving collisions. This replaces the Godot 3 pattern of passing a velocity vector and capturing the return value. The built-in property also enables is_on_floor() and is_on_wall() to work correctly after the call.
What You'll Practice: GDScript Movement
Common GDScript Movement Pitfalls
- Using _process instead of _physics_process for movement—causes inconsistent speed at different frame rates and missed collisions
- Forgetting to normalize the input direction vector, making diagonal movement faster than cardinal movement—use Input.get_vector() instead
- Passing arguments to move_and_slide() as in Godot 3—it takes no arguments in Godot 4 and reads the velocity property directly
- Multiplying velocity by delta before move_and_slide()—this double-applies frame timing and makes movement extremely slow
- Checking is_on_floor()/is_on_wall() before calling move_and_slide()—these values are only valid after the call, not before
GDScript Movement FAQ
How does move_and_slide work in Godot 4?
In Godot 4, move_and_slide() takes no parameters. You assign the velocity property on CharacterBody2D before calling it. The method moves the body, slides along collisions, and updates velocity to reflect the resolved movement. It returns a bool indicating whether a collision occurred.
What is the difference between _process and _physics_process in GDScript?
_process(delta) runs every visual frame and its rate varies with FPS. _physics_process(delta) runs at a fixed rate (default 60 Hz) and is synchronized with the physics engine. Always use _physics_process for movement and collision code so behavior is consistent regardless of frame rate.
How do I get smooth diagonal movement in Godot 4?
Use Input.get_vector("left", "right", "up", "down") which returns a clamped Vector2 (length ≤ 1.0). For keyboard input, diagonals are normalized to length 1. For analog sticks, it preserves partial magnitudes (half-push = 0.5 length). This prevents diagonal movement from being ~1.41x faster than cardinal movement.
Should I multiply velocity by delta?
No! velocity is already "per second" and move_and_slide() handles timing internally. DO multiply delta when adding to velocity (gravity, acceleration). DON'T multiply velocity itself by delta before move_and_slide().
Why does my character slide on slopes?
CharacterBody2D has floor_snap_length and floor_max_angle properties. Increase floor_snap_length to stick to slopes better, or set floor_stop_on_slope = true to prevent sliding when standing still.
What's the difference between move_and_slide() and move_and_collide()?
move_and_slide() handles sliding along surfaces automatically and updates velocity. move_and_collide() stops at the first collision and returns collision info, giving you full control over the response. Use move_and_slide() for characters; use move_and_collide() when you need custom collision handling.
How do I do knockback without breaking player control?
Add knockback to velocity, then let normal movement code run. The knockback will naturally decay as the player regains control: velocity += knockback_direction * knockback_force. For stronger knockback, temporarily disable input or use a state machine.
I'm using _physics_process but still see jitter on my 144Hz monitor?
Enable Physics Interpolation in Project Settings → Physics → Common. This smooths visual positions between physics ticks. Available in Godot 4.3+. Without it, objects only update visually at 60Hz even on faster displays.
How do I stop my character from flying off slopes?
Set floor_snap_length to a value like 8.0 in _ready(). This snaps the character to the floor within that distance, preventing the "flying off downward slopes" problem. Also set floor_stop_on_slope = true to prevent sliding when standing still.
Should I use _process for movement?
If you're using CharacterBody2D + move_and_slide(), keep movement in _physics_process. move_and_slide() is designed to work with the physics step. If you're moving a plain Node2D with no physics (just position += direction * speed), _process(delta) can be fine—but then you must multiply motion by delta yourself.
What is motion_mode on CharacterBody2D?
CharacterBody2D has two motion modes: MOTION_MODE_GROUNDED (default, for platformers—has floor/ceiling/wall concepts and slope behavior) and MOTION_MODE_FLOATING (for top-down—no floor logic, all collisions are walls, constant slide speed). Set motion_mode = MOTION_MODE_FLOATING in _ready() for top-down games.
Why is is_on_floor() always returning false?
is_on_floor() (and is_on_wall(), is_on_ceiling()) only returns valid values AFTER calling move_and_slide(). If you check it before move_and_slide() runs, it shows the result from the previous frame. Always structure your code so move_and_slide() runs first, then check floor state.
GDScript Movement Syntax Quick Reference
extends CharacterBody2D
@export var speed: float = 300.0
func _ready() -> void:
motion_mode = CharacterBody2D.MOTION_MODE_FLOATING
func _physics_process(_delta: float) -> void:
var direction := Input.get_vector("left", "right", "up", "down")
velocity = direction * speed
move_and_slide()extends CharacterBody2D
@export var speed: float = 200.0
@export var jump_force: float = -400.0
var gravity := float(ProjectSettings.get_setting("physics/2d/default_gravity"))
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_force
var direction := Input.get_axis("left", "right")
velocity.x = direction * speed
move_and_slide()extends CharacterBody2D
@export var max_speed: float = 300.0
@export var acceleration: float = 1500.0
@export var friction: float = 1000.0
func _physics_process(delta: float) -> void:
var direction := Input.get_vector("left", "right", "up", "down")
if direction != Vector2.ZERO:
velocity = velocity.move_toward(direction * max_speed, acceleration * delta)
else:
velocity = velocity.move_toward(Vector2.ZERO, friction * delta)
move_and_slide()GDScript Movement Sample Exercises
Build a direction Vector2 by reading two axes: horizontal (ui_left/ui_right) and vertical (ui_up/ui_down). Store the result in a variable called direction.
var direction = Vector2(Input.get_axis("ui_left", "ui_right"), Input.get_axis("ui_up", "ui_down"))Fill in the blank to complete the physics frame callback.
_physics_processFill in the blank to read a horizontal axis from left/right input.
get_axis+ 21 more exercises