Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

物理介绍

在游戏开发中,你经常需要知道游戏中的两个对象在何时相交或接触。这被称为碰撞检测。检测到碰撞时,你通常希望某些事情发生。这被称为碰撞响应

Godot 在 2D 和 3D 中提供了许多碰撞对象,以提供碰撞检测和响应。你可能很难决定哪个适合你的项目。如果了解了每种方法的工作原理以及它们的优缺点,你就可以避免出现问题并简化开发过程。

在本指南中,你将学会:

  • Godot 的四种碰撞对象类型

  • 每种碰撞对象的工作原理

  • 什么时候、为什么一种类型好过另一种类型

备注

本文档的示例将使用 2D 对象。每个 2D 物理对象和碰撞形状在 3D 中都有直接对应的对象,并且在大多数情况下它们的工作方式相同。

警告

无论使用哪种物理引擎,Godot 中的物理模拟都 不是 确定性的。物理引擎的确定性本质非常复杂,并且受到许多因素的影响。这意味着,即使是在看似完全相同的情况下,物理模拟的运行结果也无法保证每次都一模一样。

碰撞对象

Godot 提供了四种碰撞对象,它们都继承自 CollisionObject2D。以下列出的最后三种是物理体,它们还继承自 PhysicsBody2D

  • Area2D

    Area2D 节点提供检测影响。它们可以检测物体何时重叠,并在物体进入或离开时发出信号。Area2D 也可用于覆盖物理属性,例如一定区域内的重力或阻尼。

  • StaticBody2D

    静态体是物理引擎不会移动的物体。它参与碰撞检测,但不会因响应碰撞而移动。它们通常用于属于环境的一部分的对象或不需要任何动态行为的对象。

  • RigidBody2D

    这是实现 2D 物理模拟的节点。你不应该直接控制 RigidBody2D,而是对它施加力(重力、冲量等),让物理引擎计算由此产生的运动。在此阅读更多关于使用刚体的信息。

  • CharacterBody2D

    提供碰撞检测的物体,但没有物理模拟。所有移动和碰撞响应必须用代码实现。

物理材质

静态体和刚体可以配置为使用物理材质。这允许调整物体的摩擦和弹性,并设置是否具有吸收性和/或粗糙性。

碰撞形状

物理体可以包含任意数量的 Shape2D 对象作为子节点。这些形状用于定义物体的碰撞边界并检测与其他物体的接触。

备注

为了检测碰撞,必须至少为对象分配一个 Shape2D

分配形状的最常用方法是添加 CollisionShape2DCollisionPolygon2D 作为对象的子节点。这些节点允许你直接在编辑器工作区中绘制形状。

重要

注意不要在编辑器中缩放碰撞形状。检查器中的“Scale”属性应保持为 (1, 1)。改变碰撞形状的大小时,你应该使用尺寸控制柄,而不是 Node2D 缩放控制柄。缩放形状可能会导致意外的碰撞行为。

../../_images/player_coll_shape.webp

物理过程回调

物理引擎以固定的速率运行(默认为每秒 60 次迭代)。这个速率通常与帧率不同,帧率会根据渲染内容和可用资源而波动。

所有与物理相关的代码都必须以这个固定速率运行。因此,Godot 区分了空闲处理与物理处理。每帧运行的代码称为空闲处理,而每个物理周期运行的代码称为物理处理。Godot 提供了两个不同的回调函数,分别用于这两种处理速率。

物理回调函数 Node._physics_process() 在每个物理步骤之前被调用。任何需要访问物理体属性的代码都应该在这里运行。该方法将传递一个名为 delta 的参数,它是一个浮点数,表示自上一步以来经过的秒数。当使用默认的 60 Hz 物理更新速率时,它通常等于 0.01666...(但并不总是,详见下文)。

备注

建议在物理计算中使用 delta 参数,以便当你更改物理更新速率或玩家设备跟不上时,游戏能够正确运行。

碰撞层与遮罩

碰撞层系统是最强大但经常被误解的碰撞功能之一。该系统允许你在各种对象之间构建复杂的交互。关键概念是(Layer)与遮罩(Mask)。每个 CollisionObject2D 都有 32 个不同的物理层可以相互作用。

让我们依次看看每个属性:

  • collision_layer

    表示该对象位于哪些层。默认情况下,所有物体都在层 1 上。

  • collision_mask

    表示该对象会对哪些层上的物体进行扫描。如果对象不在任何遮罩层中,则该物体将其忽略。默认情况下,所有物体都会扫描层 1

可以通过代码配置这些属性,也可以在检查器中对其进行编辑。

跟踪每个层的用途可能比较困难,因此为正在使用的层分配名称会很有帮助。可以在项目设置 > 层名称 > 2D 物理中为层分配名称。

../../_images/physics_layer_names.webp

图形用户界面示例

游戏中有四种节点类型:墙(Wall)、玩家(Player)、敌人(Enemy)、金币(Coin)。玩家和敌人都应该与墙碰撞。玩家节点应该检测与敌人和金币的碰撞,但敌人和金币应该互相忽略。

首先将 1 至 4 层分别命名为“walls”(墙)“player”(玩家)“enemies”(敌人)“coins”(金币)并使用“Layer”属性将每个节点类型放在其各自的层中。然后通过选择它应该与之互动的层来设置每个节点的“Mask”属性。例如,玩家的设置将看起来像这样:

../../_images/player_collision_layers.webp ../../_images/player_collision_mask.webp

代码示例

在函数调用中,层通过位掩码指定。一个函数想要默认启用所有层时,可以将层掩码指定为 0xffffffff。根据你的喜好,你的代码可以使用二进制、十六进制或十进制来表示层掩码。

上述示例中启用了第 1、3 和 4 层的等效代码如下:

# Example: Setting mask value for enabling layers 1, 3 and 4

# Binary - set the bit corresponding to the layers you want to enable (1, 3, and 4) to 1, set all other bits to 0.
# Note: Layer 32 is the first bit, layer 1 is the last. The mask for layers 4, 3 and 1 is therefore:
0b00000000_00000000_00000000_00001101
# (This can be shortened to 0b1101)

# Hexadecimal equivalent (1101 binary converted to hexadecimal).
0x000d
# (This value can be shortened to 0xd.)

# Decimal - Add the results of 2 to the power of (layer to be enabled - 1).
# (2^(1-1)) + (2^(3-1)) + (2^(4-1)) = 1 + 4 + 8 = 13
#
# We can use the `<<` operator to shift the bit to the left by the layer number we want to enable.
# This is a faster way to multiply by powers of 2 than `pow()`.
# Additionally, we use the `|` (binary OR) operator to combine the results of each layer.
# This ensures we don't add the same layer multiple times, which would behave incorrectly.
(1 << 1 - 1) | (1 << 3 - 1) | (1 << 4 - 1)

# The above can alternatively be written as:
# pow(2, 1 - 1) + pow(2, 3 - 1) + pow(2, 4 - 1)

你也可以通过在任意 CollisionObject2D 上调用 set_collision_layer_value(layer_number, value)set_collision_mask_value(layer_number, value) 来独立设置位值,如下所示:

# Example: Setting mask value to enable layers 1, 3, and 4.

var collider: CollisionObject2D = $CollisionObject2D  # Any given collider.
collider.set_collision_mask_value(1, true)
collider.set_collision_mask_value(3, true)
collider.set_collision_mask_value(4, true)

导出注解可用于在编辑器中以用户友好的图形界面导出位掩码:

@export_flags_2d_physics var layers_2d_physics

在 2D 和 3D 中,渲染层和导航层还有额外的导出注解可用。参见导出位标记

Area2D

Area 节点的作用是检测影响。它们可以检测物体何时重叠,并在物体进入或离开时发出信号。Area 也可用于覆盖物理属性,例如一定区域内的重力或阻尼。

Area2D 的主要用途有三种:

  • 覆盖给定区域中的物理参数(例如重力)。

  • 检测其他物体何时进入或退出某个区域或当前哪个物体位于某个区域。

  • 检查是否与其他区域重叠。

默认情况下,Area 还会接收鼠标和触摸屏输入。

StaticBody2D

静态体是物理引擎不会移动的物体。它参与碰撞检测,但不会因响应碰撞而移动。然而,它可以使用 constant_linear_velocityconstant_angular_velocity 属性将运动或旋转传递给碰撞体,好像它正在移动一样。

StaticBody2D 节点最常用于属于环境的对象或不需要任何动态行为的对象。

StaticBody2D 的示例用法:

  • 平台(包括可移动的平台)

  • 输送带

  • 墙壁和其他障碍

RigidBody2D

这是实现 2D 物理模拟的节点。你不应该直接控制 RigidBody2D。而是对它施加力,让物理引擎计算由此产生的运动,包括与其他物体的碰撞,以及碰撞响应,如弹跳、旋转等。

你可以通过“Mass”(质量)“Friction”(摩擦)“Bounce”(反弹)等属性修改刚体的行为,这些都可以在检查器中设置。

物体的行为也受到在项目设置 > 物理中设置的世界属性的影响,或者通过进入覆盖全局物理属性的 Area2D

当一个刚体处于静止状态且有一段时间没有移动,它就会进入睡眠状态。睡眠的物体就像一个静态的物体,它的力不会被物理引擎计算。当力被施加时,无论是通过碰撞还是通过代码,该物体都会被唤醒。

使用 RigidBody2D

使用刚体的一个好处是,可以“免费”获得许多行为而无需编写任何代码。例如,如果你正在制作一个带有下降块的《愤怒的小鸟》式游戏,你只需要创建 RigidBody2D 并调整它们的属性。堆叠、下降、弹跳将由物理引擎自动计算。

但是,如果你确实希望对刚体有一定的控制权,应当小心地改变刚体的 positionlinear_velocity 或其他物理属性,这可能会导致刚体产生意外行为。要改变任何与物理相关的属性,应该使用 _integrate_forces() 回调来代替 _physics_process()。在这个回调中,你可以访问刚体的 PhysicsDirectBodyState2D,它允许安全地改变属性并与物理引擎同步。

例如,以下是《爆破彗星》式宇宙飞船的代码:

extends RigidBody2D

var thrust = Vector2(0, -250)
var torque = 20000

func _integrate_forces(state):
    if Input.is_action_pressed("ui_up"):
        state.apply_force(thrust.rotated(rotation))
    else:
        state.apply_force(Vector2())
    var rotation_direction = 0
    if Input.is_action_pressed("ui_right"):
        rotation_direction += 1
    if Input.is_action_pressed("ui_left"):
        rotation_direction -= 1
    state.apply_torque(rotation_direction * torque)

请注意,我们不是直接设置 linear_velocityangular_velocity 属性,而是将力(thrusttorque)施加到物体上并让物理引擎计算出最终的运动。

备注

当一个刚体进入睡眠状态时,_integrate_forces() 函数将不会被调用。要重写这一行为,你需要通过创建碰撞、对其施加力或禁用 can_sleep 属性来保持物体的激活。请注意,这可能会对性能产生负面影响。

接触报告

默认情况下,刚体不会跟踪接触点,因为如果场景中存在着许多物体,这可能需要大量的内存。若要启用接触报告,请将 max_contacts_reported 属性设置为非零值。然后可以通过 PhysicsDirectBodyState2D.get_contact_count() 和相关函数获得接触。

可以启用 contact_monitor 属性来通过信号监控接触。请参阅 RigidBody2D 的可用信号列表。

CharacterBody2D

CharacterBody2D 物体能够检测到与其他物体的碰撞,但不会受到重力、摩擦力等物理属性的影响。相反,它们必须由用户通过代码来控制。物理引擎不会移动角色体。

移动角色体时,你不应该直接设置 position,而应该使用 move_and_collide()move_and_slide() 方法。这些方法会让物体沿着给定的向量移动,与其他物体发生碰撞就会立即停止移动。发生碰撞后,必须手动编写对碰撞的响应逻辑。

角色碰撞响应

发生碰撞后,你可能会希望该物体发生反弹、沿着墙体滑动、或者修改被碰撞对象的属性。处理碰撞响应的方法取决于移动 CharacterBody2D 的方法。

move_and_collide

当使用 move_and_collide() 时,该函数返回一个 KinematicCollision2D 对象,其中包含有关碰撞和碰撞体的信息。你可以使用此信息来确定响应。

例如,如果要查找发生碰撞的空间点:

extends PhysicsBody2D

var velocity = Vector2(250, 250)

func _physics_process(delta):
    var collision_info = move_and_collide(velocity * delta)
    if collision_info:
        var collision_point = collision_info.get_position()

或者从碰撞物体反弹:

extends PhysicsBody2D

var velocity = Vector2(250, 250)

func _physics_process(delta):
    var collision_info = move_and_collide(velocity * delta)
    if collision_info:
        velocity = velocity.bounce(collision_info.get_normal())

move_and_slide

滑动是一种常见的碰撞响应;想象一个游戏角色在上帝视角的游戏中沿着墙壁移动,或者在平台游戏中上下坡。虽然可以在使用 move_and_collide() 之后自己编写这个响应,但 move_and_slide() 提供了一种快捷方法来实现滑动且无需编写太多代码。

警告

move_and_slide() 在计算中自动包含时间步长,因此你不应该将速度向量乘以 deltagravity 不同,它是一个与时间有关的加速度量,因此需要乘以 delta 进行缩放。

例如,使用以下代码可以制作一个能够沿着地面(包括斜坡)行走,并在地面上跳跃的角色:

extends CharacterBody2D

var run_speed = 350
var jump_speed = -1000
var gravity = 2500

func get_input():
    velocity.x = 0
    var right = Input.is_action_pressed('ui_right')
    var left = Input.is_action_pressed('ui_left')
    var jump = Input.is_action_just_pressed('ui_select')

    if is_on_floor() and jump:
        velocity.y = jump_speed
    if right:
        velocity.x += run_speed
    if left:
        velocity.x -= run_speed

func _physics_process(delta):
    velocity.y += gravity * delta
    get_input()
    move_and_slide()

有关使用 move_and_slide() 的更多详细信息,请参阅运动学角色(2D),包括带有详细代码的演示项目。