godot-2d-physics
0
总安装量
3
周安装量
安装命令
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-2d-physics
Agent 安装分布
opencode
3
gemini-cli
3
codex
3
claude-code
2
github-copilot
2
Skill 文档
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_matrix.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