Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. GDScript
  3. GDScript Movement Practice
GDScript24 exercises

GDScript Movement Practice

Copy-paste movement scripts for Godot 4: top-down + platformer. Learn velocity, move_and_slide(), _physics_process vs _process, delta rules, slope snapping, and Physics Interpolation jitter fixes.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Define the lifecycle callback that runs at a fixed rate for physics.

On this page
  1. 1Quick Fixes
  2. 2Copy-Paste Starter Scripts
  3. Standard Top-Down Movement
  4. Standard Platformer Movement
  5. 3_process vs _physics_process: The #1 Cause of Stuttering
  6. 4Physics Interpolation (High Refresh Jitter Fix)
  7. 5Input.get_vector vs Manual Input Checks
  8. 6The Delta Rule
  9. 7Floor Snapping (Platformers)
  10. 8Debugging Movement
  11. 9Why is_on_floor() Is Always False
  12. 10move_and_slide Returns Bool (Godot 3 vs 4)
  13. 11Advanced Recipes
  14. Coyote Time + Jump Buffer
  15. Dash with Cooldown
Quick FixesCopy-Paste Starter Scripts_process vs _physics_process: The #1 Cause of StutteringPhysics Interpolation (High Refresh Jitter Fix)Input.get_vector vs Manual Input ChecksThe Delta RuleFloor Snapping (Platformers)Debugging MovementWhy is_on_floor() Is Always Falsemove_and_slide Returns Bool (Godot 3 vs 4)Advanced Recipes

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.

Related GDScript Topics
GDScript FoundationsGDScript Vector MathGDScript Exports

  • Diagonal speed too fast → use Input.get_vector() (returns length ≤ 1, already clamped)
  • Move stutters / inconsistent → use _physics_process for move_and_slide()
  • Micro-jitter on high refresh → enable Physics Interpolation (Project Settings → Physics → Common)
  • Slope "flying off" → set floor_snap_length (+ optional floor_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

Ready to practice?

Start practicing GDScript Movement with spaced repetition

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()

All movement and collision code belongs in _physics_process. Using _process causes stuttering and inconsistent behavior across frame rates.

This is the most common movement bug in Godot:

FunctionRunsUse 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() returns a clamped Vector2 (length ≤ 1), fixing diagonal speed automatically. Use it instead of manual get_axis pairs plus normalize.

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 already "pixels per second"—never multiply it by delta before move_and_slide(). Only multiply delta when adding to velocity (gravity, acceleration, friction).

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:

  1. print(velocity) to see actual values
  2. Confirm you're in _physics_process, not _process
  3. Check is_on_floor() / is_on_wall() after move_and_slide()
  4. 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

Prompt

Check Your Understanding: Why does move_and_slide() take no arguments in Godot 4, and how does velocity work on CharacterBody2D?

What a strong answer looks like

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

func _physics_process(delta)Input.get_vector / Input.get_axis for directionSetting velocity propertymove_and_slide() callmotion_mode for top-down vs platformer

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

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()
Platformer with gravity and jump
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()
Acceleration and friction
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

Example 1Difficulty: 2/5

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"))
Example 2Difficulty: 1/5

Fill in the blank to complete the physics frame callback.

_physics_process
Example 3Difficulty: 2/5

Fill in the blank to read a horizontal axis from left/right input.

get_axis

+ 21 more exercises

Quick Reference
GDScript Movement Cheat Sheet →

Copy-ready syntax examples for quick lookup

Practice in Build a Game

Arena Survivor: Part 1: Your First 2D GameBuild a GameRoguelike: Part 1: Into the CaveBuild a Game

Further Reading

  • GDScript Dictionary map() and map_in_place12 min read
  • Facade Pattern in Godot 4 GDScript: Taming "End Turn" Spaghetti12 min read

Start practicing GDScript Movement

Free daily exercises with spaced repetition. No credit card required.

← Back to GDScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.