| name | godot-2d-physics |
| description | Expert patterns for Godot 2D physics including collision layers/masks, Area2D triggers, raycasting, and PhysicsDirectSpaceState2D queries. Use when implementing collision detection, trigger zones, line-of-sight systems, or manual physics queries. Trigger keywords: CollisionShape2D, CollisionPolygon2D, collision_layer, collision_mask, set_collision_layer_value, set_collision_mask_value, Area2D, body_entered, body_exited, RayCast2D, force_raycast_update, PhysicsPointQueryParameters2D, PhysicsShapeQueryParameters2D, direct_space_state, move_and_collide, move_and_slide. |
2D Physics
Expert guidance for collision detection, triggers, and raycasting in Godot 2D.
NEVER Do
- NEVER scale
CollisionShape2D nodes — Use the shape handles in the editor, NOT the Node2D scale property. Scaling causes unpredictable physics behavior and incorrect collision normals [12].
- NEVER confuse
collision_layer with collision_mask — Layer = "What AM I?", Mask = "What do I DETECT?". Setting both to the same value is usually wrong [13].
- NEVER multiply velocity by delta when using
move_and_slide() — move_and_slide() automatically includes timestep. Only multiply gravity/acceleration by delta [14].
- NEVER forget
force_raycast_update() for manual mid-frame raycasts — Raycasts update once per physics frame. If you change target_position, you MUST force an update [15].
- NEVER use
get_overlapping_bodies() every frame — It is expensive. Cache results with body_entered/body_exited signals instead [16].
- NEVER modify
RigidBody2D state directly in _process — Use _integrate_forces() for safe, synchronized access to PhysicsDirectBodyState2D [17, 411].
- NEVER move
PhysicsBody2D nodes in _process() — Use _physics_process(). Moving bodies outside the physics step causes stutter and unreliable collision detection.
- NEVER use
RigidBody2D for 1000+ simple entities — Use PhysicsServer2D to bypass node overhead for massive performance gains (Swarms/Bullets) [18, 397].
- NEVER use
Area2D for high-frequency blocking (Bullets) — Area signals can be delayed. Use move_and_collide() or ShapeCast2D for frame-perfect results [19].
- NEVER ignore 'Physics Jitter' on high-refresh monitors — Enable Physics Interpolation to prevent micro-stutter in motion [21, 400].
- NEVER scale collision shapes directly at runtime — It causes major instability. Resize the shape resource (size/radius) instead.
- NEVER use
set_deferred for immediate physics transform logic — It happens at the end of the frame. Use force_raycast_update() or PhysicsServer2D instead.
- NEVER leave Continuous CD (CCD) enabled for slow objects — It adds significant CPU overhead. Reserve it for high-speed projectiles to prevent tunneling.
- NEVER use a single collision layer for all tiles/entities — Separate layers (Ground, Walls, Enemies) to allow selective filtering via masks.
- NEVER forget to free
PhysicsServer2D RIDs manually — They are not garbage collected and will leak memory permanently.
Available Scripts
MANDATORY: Read the script matching your use case before implementation.
Programmatic layer/mask management with named layer constants and debug visualization.
Frame-based caching for PhysicsDirectSpaceState2D queries - eliminates redundant expensive queries.
Custom physics integration patterns for CharacterBody2D. Covers non-standard gravity, forces, and manual stepping. Use for non-standard physics behavior.
PhysicsDirectSpaceState2D query patterns for raycasting, point queries, and shape queries. Use for line-of-sight, ground detection, or area scanning.
Low-level PhysicsServer2D usage for thousands of moving objects. Bypasses node overhead for massive performance gains in bullet hells or swarms.
Manual physics sub-stepping for high-velocity projectiles. Ensures frame-perfect collision for objects moving faster than the physics tick.
Thread-safe RigidBody2D modification using _integrate_forces. Ideal for teleporting bodies or applying custom impulses without jitter.
Lighweight environment sensing using PhysicsDirectSpaceState2D. Performs ray queries without the overhead of RayCast2D nodes.
Clean architectural pattern for managing complex collision layers/masks using bitwise Enums and helpers.
Optimized multicasting vision system for AI. Reuses a single RayCast2D to check multiple angles in one physics frame.
Robust AOE detection using ShapeCast2D. Provides instant collision information without the signal-lag of Area2D.
Logic for localized gravity zones (Water, Space, Wind) and manual character-weight simulation.
Expert pattern for preventing signal spam when multi-shape bodies enter triggers.
Standard configuration and runtime adjustments to ensure smooth character movement on high-refresh-rate monitors.
Direct PhysicsServer2D RID management for peak performance in massive physics simulations.
Expert bounce and friction logic implementation for precision-critical movement.
Advanced CCD management for preventing bullet tunneling at extremely high velocities.
Optimized batch movement for multiple static/animatable bodies using riders-aware logic.
Collision Layers & Masks (Bitmask Deep Dive)
The Mental Model
# collision_layer (32 bits): What broadcast channels am I transmitting on?
# collision_mask (32 bits): What broadcast channels am I listening to?
# Example: Player vs Enemy
# Player:
# layer = 0b0001 (Channel 1: "I am a player")
# mask = 0b0110 (Channels 2+3: "I listen for enemies and walls")
# Enemy:
# layer = 0b0010 (Channel 2: "I am an enemy")
# mask = 0b0101 (Channels 1+3: "I listen for players and walls")
Bitmask Helpers
# ✅ GOOD: Use helper functions for clarity
func setup_player_collision() -> void:
# I am layer 1
set_collision_layer_value(1, true)
# I detect layers 2 (enemies) and 3 (world)
set_collision_mask_value(2, true)
set_collision_mask_value(3, true)
# ✅ GOOD: Bit shift for programmatic layer math
func enable_layers(base_layer: int, count: int) -> void:
var mask := 0
for i in range(count):
mask |= (1 << (base_layer + i - 1))
collision_mask = mask
# ❌ BAD: Hardcoded bitmasks without documentation
collision_mask = 0b110110 # What does this mean?!
Common Patterns
# Pattern: Projectile that hits enemies but ignores other projectiles
# projectile.gd
extends Area2D
func _ready() -> void:
set_collision_layer_value(4, true) # Layer 4: "Projectiles"
set_collision_mask_value(2, true) # Mask Layer 2: "Enemies"
# Result: Projectiles don't collide with each other
# Pattern: One-way platform (player can jump through from below)
# platform.gd
extends StaticBody2D
@export var one_way := true
func _ready() -> void:
set_collision_layer_value(3, true) # Layer 3: "World"
if one_way:
# Use Area2D + collision exemption instead
# (Standard one-way platforms use different technique)
pass
Area2D Expert Patterns
Problem: Duplicate Triggers on Multi-CollisionShape
# ❌ BAD: body_entered fires MULTIPLE times if Area2D has multiple shapes
extends Area2D
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
print("Entered!") # Fires 3x if Area has 3 CollisionShapes!
# ✅ GOOD: Track unique bodies with Set
extends Area2D
var _active_bodies := {} # Use dict as Set
func _ready() -> void:
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node2D) -> void:
if body not in _active_bodies:
_active_bodies[body] = true
print("First entrance!") # Fires once
func _on_body_exited(body: Node2D) -> void:
_active_bodies.erase(body)
Damage-Over-Time with Immunity Frames
# lava_zone.gd
extends Area2D
@export var damage_per_tick := 5
@export var tick_rate := 0.5 # Damage every 0.5s
var _damage_timers := {} # body -> time_until_next_tick
func _ready() -> void:
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node2D) -> void:
if body.has_method("take_damage"):
_damage_timers[body] = 0.0 # Immediate first tick
func _on_body_exited(body: Node2D) -> void:
_damage_timers.erase(body)
func _process(delta: float) -> void:
for body in _damage_timers.keys():
_damage_timers[body] -= delta
if _damage_timers[body] <= 0.0:
body.take_damage(damage_per_tick)
_damage_timers[body] = tick_rate
RayCast2D Advanced Usage
Dynamic Raycast Rotation
# enemy_vision.gd - Enemy looks toward player
extends CharacterBody2D
@onready var vision_ray: RayCast2D = $VisionRay
func can_see_target(target: Node2D) -> bool:
var direction := global_position.direction_to(target.global_position)
vision_ray.target_position = direction * 300 # 300px range
vision_ray.force_raycast_update() # CRITICAL: Update mid-frame
if vision_ray.is_colliding():
return vision_ray.get_collider() == target
return false
Multipa Raycasts for Ledge Detection
# platformer_controller.gd
extends CharacterBody2D
@onready var floor_front: RayCast2D = $FloorCheckFront
@onready var floor_back: RayCast2D = $FloorCheckBack
func at_ledge() -> bool:
return floor_front.is_colliding() and not floor_back.is_colliding()
func _physics_process(delta: float) -> void:
if at_ledge() and is_on_floor():
# Enemy AI: Turn around at ledges
velocity.x *= -1
Raycast Exclusions
# Ignore specific bodies (e.g., self)
func _ready() -> void:
$RayCast2D.add_exception(self)
$RayCast2D.add_exception($Weapon) # Ignore attached weapon collider
# Reset exclusions
$RayCast2D.clear_exceptions()
PhysicsDirectSpaceState2D (Manual Queries)
Point Query: Click Detection
# Check if mouse click hits any physics body
func get_body_at_mouse() -> Node2D:
var mouse_pos := get_global_mouse_position()
var space := get_world_2d().direct_space_state
var query := PhysicsPointQueryParameters2D.new()
query.position = mouse_pos
query.collide_with_areas = false
query.collision_mask = 0b11111111 # All layers
var results := space.intersect_point(query, 1) # Max 1 result
if results.is_empty():
return null
return results[0].collider
Shape Cast: AOE Attack
# AOE damage in circle around player
func damage_nearby_enemies(center: Vector2, radius: float, damage: int) -> void:
var space := get_world_2d().direct_space_state
var query := PhysicsShapeQueryParameters2D.new()
var circle := CircleShape2D.new()
circle.radius = radius
query.shape = circle
query.transform = Transform2D(0.0, center)
query.collision_mask = 0b0010 # Layer 2: Enemies
var hits := space.intersect_shape(query)
for hit in hits:
var enemy: Node2D = hit.collider
if enemy.has_method("take_damage"):
enemy.take_damage(damage)
Ray Cast: Instant Hit Weapon
# Hitscan weapon (no projectile)
func fire_hitscan_weapon(from: Vector2, direction: Vector2, max_range: float) -> void:
var space := get_world_2d().direct_space_state
var query := PhysicsRayQueryParameters2D.create(from, from + direction * max_range)
query.exclude = [self]
query.collision_mask = 0b0010 # Enemies
var result := space.intersect_ray(query)
if result:
var hit_enemy: Node2D = result.collider
var hit_point: Vector2 = result.position
spawn_hit_effect(hit_point)
if hit_enemy.has_method("take_damage"):
hit_enemy.take_damage(25)
Decision Tree: Collision Detection Methods
| Use Case | Method | Why |
|---|
| Continuous trigger zone | Area2D + signals | Memory of what's inside, signals are efficient |
| One-time pickup (coin) | Area2D + queue_free() on enter | Simple, automatic cleanup |
| Line-of-sight check | RayCast2D | Efficient, built-in |
| Click-to-select units | PhysicsPointQueryParameters2D | Single query, no permanent node |
| AOE spell | PhysicsShapeQueryParameters2D | One-shot query, flexible shape |
| Instant-hit weapon | PhysicsRayQueryParameters2D | Hitscan, no projectile physics |
| Platformer ground check | RayCast2D or raycast down | Precise ledge detection |
Edge Cases
Collision During _ready()
# ❌ BAD: Raycasts don't work in _ready() (physics not initialized)
func _ready() -> void:
if $RayCast2D.is_colliding(): # Always false!
print("Hit something")
# ✅ GOOD: Wait for physics frame
func _ready() -> void:
await get_tree().physics_frame
if $RayCast2D.is_colliding():
print("Hit something")
Area2D Not Detecting CharacterBody2D
# Problem: CharacterBody2D has collision_layer = 0 by default
# Solution: Explicitly set layer
# character.gd
func _ready() -> void:
collision_layer = 0b0001 # Layer 1: Player
Raycast Hitting Backfaces
# Raycasts hit both front and back of collision shapes
# To raycast one-way (front only), use Area2D monitoring
Performance
# ✅ GOOD: Disable raycasts when not needed
func _ready() -> void:
$OptionalRaycast.enabled = false
func check_vision() -> void:
$OptionalRaycast.enabled = true
$OptionalRaycast.force_raycast_update()
var sees_player := $OptionalRaycast.is_colliding()
$OptionalRaycast.enabled = false
return sees_player
# ❌ BAD: Always-on raycasts for rarely-used checks
# Leave RayCast2D.enabled = true for vision checks once per second
Expert Techniques & Optimizations
1. Physics-Server-Batching (Low-Level Swarms)
For massive simulations (e.g., thousands of projectiles), avoid the overhead of the SceneTree by using PhysicsServer2D directly. This allows you to batch movement and collision updates in a single loop, significantly reducing CPU usage by bypassing node-based lifecycle overhead.
class_name PhysicsBatchManager extends Node
## Manages thousands of physics bodies directly via PhysicsServer2D.
var _bodies: Array[RID] = []
func create_bullet_swarm(count: int) -> void:
for i in range(count):
var body := PhysicsServer2D.body_create()
PhysicsServer2D.body_set_mode(body, PhysicsServer2D.BODY_MODE_KINEMATIC)
PhysicsServer2D.body_set_space(body, get_world_2d().space)
_bodies.append(body)
func _physics_process(_delta: float) -> void:
# Batch update all body transforms.
for body in _bodies:
var current_transform := PhysicsServer2D.body_get_state(body, PhysicsServer2D.BODY_STATE_TRANSFORM)
var next_transform := current_transform.translated(Vector2.RIGHT * 5.0)
PhysicsServer2D.body_set_state(body, PhysicsServer2D.BODY_STATE_TRANSFORM, next_transform)
2. Multi-Shape-Sync (Compound RID Bodies)
A single physics body can consist of multiple shapes (e.g., a shield and a character). To sync these shapes dynamically without creating multiple nodes, use PhysicsServer2D.body_add_shape(). This is ideal for characters with dynamic equipment or vehicles with complex, non-uniform collision volumes.
class_name CompoundBodySync extends Node2D
## Synchronizes multiple shapes within a single low-level physics body.
var _body: RID
var _shapes: Array[RID] = []
func _ready() -> void:
_body = PhysicsServer2D.body_create()
# Add multiple collision shapes to the same body RID.
var circle := PhysicsServer2D.circle_shape_create()
PhysicsServer2D.shape_set_data(circle, 20.0)
PhysicsServer2D.body_add_shape(_body, circle, Transform2D.IDENTITY)
_shapes.append(circle)
var box := PhysicsServer2D.rectangle_shape_create()
PhysicsServer2D.shape_set_data(box, Vector2(10, 50))
PhysicsServer2D.body_add_shape(_body, box, Transform2D.IDENTITY.translated(Vector2(30, 0)))
_shapes.append(box)
3. Collision-Visual-Debugger (Runtime Gizmos)
Professional debugging requires real-time visualization of collision data that isn't visible via standard debug options. Use CanvasItem._draw() to render contact points and normals extracted from KinematicCollision2D or the physics space state.
class_name CollisionVisualDebugger extends Node2D
## Renders collision normals and hit points for real-time physics debugging.
var _last_collision: KinematicCollision2D
func update_debug_info(collision: KinematicCollision2D) -> void:
_last_collision = collision
queue_redraw()
func _draw() -> void:
if not _last_collision: return
var hit_pos := to_local(_last_collision.get_position())
var normal := _last_collision.get_normal()
# Draw hit point and normal vector.
draw_circle(hit_pos, 5.0, Color.RED)
draw_line(hit_pos, hit_pos + normal * 30.0, Color.GREEN, 2.0)
Reference
Related