godot-2d-physics

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

2D 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
undefined
gdscript
undefined

collision_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:"我监听玩家和墙体")

undefined
undefined

Bitmask Helpers

位掩码辅助工具

gdscript
undefined
gdscript
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?!
undefined
collision_mask = 0b110110 # 这代表什么?!
undefined

Common Patterns

常见模式

gdscript
undefined
gdscript
undefined

Pattern: 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
undefined
gdscript
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)
undefined
extends 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)
undefined

Damage-Over-Time with Immunity Frames

带无敌帧的持续伤害

gdscript
undefined
gdscript
undefined

lava_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
undefined
gdscript
undefined

enemy_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 false
undefined
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范围 vision_ray.force_raycast_update() # 关键:帧中更新
if vision_ray.is_colliding():
    return vision_ray.get_collider() == target
return false
undefined

Multipa Raycasts for Ledge Detection

多射线检测用于边缘检测

gdscript
undefined
gdscript
undefined

platformer_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
undefined
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(): # 敌人AI:在边缘处转身 velocity.x *= -1
undefined

Raycast Exclusions

射线检测排除

gdscript
undefined
gdscript
undefined

Ignore 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
undefined
gdscript
undefined

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
undefined
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  # 所有层

var results := space.intersect_point(query, 1)  # 最多1个结果
if results.is_empty():
    return null
return results[0].collider
undefined

Shape Cast: AOE Attack

形状投射:范围攻击

gdscript
undefined
gdscript
undefined

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)
undefined
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  # 层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)
undefined

Ray Cast: Instant Hit Weapon

射线投射:即时命中武器

gdscript
undefined
gdscript
undefined

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)

---
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 CaseMethodWhy
Continuous trigger zoneArea2D + signalsMemory of what's inside, signals are efficient
One-time pickup (coin)Area2D + queue_free() on enterSimple, automatic cleanup
Line-of-sight checkRayCast2DEfficient, built-in
Click-to-select unitsPhysicsPointQueryParameters2DSingle query, no permanent node
AOE spellPhysicsShapeQueryParameters2DOne-shot query, flexible shape
Instant-hit weaponPhysicsRayQueryParameters2DHitscan, no projectile physics
Platformer ground checkRayCast2D or raycast downPrecise ledge detection

用场景方法原因
持续触发区域Area2D + 信号记录区域内的物体,信号效率高
一次性拾取物(硬币)Area2D + 进入时调用queue_free()简单,自动清理
视线检测RayCast2D高效,内置功能
点击选择单位PhysicsPointQueryParameters2D单次查询,无需永久节点
范围法术PhysicsShapeQueryParameters2D一次性查询,形状灵活
即时命中武器PhysicsRayQueryParameters2D命中扫描,无需投射物物理
平台游戏地面检测RayCast2D或向下射线检测精确的边缘检测

Edge Cases

边缘情况

Collision During _ready()

_ready()期间的碰撞

gdscript
undefined
gdscript
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")
undefined
func _ready() -> void: await get_tree().physics_frame if $RayCast2D.is_colliding(): print("命中物体")
undefined

Area2D Not Detecting CharacterBody2D

Area2D未检测到CharacterBody2D

gdscript
undefined
gdscript
undefined

Problem: 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
undefined
func _ready() -> void: collision_layer = 0b0001 # 层1:玩家
undefined

Raycast Hitting Backfaces

射线检测命中背面

gdscript
undefined
gdscript
undefined

Raycasts hit both front and back of collision shapes

射线检测会命中碰撞形状的正面和背面

To raycast one-way (front only), use Area2D monitoring

要实现单向射线检测(仅正面),使用Area2D监控


---

---

Performance

性能

gdscript
undefined
gdscript
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即可

undefined
undefined

Reference

参考

  • Master Skill: godot-master
  • 大师技能:godot-master