godot-2d-physics
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese2D Physics
2D物理系统
Expert guidance for collision detection, triggers, and raycasting in Godot 2D.
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.
- 切勿缩放CollisionShape2D节点 — 使用编辑器中的形状控制柄,而非Node2D的缩放属性。缩放会导致不可预测的物理行为和错误的碰撞法线。
- 切勿混淆collision_layer与collision_mask — Layer = "我是什么?",Mask = "我能检测什么?"。将两者设置为相同值几乎都是错误的做法。
- 使用move_and_slide()时切勿将速度乘以delta — move_and_slide()会自动在计算中包含时间步长。仅需将重力(加速度)乘以delta即可。
- 手动射线检测时切勿忘记调用force_raycast_update() — 射线检测每物理帧更新一次。如果在帧中更改target_position/rotation,必须调用force_raycast_update()。
- 切勿每帧调用get_overlapping_bodies() — 改用body_entered/body_exited信号缓存结果。持续查询开销大且无必要。
Available Scripts
可用脚本
MANDATORY: Read the script matching your use case before implementation.
强制要求:在实现前阅读与你的用例匹配的脚本。
collision_matrix.gd
collision_matrix.gd
Programmatic layer/mask management with named layer constants and debug visualization.
通过命名层常量和调试可视化实现程序化层/掩码管理。
physics_query_cache.gd
physics_query_cache.gd
Frame-based caching for PhysicsDirectSpaceState2D queries - eliminates redundant expensive queries.
针对PhysicsDirectSpaceState2D查询的基于帧的缓存 - 消除冗余的高开销查询。
custom_physics.gd
custom_physics.gd
Custom physics integration patterns for CharacterBody2D. Covers non-standard gravity, forces, and manual stepping. Use for non-standard physics behavior.
CharacterBody2D的自定义物理集成模式。涵盖非标准重力、作用力和手动步进。适用于实现非标准物理行为。
physics_queries.gd
physics_queries.gd
PhysicsDirectSpaceState2D query patterns for raycasting, point queries, and shape queries. Use for line-of-sight, ground detection, or area scanning.
PhysicsDirectSpaceState2D查询模式,包括射线检测、点查询和形状查询。适用于视线检测、地面检测或区域扫描。
Collision Layers & Masks (Bitmask Deep Dive)
碰撞层与掩码(位掩码深入解析)
The Mental Model
心智模型
gdscript
undefinedgdscript
undefinedcollision_layer (32 bits): What broadcast channels am I transmitting on?
collision_layer (32位):我在哪些广播频道发送信号?
collision_mask (32 bits): What broadcast channels am I listening to?
collision_mask (32位):我监听哪些广播频道?
Example: Player vs Enemy
示例:玩家 vs 敌人
Player:
玩家:
layer = 0b0001 (Channel 1: "I am a player")
layer = 0b0001 (频道1:"我是玩家")
mask = 0b0110 (Channels 2+3: "I listen for enemies and walls")
mask = 0b0110 (频道2+3:"我监听敌人和墙体")
Enemy:
敌人:
layer = 0b0010 (Channel 2: "I am an enemy")
layer = 0b0010 (频道2:"我是敌人")
mask = 0b0101 (Channels 1+3: "I listen for players and walls")
mask = 0b0101 (频道1+3:"我监听玩家和墙体")
undefinedundefinedBitmask Helpers
位掩码辅助工具
gdscript
undefinedgdscript
undefined✅ 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)func setup_player_collision() -> void:
# 我属于层1
set_collision_layer_value(1, true)
# 我检测层2(敌人)和层3(世界)
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
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?!
undefinedcollision_mask = 0b110110 # 这代表什么?!
undefinedCommon Patterns
常见模式
gdscript
undefinedgdscript
undefinedPattern: Projectile that hits enemies but ignores other projectiles
模式:击中敌人但忽略其他投射物的投射物
projectile.gd
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
extends Area2D
func _ready() -> void:
set_collision_layer_value(4, true) # 层4:"投射物"
set_collision_mask_value(2, true) # 掩码层2:"敌人"
# 结果:投射物之间不会发生碰撞
Pattern: One-way platform (player can jump through from below)
模式:单向平台(玩家可从下方跳跃穿过)
platform.gd
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
---extends StaticBody2D
@export var one_way := true
func _ready() -> void:
set_collision_layer_value(3, true) # 层3:"世界"
if one_way:
# 使用Area2D + 碰撞豁免替代
# (标准单向平台使用不同技术)
pass
---Area2D Expert Patterns
Area2D专家模式
Problem: Duplicate Triggers on Multi-CollisionShape
问题:多CollisionShape时触发重复信号
gdscript
undefinedgdscript
undefined❌ BAD: body_entered fires MULTIPLE times if Area2D has multiple shapes
❌ 不推荐:如果Area2D有多个形状,body_entered会触发多次
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!
extends Area2D
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
print("进入!") # 如果Area有3个CollisionShapes,会触发3次!
✅ GOOD: Track unique bodies with Set
✅ 推荐:使用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)
undefinedextends Area2D
var _active_bodies := {} # 使用字典作为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("首次进入!") # 仅触发一次
func _on_body_exited(body: Node2D) -> void:
_active_bodies.erase(body)
undefinedDamage-Over-Time with Immunity Frames
带无敌帧的持续伤害
gdscript
undefinedgdscript
undefinedlava_zone.gd
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
---extends Area2D
@export var damage_per_tick := 5
@export var tick_rate := 0.5 # 每0.5秒造成一次伤害
var _damage_timers := {} # body -> 下次伤害倒计时
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 # 立即造成首次伤害
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
RayCast2D高级用法
Dynamic Raycast Rotation
动态射线检测旋转
gdscript
undefinedgdscript
undefinedenemy_vision.gd - Enemy looks toward player
enemy_vision.gd - 敌人朝向玩家
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 falseundefinedextends 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范围
vision_ray.force_raycast_update() # 关键:帧中更新
if vision_ray.is_colliding():
return vision_ray.get_collider() == target
return falseundefinedMultipa Raycasts for Ledge Detection
多射线检测用于边缘检测
gdscript
undefinedgdscript
undefinedplatformer_controller.gd
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
undefinedextends 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():
# 敌人AI:在边缘处转身
velocity.x *= -1
undefinedRaycast Exclusions
射线检测排除
gdscript
undefinedgdscript
undefinedIgnore specific bodies (e.g., self)
忽略特定物体(例如自身)
func _ready() -> void:
$RayCast2D.add_exception(self)
$RayCast2D.add_exception($Weapon) # Ignore attached weapon collider
func _ready() -> void:
$RayCast2D.add_exception(self)
$RayCast2D.add_exception($Weapon) # 忽略附加的武器碰撞体
Reset exclusions
重置排除列表
$RayCast2D.clear_exceptions()
---$RayCast2D.clear_exceptions()
---PhysicsDirectSpaceState2D (Manual Queries)
PhysicsDirectSpaceState2D(手动查询)
Point Query: Click Detection
点查询:点击检测
gdscript
undefinedgdscript
undefinedCheck 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].colliderundefinedfunc 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 # 所有层
var results := space.intersect_point(query, 1) # 最多1个结果
if results.is_empty():
return null
return results[0].colliderundefinedShape Cast: AOE Attack
形状投射:范围攻击
gdscript
undefinedgdscript
undefinedAOE 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)undefinedfunc 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 # 层2:敌人
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)undefinedRay Cast: Instant Hit Weapon
射线投射:即时命中武器
gdscript
undefinedgdscript
undefinedHitscan 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)
---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 # 敌人
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 |
| 用场景 | 方法 | 原因 |
|---|---|---|
| 持续触发区域 | Area2D + 信号 | 记录区域内的物体,信号效率高 |
| 一次性拾取物(硬币) | Area2D + 进入时调用queue_free() | 简单,自动清理 |
| 视线检测 | RayCast2D | 高效,内置功能 |
| 点击选择单位 | PhysicsPointQueryParameters2D | 单次查询,无需永久节点 |
| 范围法术 | PhysicsShapeQueryParameters2D | 一次性查询,形状灵活 |
| 即时命中武器 | PhysicsRayQueryParameters2D | 命中扫描,无需投射物物理 |
| 平台游戏地面检测 | RayCast2D或向下射线检测 | 精确的边缘检测 |
Edge Cases
边缘情况
Collision During _ready()
_ready()期间的碰撞
gdscript
undefinedgdscript
undefined❌ BAD: Raycasts don't work in _ready() (physics not initialized)
❌ 不推荐:_ready()中射线检测无效(物理未初始化)
func _ready() -> void:
if $RayCast2D.is_colliding(): # Always false!
print("Hit something")
func _ready() -> void:
if $RayCast2D.is_colliding(): # 始终返回false!
print("命中物体")
✅ GOOD: Wait for physics frame
✅ 推荐:等待物理帧
func _ready() -> void:
await get_tree().physics_frame
if $RayCast2D.is_colliding():
print("Hit something")
undefinedfunc _ready() -> void:
await get_tree().physics_frame
if $RayCast2D.is_colliding():
print("命中物体")
undefinedArea2D Not Detecting CharacterBody2D
Area2D未检测到CharacterBody2D
gdscript
undefinedgdscript
undefinedProblem: CharacterBody2D has collision_layer = 0 by default
问题:CharacterBody2D默认collision_layer = 0
Solution: Explicitly set layer
解决方案:显式设置层
character.gd
character.gd
func _ready() -> void:
collision_layer = 0b0001 # Layer 1: Player
undefinedfunc _ready() -> void:
collision_layer = 0b0001 # 层1:玩家
undefinedRaycast Hitting Backfaces
射线检测命中背面
gdscript
undefinedgdscript
undefinedRaycasts hit both front and back of collision shapes
射线检测会命中碰撞形状的正面和背面
To raycast one-way (front only), use Area2D monitoring
要实现单向射线检测(仅正面),使用Area2D监控
---
---Performance
性能
gdscript
undefinedgdscript
undefined✅ 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
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
对于每秒一次的视线检测,保持RayCast2D.enabled = true即可
undefinedundefinedReference
参考
- Master Skill: godot-master
- 大师技能:godot-master