Skip to content

G2.7 — Physics Bodies & Areas

What you'll learn

  • Move a CharacterBody2D by setting velocity and calling move_and_slide in _physics_process.
  • Give bodies and areas a CollisionShape2D, without which they detect nothing.
  • Detect overlaps with Area2D and its body_entered / area_entered signals.
  • Configure collision layers ("what I am on") and masks ("what I scan for"), and the asymmetry between them.

How it applies

  • No collision shape means no collision. A body or area with no CollisionShape2D child has nothing to collide with or detect — the "my walls don't stop the player" and "my hitbox never registers" bugs almost always trace here first.
  • Layer/mask is the single most common 2D detection bug. Detection is asymmetric: A senses B only if A's mask includes a layer B is on. Get the direction backwards and overlaps silently never fire — no error, just nothing happening. Understanding "I am on" versus "I look for" is the fix.
  • Moving by position defeats collision. Teleporting a body by writing position skips the physics solver, so it tunnels through walls; velocity + move_and_slide lets the engine resolve collisions and slide along surfaces.
  • The layer matrix is an interaction test surface. Which pairs can interact (player vs enemy hitbox, pickup vs player, enemy vs world) is a matrix; naming and partitioning it deliberately keeps the interaction space small and testable, the same instinct as an interaction test matrix in QA.

Concepts

Body types, briefly

Godot has three 2D body types; pick by who drives the motion:

  • StaticBody2D — does not move (walls, floors, level geometry).
  • RigidBody2D — driven by the physics simulation (forces, gravity, bouncing). You influence it with forces, not by setting position.
  • CharacterBody2D — driven by your code. You compute a velocity and let a helper resolve collisions. This is what player and enemy characters use.

For a code-controlled character, CharacterBody2D is the answer; the others appear as level geometry and simulated objects respectively.

CharacterBody2D: velocity and move_and_slide

You move a CharacterBody2D by setting its velocity (pixels per second, a Vector2) and calling move_and_slide() each physics frame:

extends CharacterBody2D

@export var speed := 200.0

func _physics_process(_delta: float) -> void:
    var dir := Input.get_vector("left", "right", "up", "down")   # normalized (G2.5)
    velocity = dir * speed
    move_and_slide()        # engine moves the body and resolves collisions this frame

move_and_slide consumes velocity, moves the body, and slides along anything it hits instead of stopping dead — and it already accounts for the physics step, so you do not multiply velocity by delta when using it (the helper handles the timing). Note what not to do: setting position directly skips collision resolution and lets the body pass through walls.

CollisionShape2D: the shape is mandatory

A body or area is invisible to the physics engine until it has a CollisionShape2D child with an actual shape (a rectangle, circle, or capsule resource) assigned:

Player (CharacterBody2D)
└── CollisionShape2D        # assign a shape resource, or it collides with nothing

The most common "collision does nothing" cause is a missing or empty CollisionShape2D. The shape is a child node so it transforms with the body (G2.2) — which is also why scaling the parent scales the shape (a G2.2 footgun worth recalling).

Area2D: detecting overlap

An Area2D detects overlaps without resolving them — bodies and other areas pass through it, and it reports the overlap via signals (G2.6):

func _ready() -> void:
    $Area2D.body_entered.connect(_on_body_entered)
    $Area2D.area_entered.connect(_on_area_entered)

func _on_body_entered(body: Node2D) -> void:
    # a physics body overlapped this area
    ...

body_entered(body) fires when a physics body overlaps; area_entered(area) when another area does. The payload is what entered. Area2D is the basis for the mechanic behind hitboxes, hurtboxes, pickups, and detection ranges — the game books build their component architecture on it (ARPG M3); here you learn the area and its signals themselves.

Collision layers and masks

Every body and area has two bit-sets that decide who interacts with whom:

  • layer — the layers this node is on ("what I am").
  • mask — the layers this node scans for ("what I look for").

The rule is asymmetric: node A detects node B only if A's mask includes a layer B is on. B's own mask is irrelevant to whether A detects B. So a player-hitbox detects an enemy-hurtbox when the hitbox's mask includes the enemy-hurtbox's layer. Naming the layers (Project Settings → Layer Names → 2D Physics) keeps this legible: world, player, enemy, player_hitbox, enemy_hurtbox, and so on.

Example

The asymmetry, stated as the rule to memorize:

A node senses another only if its mask includes a layer the other is on.

Set a pickup's layer to pickup and the player's mask to include pickup, and the player detects the pickup. If you instead set the player's layer to pickup and expect detection, nothing happens — layer is "what I am," not "what I sense." This one-directional rule is the most common 2D detection bug, and stating it as mask-includes-the-other's-layer is how you avoid wiring it backwards.

Walkthrough

  1. Build a CharacterBody2D Player with a Sprite2D and a CollisionShape2D (assign a rectangle shape). In its script, set velocity from Input.get_vector(...) × speed and call move_and_slide in _physics_process. Add a StaticBody2D wall with its own CollisionShape2D. Run and confirm the player slides along the wall instead of passing through.
  2. Remove the player's CollisionShape2D (or clear its shape) and run again — it now passes through the wall. Restore the shape. That is "no shape, no collision."
  3. Add an Area2D "pickup" with a CollisionShape2D, and connect its body_entered to a handler that prints and queue_frees the pickup. Walk the player into it and confirm the overlap fires.
  4. Layers/masks: in Project Settings, name layers player and pickup. Put the player body on layer player; put the pickup area on layer pickup and set the player's mask (or the area's mask, as the detector) to include the other's layer. Misconfigure it deliberately (detector's mask missing the other's layer) and watch detection stop with no error; then fix it.

Optional sanity check

Try moving the player with position += dir * speed * delta instead of velocity + move_and_slide. It moves, but walks straight through the StaticBody2D wall, because writing position skips the physics solver. Switch back to move_and_slide and the wall stops it. That contrast is why code-driven characters use move_and_slide, not direct position writes.

Self-check quiz

Q1 — Your CharacterBody2D passes straight through walls. Which is NOT a plausible cause?

A. The body has no CollisionShape2D (or its shape is empty). B. You move it with position += instead of velocity + move_and_slide. C. The wall StaticBody2D has no collision shape. D. The body's modulate color is wrong.

Reveal answer

D. modulate is a visual tint (G2.3) and has nothing to do with collision. A, B, and C are all real causes: no shape means nothing to collide with (on either the body or the wall), and writing position directly bypasses the solver so no collision is resolved. The fix space is shapes + move_and_slide, not color.

Q2 — A player-hitbox should detect an enemy-hurtbox. The hurtbox is on layer enemy_hurtbox. What must be true?

A. The hurtbox's mask must include player_hitbox. B. The hitbox's mask must include enemy_hurtbox (the layer the hurtbox is on). C. Both must be on the same layer. D. Neither needs a mask; layers alone suffice.

Reveal answer

B. Detection is asymmetric: the detector (the hitbox) senses the other only if the detector's mask includes a layer the other is on. A points the rule at the wrong node's mask. C would make them share identity, not establish detection. D is false — the mask is exactly what defines what a node scans for.

Q3 — When should a node be an Area2D rather than a CharacterBody2D?

A. When it should detect overlaps but not physically block or be blocked (hitboxes, pickups, triggers). B. Whenever it moves. C. Never; Area2D is deprecated. D. Only for UI.

Reveal answer

A. Area2D detects overlap and reports it via signals without resolving collision, which is exactly what hitboxes, pickups, and triggers want; CharacterBody2D is for a code-driven body that should collide and slide. B is unrelated (areas and bodies can both move). C is false. D confuses physics areas with UI.

Integration question

Q4 — open

You are building the mechanics a melee hit needs (the ARPG keeps the hitbox/hurtbox architecture; you supply the parts). The player is a CharacterBody2D that should slide along walls; an attack should be an Area2D that detects enemies but does not physically shove them; and only the player's attack should hit enemies, not other players or the world. Describe the nodes and shapes involved, how the player moves, how the hit is detected, and how layers/masks are set so the attack hits only enemy hurtboxes — and name the most likely silent failure and how you would catch it.

Reveal expected answer

Nodes/shapes: the player is a CharacterBody2D with a CollisionShape2D (for body collision against the world) and a child Area2D "hitbox" with its own CollisionShape2D (for hit detection); enemies have a body plus an Area2D "hurtbox" with a shape. Every body and area needs a shape or it does nothing. Movement: the player sets velocity from input (G2.5) and calls move_and_slide in _physics_process, so it slides along the world StaticBody2D walls rather than tunneling — not by writing position. Detection: the hitbox Area2D reports overlaps via area_entered(area); the handler reads the entered area as the enemy's hurtbox (using as + a null guard, L2.1) and applies the hit. Layers/masks: name layers world, player, enemy_hurtbox, player_hitbox. Put the hurtbox on enemy_hurtbox; set the player hitbox's mask to include enemy_hurtbox and not player or world, so the attack senses only enemy hurtboxes — the asymmetric "mask includes the other's layer" rule. Most likely silent failure: the mask wired backwards (hitbox on the right layer but its mask missing enemy_hurtbox, or the layer/mask roles swapped), so area_entered never fires and the attack does nothing with no error. Catch it by printing in the handler (or watching the Output) and, if silent, checking the layer/mask matrix against the rule — detection requires the detector's mask to include the target's layer. The mechanics here (body + move_and_slide, area + signals, layer/mask) are exactly what the game books' component architecture is assembled from.