Skill v1.0.1
Automated scan100/10026 files
version: "1.0.1" 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_exitedsignals instead [16]. - NEVER modify `RigidBody2D` state directly in `_process` — Use
_integrate_forces()for safe, synchronized access toPhysicsDirectBodyState2D[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
PhysicsServer2Dto 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()orShapeCast2Dfor 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()orPhysicsServer2Dinstead. - 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.
collision_setup.gd
Programmatic layer/mask management with named layer constants and debug visualization.
physics_query_cache.gd
Frame-based caching for PhysicsDirectSpaceState2D queries - eliminates redundant expensive queries.
custom_physics.gd
Custom physics integration patterns for CharacterBody2D. Covers non-standard gravity, forces, and manual stepping. Use for non-standard physics behavior.
physics_queries.gd
PhysicsDirectSpaceState2D query patterns for raycasting, point queries, and shape queries. Use for line-of-sight, ground detection, or area scanning.
physics_server_swarm.gd
Low-level PhysicsServer2D usage for thousands of moving objects. Bypasses node overhead for massive performance gains in bullet hells or swarms.
substepping_logic.gd
Manual physics sub-stepping for high-velocity projectiles. Ensures frame-perfect collision for objects moving faster than the physics tick.
safe_rigidbody_state.gd
Thread-safe RigidBody2D modification using _integrate_forces. Ideal for teleporting bodies or applying custom impulses without jitter.
physics_direct_query.gd
Lighweight environment sensing using PhysicsDirectSpaceState2D. Performs ray queries without the overhead of RayCast2D nodes.
collision_bitmask_helper.gd
Clean architectural pattern for managing complex collision layers/masks using bitwise Enums and helpers.
raycast_vision_stack.gd
Optimized multicasting vision system for AI. Reuses a single RayCast2D to check multiple angles in one physics frame.
shapecast_aoe.gd
Robust AOE detection using ShapeCast2D. Provides instant collision information without the signal-lag of Area2D.
custom_gravity_override.gd
Logic for localized gravity zones (Water, Space, Wind) and manual character-weight simulation.
collision_debouncer.gd
Expert pattern for preventing signal spam when multi-shape bodies enter triggers.
jitter_interpolation_fix.gd
Standard configuration and runtime adjustments to ensure smooth character movement on high-refresh-rate monitors.
physics_server_direct_body.gd
Direct PhysicsServer2D RID management for peak performance in massive physics simulations.
move_and_collide_precision.gd
Expert bounce and friction logic implementation for precision-critical movement.
continuous_collision_detection.gd
Advanced CCD management for preventing bullet tunneling at extremely high velocities.
performance_batch_mover.gd
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 clarityfunc setup_player_collision() -> void:# I am layer 1set_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 mathfunc enable_layers(base_layer: int, count: int) -> void:var mask := 0for i in range(count):mask |= (1 << (base_layer + i - 1))collision_mask = mask# ❌ BAD: Hardcoded bitmasks without documentationcollision_mask = 0b110110 # What does this mean?!
Common Patterns
# Pattern: Projectile that hits enemies but ignores other projectiles# projectile.gdextends Area2Dfunc _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.gdextends StaticBody2D@export var one_way := truefunc _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 shapesextends Area2Dfunc _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 Setextends Area2Dvar _active_bodies := {} # Use dict as Setfunc _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] = trueprint("First entrance!") # Fires oncefunc _on_body_exited(body: Node2D) -> void:_active_bodies.erase(body)
Damage-Over-Time with Immunity Frames
# lava_zone.gdextends Area2D@export var damage_per_tick := 5@export var tick_rate := 0.5 # Damage every 0.5svar _damage_timers := {} # body -> time_until_next_tickfunc _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 tickfunc _on_body_exited(body: Node2D) -> void:_damage_timers.erase(body)func _process(delta: float) -> void:for body in _damage_timers.keys():_damage_timers[body] -= deltaif _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 playerextends CharacterBody2D@onready var vision_ray: RayCast2D = $VisionRayfunc can_see_target(target: Node2D) -> bool:var direction := global_position.direction_to(target.global_position)vision_ray.target_position = direction * 300 # 300px rangevision_ray.force_raycast_update() # CRITICAL: Update mid-frameif vision_ray.is_colliding():return vision_ray.get_collider() == targetreturn false
Multipa Raycasts for Ledge Detection
# platformer_controller.gdextends CharacterBody2D@onready var floor_front: RayCast2D = $FloorCheckFront@onready var floor_back: RayCast2D = $FloorCheckBackfunc 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 ledgesvelocity.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 bodyfunc get_body_at_mouse() -> Node2D:var mouse_pos := get_global_mouse_position()var space := get_world_2d().direct_space_statevar query := PhysicsPointQueryParameters2D.new()query.position = mouse_posquery.collide_with_areas = falsequery.collision_mask = 0b11111111 # All layersvar results := space.intersect_point(query, 1) # Max 1 resultif results.is_empty():return nullreturn results[0].collider
Shape Cast: AOE Attack
# AOE damage in circle around playerfunc damage_nearby_enemies(center: Vector2, radius: float, damage: int) -> void:var space := get_world_2d().direct_space_statevar query := PhysicsShapeQueryParameters2D.new()var circle := CircleShape2D.new()circle.radius = radiusquery.shape = circlequery.transform = Transform2D(0.0, center)query.collision_mask = 0b0010 # Layer 2: Enemiesvar hits := space.intersect_shape(query)for hit in hits:var enemy: Node2D = hit.colliderif 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_statevar query := PhysicsRayQueryParameters2D.create(from, from + direction * max_range)query.exclude = [self]query.collision_mask = 0b0010 # Enemiesvar result := space.intersect_ray(query)if result:var hit_enemy: Node2D = result.collidervar hit_point: Vector2 = result.positionspawn_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 framefunc _ready() -> void:await get_tree().physics_frameif $RayCast2D.is_colliding():print("Hit something")
Area2D Not Detecting CharacterBody2D
# Problem: CharacterBody2D has collision_layer = 0 by default# Solution: Explicitly set layer# character.gdfunc _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 neededfunc _ready() -> void:$OptionalRaycast.enabled = falsefunc check_vision() -> void:$OptionalRaycast.enabled = true$OptionalRaycast.force_raycast_update()var sees_player := $OptionalRaycast.is_colliding()$OptionalRaycast.enabled = falsereturn 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: RIDvar _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: KinematicCollision2Dfunc update_debug_info(collision: KinematicCollision2D) -> void:_last_collision = collisionqueue_redraw()func _draw() -> void:if not _last_collision: returnvar 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
- Master Skill: godot-master