godot-2d-physics

安装量: 70
排名: #10984

安装

npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-2d-physics
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

返回排行榜