- 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.
- NEVER confuse collision_layer with collision_mask
- — Layer = "What AM I?", Mask = "What do I DETECT?". Setting both to the same value is almost always wrong.
- NEVER multiply velocity by delta when using move_and_slide()
- — move_and_slide() automatically includes timestep in calculations. Only multiply gravity (acceleration) by delta.
- NEVER forget to call force_raycast_update() for manual raycasts
- — Raycasts update once per physics frame. If you change target_position/rotation mid-frame, you MUST call force_raycast_update().
- NEVER use get_overlapping_bodies() every frame
- — Cache results with body_entered/body_exited signals instead. Continuous queries are expensive and unnecessary.
- 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. 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
Reference Master Skill: godot-master