Skip to content

M2.1 — Player Movement with CharacterBody2D

What you'll learn

  • Use CharacterBody2D (not RigidBody2D) for a code-driven character, with a CollisionShape2D.
  • Move in _physics_process with Input.get_vector, a speed constant, and move_and_slide.
  • Keep motion frame-rate independent — the first felt payoff, a character you can walk around.

How it applies

  • Movement feel is the genre's handshake. Before loot or stats, the player judges an ARPG by how it feels to move. Frame-rate-independent motion (via _physics_process and delta) means the character covers the same ground per second on a 60 Hz laptop and a 144 Hz desktop — without it, the game is literally faster on better hardware, which speedrunners exploit and testers can't reproduce across machines.
  • Diagonal speed is a real bug. Naively adding X and Y input makes diagonal movement ~1.41× faster than cardinal. Input.get_vector normalizes, so the player can't gain speed by walking diagonally — a fairness property in a genre about kiting enemies.
  • The collision body is the contract with the world. CharacterBody2D + a collision shape is what lets walls stop the player and, in M3, what enemies' attacks register against. Choosing the right body type now avoids fighting the physics engine later.
  • Determinism for QA. A fixed physics step means "hold right for 1 second" travels a repeatable distance every run — a property a tester can assert. Tie movement to the variable frame loop and that assertion drifts per machine.

Concepts

Which body type, and why

Godot 2D has three movable body archetypes. The choice is not stylistic:

  • RigidBody2D — fully physics-simulated (forces, mass, bounce). You nudge it; the engine decides where it ends up. Great for crates and ragdolls, wrong for a character you want under precise control — it will drift, spin, and slide in ways a player reads as "floaty."
  • Area2D / bare Node2D — no collision response at all. The character would walk through walls.
  • CharacterBody2D — built for the case where your code decides the velocity each frame and the engine only resolves collisions and slides along surfaces. This is the standard choice for a player or enemy you steer directly.

Use CharacterBody2D. You will set its velocity and call move_and_slide(); the engine handles the "stop at the wall, slide along it" part.

_physics_process and delta

Godot calls two per-step callbacks:

  • _process(delta) runs once per rendered framevariable rate (60, 144, or whatever the monitor and load produce). Right for animation and UI.
  • _physics_process(delta) runs at a fixed rate (default 60 Hz) — the same delta every step. Right for movement and anything physics-touching.

Put movement in _physics_process. The delta it passes is the seconds per step; multiplying motion by delta makes distance-per-second independent of how often the callback fires. move_and_slide already integrates velocity over the physics step internally, so you scale velocity by speed (a per-second value) and let move_and_slide apply the step — you do not multiply velocity by delta yourself when using move_and_slide. (You would multiply by delta if you were moving position by hand.)

Reading direction and applying it

The movement loop is three lines of intent:

  1. Get a normalized direction from the four move_* actions with Input.get_vector.
  2. Set velocity to that direction times a speed constant (pixels per second).
  3. Call move_and_slide().

We build the script incrementally.

Example

Start with the class and a tunable speed. Attach this to the player root (a CharacterBody2D):

extends CharacterBody2D

## Movement speed in pixels per second. A tunable; 120-180 feels right for a top-down ARPG.
@export var speed: float = 150.0

@export surfaces speed in the Inspector, so a designer or tester can tune it without editing code — the same "one knob, no recompile" idea the genre uses everywhere.

Example

Add the movement step. Input.get_vector returns Vector2.ZERO when nothing is held, so a released key naturally stops the body (velocity becomes zero):

func _physics_process(_delta: float) -> void:
    var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity = direction * speed
    move_and_slide()

The _delta is prefixed with _ because this body does not use it directly — move_and_slide consumes the physics step internally. Renaming it _delta documents "intentionally unused" and silences the editor's unused-parameter hint.

Why the underscore, and why no manual delta

A frequent early mistake is velocity = direction * speed * delta and move_and_slide(). That double-counts the step: move_and_slide already advances by the physics step, so multiplying velocity by delta first makes the character crawl at 1/60 of the intended speed. Rule: with move_and_slide, velocity is a per-second value; do not pre-multiply by delta.

The collision shape

A CharacterBody2D with no CollisionShape2D has nothing to collide with: it will pass through walls and, later, nothing can register hits against its body. Add a CollisionShape2D child with a shape sized to the character (a CapsuleShape2D or RectangleShape2D roughly covering the sprite's feet/body is typical for top-down). The shape is the body's physical footprint, distinct from the visual sprite (M2.2) and from the hurtbox you add in M3.

Walkthrough

You will build the player scene and walk it around the empty arena from M1.4.

  1. Scene → New Scene → Other Node → CharacterBody2D. Rename the root Player. Save as res://scenes/actor/player.tscn.
  2. With Player selected, add child CollisionShape2D. In the Inspector, set its Shape to a new CapsuleShape2D (or RectangleShape2D); size it to roughly cover where the character will stand.
  3. Add a temporary visual so you can see the body move: add a child Sprite2D, and in the Inspector set its Texture to Godot's built-in icon.svg (or any placeholder). M2.2 replaces this with real animation.
  4. Attach a script to Player: right-click PlayerAttach Script, save as res://scripts/player.gd. Write it incrementally — the extends/@export block, then the _physics_process body shown above. Do not paste a finished file; type the pieces.
  5. Put the player in the world: open res://scenes/world/main.tscn, select the World node, and from the FileSystem dock drag player.tscn onto it. The Player is now a child of World.
  6. Press F5. Use WASD (your M1.3 actions) to walk the placeholder around. Release the keys; the body stops. Walk into the window's edge — nothing stops you yet, because there are no walls (M8 adds the arena). The point of this chapter is that the body obeys input at a constant speed.

Optional sanity check

In the Inspector, change Player's speed from 150 to 400 while the game is not running, relaunch, and confirm the character is faster — proof the @export knob works. Then walk diagonally and confirm it is not faster than walking straight (proof get_vector normalized). Reset speed to a value that feels right.

Self-check quiz

Q1 — Why does movement go in _physics_process rather than _process?

A. _process cannot read the Input singleton. B. _physics_process runs at a fixed rate, so movement is frame-rate independent and reproducible across machines; _process runs at the variable render rate. C. move_and_slide only exists inside _process. D. _physics_process is faster.

Reveal answer

B. The fixed step is the whole reason: the same delta every call means "hold right for a second" covers the same distance on any monitor, and physics interactions resolve consistently. A is false (Input is global). C is false (move_and_slide is a CharacterBody2D method, callable from _physics_process, which is exactly where it belongs). D is not the reason — it's about correctness, not speed.

Q2 — A learner writes velocity = direction * speed * delta and then calls move_and_slide(). The character crawls. Why?

A. speed is too low. B. move_and_slide already advances by the physics step, so multiplying velocity by delta applies the step twice — the body moves at roughly 1/60 of the intended speed. C. direction is not normalized. D. delta is negative on the first frame.

Reveal answer

B. With move_and_slide, velocity is a per-second value the method integrates over the step; pre-multiplying by delta double-counts it. The fix is velocity = direction * speed. A is unlikely to produce a precise crawl. C would affect diagonal speed, not overall crawl. D is false — delta is the positive fixed step.

Q3 — Why use CharacterBody2D instead of RigidBody2D for the player?

A. RigidBody2D cannot collide with walls. B. CharacterBody2D lets your code set velocity directly and only asks the engine to resolve collisions/slide, giving precise control; RigidBody2D is force-simulated and feels floaty for a directly-steered character. C. RigidBody2D does not exist in 2D. D. CharacterBody2D renders sprites and RigidBody2D does not.

Reveal answer

B. The distinction is who decides the motion: with CharacterBody2D your code does, and the engine only resolves collisions; with RigidBody2D the physics solver does, which drifts and slides in ways players read as loss of control. A is false (rigid bodies collide). C is false. D is false (neither renders; a child Sprite2D does).

Integration question

Q4 — open

You now have a CharacterBody2D reading the move_* actions from M1.3 and living under the World node from M1.4. Trace the chain from a W keypress to the character moving up, naming the M1 decision each link depends on. Then explain why the placeholder Sprite2D you added is the wrong long-term approach and what M2.2 replaces it with.

Reveal expected answer

The chain: pressing W is read as the move_up action (M1.3's InputMap), which get_vector turns into a Vector2 pointing up (−Y, because M1.1's 2D space has +Y downward); that direction times speed becomes velocity, and move_and_slide advances the CharacterBody2D at the fixed _physics_process rate; the body is a child of World (M1.4), so when the camera arrives in M2.3 the player will scroll with the world rather than being pinned like the UI. The placeholder Sprite2D with a static icon is wrong long-term because a character needs to show idle vs running and which way it faces; M2.2 replaces the single static texture with an AnimatedSprite2D (or AnimationPlayer) driven by the same velocity the movement code produces, so the visuals reflect the motion rather than ignoring it.