Skip to content

Godot Physics & Signals


1. 物理系统概览

Godot 4 内置两套物理引擎:

引擎 说明
Godot Physics 默认引擎,纯 GDScript/C++ 实现,跨平台稳定
Jolt Physics 可选插件,性能更强,适合大型 3D 场景

物理世界由 PhysicsServer3D / PhysicsServer2D 在底层管理。每个物理帧(默认 60Hz)独立于渲染帧运行,通过 _physics_process(delta) 回调暴露给脚本。

渲染帧  _process(delta)          ← 用于视觉/UI/动画插值
物理帧  _physics_process(delta)  ← 用于移动/力/碰撞检测

原则:所有涉及物理体移动、力、碰撞检测的逻辑必须写在 _physics_process 中,否则行为不稳定。


2. 物理节点类型

Node
└── CollisionObject2D / CollisionObject3D   ← 所有物理体的基类
    ├── Area2D / Area3D                     ← 检测区域,不参与物理模拟
    ├── PhysicsBody2D / PhysicsBody3D
    │   ├── StaticBody                      ← 不移动的碰撞体(地形、墙壁)
    │   ├── AnimatableBody                  ← 可被脚本/动画移动的静态体
    │   ├── RigidBody                       ← 全物理模拟(重力、力、碰撞响应)
    │   └── CharacterBody                   ← 玩家/NPC,手动控制移动
    └── VehicleBody3D                       ← 车辆专用(3D)

3. 碰撞形状与层级

3.1 CollisionShape

每个物理体必须至少挂载一个 CollisionShape2DCollisionShape3D 子节点。

常用 Shape:

Shape 用途
BoxShape3D 箱体,性能最优
SphereShape3D 球体,旋转无影响
CapsuleShape3D 角色胶囊体
ConcavePolygonShape3D 复杂静态地形(只能用于 StaticBody)
ConvexPolygonShape3D 复杂动态体(凸包)

3.2 碰撞层与掩码

collision_layer  ← 该物体"属于"哪些层(我是谁)
collision_mask   ← 该物体"检测"哪些层(我看谁)
# 设置角色在第1层,检测第2、3层
$Player.collision_layer = 1        # 0b0001
$Player.collision_mask  = 0b0110   # 第2层 + 第3层

两个物体发生碰撞的条件:A 的 mask 包含 B 的 layer, B 的 mask 包含 A 的 layer。


4. RigidBody3D / RigidBody2D

RigidBody 完全由物理引擎驱动,支持重力、冲量、力矩。

4.1 运动模式

# 通过 freeze_mode 和 freeze 属性控制
body.freeze = false                          # 默认:参与物理
body.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC  # 冻结后可手动控制位置
physics_material_override 属性 说明
friction 摩擦力
bounce 弹性
rough / absorbent 是否覆盖对方参数

4.2 施加力与冲量

func _physics_process(_delta):
    # 持续力(每帧累加)
    apply_force(Vector3(10, 0, 0))

    # 一次性冲量(用于跳跃、爆炸)
    apply_impulse(Vector3(0, 500, 0))

    # 扭矩(旋转)
    apply_torque(Vector3(0, 1, 0))

4.3 重要信号

# 碰撞发生时(需在 Inspector 中启用 contact_monitor 并设置 max_contacts_reported)
body.body_entered.connect(_on_body_entered)
body.body_exited.connect(_on_body_exited)

func _on_body_entered(body: Node):
    print("碰撞到: ", body.name)

4.4 _integrate_forces — 精确控制

func _integrate_forces(state: PhysicsDirectBodyState3D):
    # state 包含当前速度、接触点、质量等
    var velocity = state.linear_velocity
    # 直接设置速度(绕过力的累加)
    state.linear_velocity = Vector3(5, velocity.y, 0)

5. CharacterBody3D / CharacterBody2D

CharacterBody 是做角色控制器的首选,不受物理引擎自动模拟,由脚本完全控制。

5.1 move_and_slide

extends CharacterBody3D

const SPEED = 5.0
const JUMP_VELOCITY = 4.5

func _physics_process(delta):
    # 添加重力
    if not is_on_floor():
        velocity += get_gravity() * delta

    # 跳跃
    if Input.is_action_just_pressed("ui_accept") and is_on_floor():
        velocity.y = JUMP_VELOCITY

    # 水平移动
    var dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    velocity.x = dir.x * SPEED
    velocity.z = dir.y * SPEED

    move_and_slide()

5.2 move_and_collide(底层)

func _physics_process(delta):
    var collision = move_and_collide(velocity * delta)
    if collision:
        # 反弹
        velocity = velocity.bounce(collision.get_normal())
方法 适用场景
move_and_slide() 角色、NPC,自动处理坡面滑动
move_and_collide() 弹球、自定义碰撞响应

5.3 常用状态查询

is_on_floor()       # 是否站在地面
is_on_ceiling()     # 是否顶到天花板
is_on_wall()        # 是否贴墙
get_floor_normal()  # 地面法线(用于坡度计算)
get_last_slide_collision()  # 最后一次碰撞信息

6. Area3D / Area2D

Area 不参与物理碰撞响应,只做重叠检测,常用于触发器、伤害区域、拾取范围。

6.1 基本用法

extends Area3D

func _ready():
    body_entered.connect(_on_body_entered)
    body_exited.connect(_on_body_exited)
    area_entered.connect(_on_area_entered)

func _on_body_entered(body: Node3D):
    if body.is_in_group("Player"):
        print("玩家进入区域")

func _on_body_exited(body: Node3D):
    print("物体离开区域")

6.2 重力与阻尼覆盖

Area 可以覆盖区域内 RigidBody 的重力方向和大小:

# Inspector 中设置,或脚本:
$WaterArea.gravity = 2.0
$WaterArea.gravity_direction = Vector3(0, -1, 0)
$WaterArea.linear_damp = 3.0   # 模拟水的阻力

6.3 检测 Area 内的物体(主动查询)

func get_enemies_in_range() -> Array:
    return get_overlapping_bodies().filter(
        func(b): return b.is_in_group("Enemy")
    )

7. StaticBody 与 AnimatableBody

StaticBody

完全静止,消耗最小,用于地形、墙壁、平台。

extends StaticBody3D
# 通常不需要脚本,直接在编辑器摆放

AnimatableBody

可以被 AnimationPlayer 或脚本移动位置,会把运动速度传递给碰到的 RigidBody(如传送带)。

extends AnimatableBody3D

func _physics_process(delta):
    # 直接修改 position,引擎会计算速度并推动碰到的物体
    position.x += 2.0 * delta

8. 物理查询:Raycast 与 ShapeCast

8.1 RayCast 节点

@onready var ray = $RayCast3D

func _physics_process(_delta):
    if ray.is_colliding():
        var point  = ray.get_collision_point()
        var normal = ray.get_collision_normal()
        var obj    = ray.get_collider()
        print("打到: ", obj.name, " 法线: ", normal)

8.2 即时 Raycast(不需要节点)

func cast_ray(from: Vector3, to: Vector3):
    var space = get_world_3d().direct_space_state
    var query = PhysicsRayQueryParameters3D.create(from, to)
    query.exclude = [self]           # 排除自身
    query.collision_mask = 0b0010    # 只检测第2层

    var result = space.intersect_ray(query)
    if result:
        print("命中: ", result["collider"].name)
        print("位置: ", result["position"])

8.3 ShapeCast(形状扫描)

# 用 SphereShape3D 扫描前方
var query = PhysicsShapeQueryParameters3D.new()
query.shape = SphereShape3D.new()
query.shape.radius = 0.5
query.transform = global_transform
query.motion = Vector3(0, 0, -5)

var results = space.cast_motion(query)
# results[0] = 未碰撞的安全比例, results[1] = 碰撞时的比例

9. Signals 信号系统概览

Godot 的信号(Signal)是观察者模式的官方实现,用于节点间解耦通信。

发射方 (Emitter)  →  emit_signal()  →  接收方 (Listener) 的回调函数

为什么用信号而不是直接调用函数?

直接调用 信号
发射方需要持有接收方引用 发射方不需要知道谁在监听
强耦合 松耦合,易于扩展
多个接收方需要手动遍历 一次 emit 通知所有连接者

10. 内置信号与连接方式

10.1 编辑器连接(推荐用于简单情况)

在 Inspector 的 Node > Signals 面板中,双击信号 → 选择目标节点 → 自动生成回调函数。

10.2 代码连接

# 基本语法
signal_source.some_signal.connect(callable)

# 示例:按钮点击
$Button.pressed.connect(_on_button_pressed)

func _on_button_pressed():
    print("按钮被按下")

10.3 Lambda 连接

$Timer.timeout.connect(func(): print("时间到!"))

10.4 断开连接

$Button.pressed.disconnect(_on_button_pressed)

# 检查是否已连接
if $Button.pressed.is_connected(_on_button_pressed):
    $Button.pressed.disconnect(_on_button_pressed)

10.5 连接标志(Flags)

# CONNECT_ONE_SHOT:触发一次后自动断开
$Area.body_entered.connect(_on_enter, CONNECT_ONE_SHOT)

# CONNECT_DEFERRED:延迟到帧末执行(物理回调中修改场景树时必须用)
$Body.body_entered.connect(_on_enter, CONNECT_DEFERRED)

11. 自定义信号

11.1 声明与发射

extends Node

# 无参数信号
signal player_died

# 带参数信号
signal health_changed(old_value: int, new_value: int)

# 带参数发射
func take_damage(amount: int):
    var old_hp = health
    health -= amount
    health_changed.emit(old_hp, health)  # 发射信号

    if health <= 0:
        player_died.emit()

11.2 接收自定义信号

extends Node

func _ready():
    var player = $Player
    player.health_changed.connect(_on_health_changed)
    player.player_died.connect(_on_player_died)

func _on_health_changed(old_val: int, new_val: int):
    $HUD.update_health_bar(new_val)
    print("血量: %d -> %d" % [old_val, new_val])

func _on_player_died():
    get_tree().change_scene_to_file("res://scenes/game_over.tscn")

11.3 await 等待信号(协程风格)

func interact_with_door():
    $Door.open()
    await $Door.animation_finished   # 暂停直到信号发出
    print("门已打开,继续执行")
    $Player.move_through_door()

12. 信号与物理的综合实战

实战一:拾取物品系统

# pickup_item.gd
extends Area3D

signal item_collected(item_name: String, value: int)

@export var item_name: String = "Gold Coin"
@export var value: int = 10

func _ready():
    body_entered.connect(_on_body_entered)

func _on_body_entered(body: Node3D):
    if body.is_in_group("Player"):
        item_collected.emit(item_name, value)
        queue_free()
# player.gd
extends CharacterBody3D

var score: int = 0
signal score_changed(new_score: int)

func _ready():
    # 动态连接场景中所有拾取物
    get_tree().get_nodes_in_group("Pickups").map(func(p):
        p.item_collected.connect(_on_item_collected)
    )

func _on_item_collected(item_name: String, value: int):
    score += value
    print("拾取: ", item_name)
    score_changed.emit(score)

实战二:爆炸范围伤害

# explosion.gd
extends Node3D

signal exploded(position: Vector3, radius: float)

@export var radius: float = 5.0
@export var damage: float = 100.0

func explode():
    # 球形查询
    var space = get_world_3d().direct_space_state
    var params = PhysicsShapeQueryParameters3D.new()
    params.shape = SphereShape3D.new()
    params.shape.radius = radius
    params.transform = global_transform
    params.collision_mask = 0b0011  # 检测层1和层2

    var hits = space.intersect_shape(params)
    for hit in hits:
        var body = hit["collider"]
        if body.has_method("take_damage"):
            # 距离衰减
            var dist = global_position.distance_to(body.global_position)
            var falloff = 1.0 - clamp(dist / radius, 0.0, 1.0)
            body.take_damage(damage * falloff)

    exploded.emit(global_position, radius)

    # 播放效果后销毁
    await $AnimationPlayer.animation_finished
    queue_free()

实战三:平台触发器

# moving_platform_trigger.gd
extends Area3D

@onready var platform: AnimatableBody3D = $"../MovingPlatform"

func _ready():
    body_entered.connect(func(_b): platform.set_process(true))
    body_exited.connect(func(_b):
        if get_overlapping_bodies().is_empty():
            platform.set_process(false)
    )

13. 常见陷阱与最佳实践

陷阱 1:在物理回调中直接修改场景树

# 错误:可能导致崩溃
func _on_body_entered(body):
    body.queue_free()  # 有时安全,有时不安全

# 正确:使用 CONNECT_DEFERRED 或 call_deferred
func _on_body_entered(body):
    body.call_deferred("queue_free")

# 或在连接时加 flag
area.body_entered.connect(_on_enter, CONNECT_DEFERRED)

陷阱 2:重复连接信号

# 错误:每次 _ready 调用都连接,导致多次触发
func _ready():
    $Button.pressed.connect(_on_pressed)  # 如果节点被重新实例化会重复连接

# 正确:连接前检查
func _ready():
    if not $Button.pressed.is_connected(_on_pressed):
        $Button.pressed.connect(_on_pressed)

陷阱 3:在 _process 中做物理查询

# 错误:_process 与物理帧不同步,结果不稳定
func _process(delta):
    var result = space.intersect_ray(query)  # 避免

# 正确
func _physics_process(delta):
    var result = space.intersect_ray(query)  # 物理帧中查询

陷阱 4:CharacterBody 忘记添加重力

# 常见 bug:角色浮空不落地
func _physics_process(delta):
    # 忘记这两行
    if not is_on_floor():
        velocity += get_gravity() * delta
    move_and_slide()

最佳实践总结

实践 说明
物理逻辑放 _physics_process 确保与物理帧同步
用信号解耦节点通信 避免父节点持有子节点引用
用碰撞层过滤不必要检测 减少 CPU 开销
简单形状优先 Box/Sphere > Capsule > Convex > Concave
场景树操作用 call_deferred 防止物理回调中的崩溃
Area 做触发,Body 做碰撞 职责清晰,性能更好
信号参数用强类型注解 提升可读性,便于 IDE 提示

参考资料