如何检测 Godot 中的碰撞?

Syo*_*oma 11 godot

我有3个场景。一个名为“KinematicBody2D.tscn”的 KinematicBody2D 节点。该场景是一个玩家在屏幕上从左向右移动。我还有一个名为“mob.tscn”的场景,它是一个igidbody2d节点。这个场景只有精灵和一小段代码,使得生物一旦使用可见性通知器离开屏幕就会删除自己(我还关闭了遮罩方块,这样就不会有物理了)。最后,我有了主场景,其中包含玩家场景,并经常实例化生物场景,以在屏幕顶部生成生物。

我想检测生物何时接触玩家并给出输出

请非常仔细地解释一切,因为过去几天我一直在努力弄清楚,但我看过的大多数地方我都不明白该怎么做,当我复制代码时它不起作用。我想更清楚地举一些例子:

在哪里以及如何添加碰撞形状 2d 或 area2d 或沿着这些线的其他节点。

在哪里以及如何连接和编写代码

先感谢您

The*_*aot 62

对于这个答案,我将首先解释碰撞层和掩模。然后继续检测碰撞。然后过滤身体和通讯。最后我会提到一些关于光线投射和区域的内容,以及其他查询。不,你不需要知道所有这些,但你要求“一切都经过深思熟虑”。

\n

虽然问题是关于 2D 的,但对于 3D 来说,只要您使用节点的 3D 版本,情况就基本相同。我会提到它的不同之处。请注意,为了以防万一,请注意 3D 对象不能与 2D 对象发生碰撞。

\n

如果我说Class( 2D/ 3D),我的意思是我所说的适用于ClassClass2D

\n

您还会发现我说的“运动学/角色”指的是 Godot 3 中的运动体或 Godot 4 中的角色体。我将指定 Godot 3 或 Godot 4 的特定内容。

\n

另外,由于我们谈论的是物理学,因此一般来说您将在_physics_process(\xe2\x80\xa6). 当您需要在其他地方编写代码时,我会提到任何情况。

\n
\n

碰撞层和碰撞遮罩

\n

首先,设置collision_layercollision_mask属性的默认值,以便所有内容都相互碰撞。

\n
\n

否则,定义一些层可能会很有用。您可以转到“项目设置”->“常规”->“图层名称”->“2d 物理”(如果您在 3D 环境中工作,则为“3d 物理”),然后为其中的图层命名。

\n

例如,您可能有这些层(取决于您正在做的游戏类型):

\n
    \n
  • 玩家角色
  • \n
  • 敌人角色
  • \n
  • 玩家投射物
  • \n
  • 敌方射弹
  • \n
  • 收藏品
  • \n
  • 地面和墙壁
  • \n
\n

类似的事情。然后,collision_layer根据每个物理对象的性质,为每个物理对象赋予其自己的属性。

\n

在中collision_mask设置它们可以与\xe2\x80\xbb 碰撞的内容。有时,考虑一下它们不能碰撞的东西会更容易。例如,敌方射弹不应与其他敌方射弹发生碰撞(并告诉 Godot 不要检查这些碰撞可以帮助提高性能)。玩家角色可能不会与玩家射弹互动,敌方角色也不会与敌方射弹互动。同样,敌人角色可能不会与收藏品互动。所有东西都会与地面和墙壁碰撞。

\n

\xe2\x80\xbb:实际上,一个对象将与它们在碰撞掩码中指定的任何对象以及在碰撞掩码中指定它们的任何对象发生碰撞。也就是说,碰撞是双向检查的。Godot 4.0 正在改变这种情况。

\n

您总共有 32 个图层可供使用。对于某些人来说,这还不够,我们将不得不资源到其他过滤方式,我们稍后会看到。无论如何,尽量提高图层的效率。将它们用作广泛的类别。

\n
\n

如果您想从代码中设置collision_layercollision_mask属性,您需要记住它们是二进制标志集。我已经在别处解释过了。

\n
\n

设置碰撞器

\n

运动/角色、静态和刚体以及区域需要CollisionShape( 2D/ 3D) 或CollisionPolygon( 2D/ 3D) 作为子项。直接子节点。就物理学而言,这就是定义它们的大小和形状的因素。添加精灵或其他图形节点仅与图形有关,对物理没有影响。

\n

如果您使用CollisionShape( 2D/ 3D),请确保将 设为shape您的CollisionShape( 2D/ 3D)。一旦选择了所需的形状类型,编辑器将允许您直观地修改它,或者您可以在检查器面板中设置其参数。

\n

同样,如果您使用CollisionPolygon( 2D/ 3D),则需要编辑( / )polygon的属性(这是一个点数组)。为此,编辑器将允许您绘制多边形,或者您可以修改检查器面板中每个点的坐标。CollisionPolygon2D3D

\n

顺便说一下,您可以拥有多个这样的节点。也就是说,如果单个CollisionShape( 2D/ 3D) 或CollisionPolygon( 2D/ 3D) 不足以指定对象的形状和大小,您可以添加更多。请注意,它们越简单,性能就越好。

\n

如果您有图形节点(例如精灵),请查找选择后应出现在顶部(“视图”右侧)的工具菜单。在那里您可以找到从图形节点生成CollisionShape( 2D/ 3D) 或CollisionPolygon( 2D/ ) 的选项。我发现这对于3D 环境特别有用。3DMeshInstance

\n

一个简单的典型设置可能如下所示(2D):

\n

戈多3

\n
KinematicBody2D\n\xe2\x94\x9c CollisionShape2D\n\xe2\x94\x94 Sprite\n
Run Code Online (Sandbox Code Playgroud)\n

戈多4

\n
CharacterBody2D\n\xe2\x94\x9c CollisionShape2D\n\xe2\x94\x94 Sprite2D\n
Run Code Online (Sandbox Code Playgroud)\n

或者像这样(3D):

\n

戈多3

\n
KinematicBody\n\xe2\x94\x9c CollisionShape\n\xe2\x94\x94 MeshInstance\n
Run Code Online (Sandbox Code Playgroud)\n

戈多4

\n
CharacterBody3D\n\xe2\x94\x9c CollisionShape3D\n\xe2\x94\x94 MeshInstance3D\n
Run Code Online (Sandbox Code Playgroud)\n

我想强调的是,将图形(精灵/网格)作为物理体的子项非常重要。我们将移动物理体,因为它们与物理相互作用或发生反应(因此它们与环境发生碰撞),并且我们希望图形随之移动。一般来说,孩子会跟着父母一起搬家。

\n

我将讨论在其他地方的常见情况下使用哪种物理体。

\n
\n

检测碰撞

\n

我们有两个物体发生碰撞。它们中的任何一个都可以检测到碰撞。

\n

注意:如果通常您想检查给定位置是否有物体,请参阅下面的物理查询。

\n
\n

检测运动/角色身体

\n

运动学/角色身体只能检测到由于自身运动而遇到的障碍物(区域不是障碍物)。也就是说,如果另一个物体击中它,它可能无法检测到它。

\n

我们有两种方法,具体取决于您使用move_and_collide(\xe2\x80\xa6)move_and_slide(\xe2\x80\xa6)我们还可以利用一个区域来进行更好的检测。我会回过头来讨论这一点。

\n
\n

move_and_collide(\xe2\x80\xa6)

\n

当您使用 移动运动/角色主体时move_and_collide(\xe2\x80\xa6),它会返回一个KinematicCollision( 2D/ 3D) 对象,告诉您它所碰撞的信息。您可以通过检查其属性来获取它碰撞的对象collider

\n
var collision := move_and_collide(direction * delta)\nif collision != null:\n    var body := collision.collider\n    print("Collided with: ", body.name)\n
Run Code Online (Sandbox Code Playgroud)\n
\n

move_and_slide(\xe2\x80\xa6)

\n

在更常见的情况下,您可以使用move_and_slide(\xe2\x80\xa6)(或move_and_slide_with_snap(\xe2\x80\xa6)) 移动运动学/角色身体。在这种情况下,您应该调用get_slide_collision(slide_idx),这也为我们提供了一个KinematicCollision( 2D/ 3D) 对象。\xe2\x80\x8b 这是一个例子:

\n

戈多3

\n
velocity = move_and_slide(velocity)\nfor index in get_slide_count():\n    \xe2\x80\x8bvar collision := get_slide_collision(index)\n    var body := collision.collider\n   \xe2\x80\x8b print("Collided with: ", body.name)\n
Run Code Online (Sandbox Code Playgroud)\n

戈多4

\n
move_and_slide()\nfor index in get_slide_collision_count():\n    \xe2\x80\x8bvar collision := get_slide_collision(index)\n    var body := collision.get_collider()\n   \xe2\x80\x8b print("Collided with: ", body.name)\n
Run Code Online (Sandbox Code Playgroud)\n

在 Godot 4 中move_and_slide不带参数。并且是( / )velocity的属性。CharacterBody2D3D

\n

正如您所看到的,我们get_slide_count()在 Godot 3 和get_slide_collision_count()Godot 4 中使用其中之一来计算运动/角色身体在其运动中碰撞了多少个对象(包括滑动)。然后我们让每个人都利用get_slide_collision(slide_idx).

\n
\n

在刚体上检测

\n

要对刚体碰撞做出反应,需要将其contact_monitor属性设置为 true 并增加其contacts_reported属性。这限制了刚体跟踪的碰撞次数。因此,即使您可能只对与运动学/角色身体的碰撞感兴趣,您也需要为墙壁和地板或当时可能发生的其他碰撞留出空间。

\n

接下来您将使用"body_entered""body_exited"信号。您可以将它们连接到同一刚体中的脚本(有关如何执行此操作的信息,请参阅“关于连接信号”)。处理程序看起来像这样:

\n
func _on_body_entered(body:Node):\n    print(body, " entered")\n\nfunc _on_body_exited(body:Node):\n    print(body, " exited")\n
Run Code Online (Sandbox Code Playgroud)\n

即使它们被称为"body_entered""body_exited",您也可以将它们视为接触的开始和结束。如果你只关心碰撞的瞬间,那么你想要"body_entered"

\n
func _on_body_entered(body:Node):\n    \xe2\x80\x8bprint("Collided with: ", body.name)\n
Run Code Online (Sandbox Code Playgroud)\n
\n

检测某个区域

\n

区域节点是碰撞对象,但不是物理对象。它不会推动事物,事物也不会推动它。相反,他们穿过了。

\n

它们默认监视碰撞(由monitoring属性控制),并且还具有"body_entered""body_exited"信号,表明您可以使用与刚体中相同的方式。您可以设置 acollision_mask来控制它。

\n

此外,区域有"area_entered""area_exited"信号。也就是说,他们可以检测其他区域。这就是它的collision_layer用武之地monitorable

\n

我将回到面积的使用。

\n
\n

可挑选

\n

input_pickable您还可以通过将(或input_ray_pickable在 3D 中)设置为 ,使碰撞对象(静态、运动/角色、刚体或区域)可使用鼠标或定点设备拾取true

\n

然后连接主体或区域的"input_event"信号(或重写_input_event(\xe2\x80\xa6)方法)以找出玩家何时单击它。

\n

_input_event(\xe2\x80\xa6)对于 2D 节点,该方法如下所示:

\n

戈多3

\n
func _input_event(viewport: Object, event: InputEvent, shape_idx: int) -> void:\n    pass\n
Run Code Online (Sandbox Code Playgroud)\n

戈多4

\n
func _input_event(viewport: Viewport, event: InputEvent, shape_idx: int) -> void:\n    pass\n
Run Code Online (Sandbox Code Playgroud)\n

_input_event(\xe2\x80\xa6)对于 3D 节点,该方法如下所示:

\n

戈多3

\n
func _input_event(camera: Object, event: InputEvent, position: Vector3, normal: Vector3, shape_idx: int) -> void\n    pass\n
Run Code Online (Sandbox Code Playgroud)\n

戈多4

\n
func _input_event(camera: Camera3D, event: InputEvent, position: Vector3, normal: Vector3, shape_idx: int) -> void\n    pass\n
Run Code Online (Sandbox Code Playgroud)\n

不要将它们与 混淆_input

\n
\n

关于连接信号

\n

您可以从编辑器连接信号,在“节点”面板 ->“信号”选项卡中,您将找到所选节点的信号。从那里您可以将它们连接到同一场景上附加脚本的任何节点。因此,您应该预先在要将信号连接到的节点上附加一个脚本。

\n

一旦你告诉 Godot 连接一个信号,它会要求你选择要将其连接到的节点,并允许你指定处理它的方法的名称(默认情况下会生成一个名称)。在“高级”下,您还可以添加要传递给该方法的额外参数,无论信号是否可以/将等待下一帧(“延迟”),以及一旦触发它是否会自行断开连接(“一次性”)。

\n

通过按“连接”按钮,Godot 将相应地连接信号,如果目标节点的脚本不存在,则使用提供的名称创建一个方法。

\n

还可以连接和断开代码中的信号。为此,请使用connect(\xe2\x80\xa6),disconnect(\xe2\x80\xa6)is_connected(\xe2\x80\xa6)方法。例如,您可以从代码中实例化一个场景,然后使用该connect(\xe2\x80\xa6)方法将信号连接到实例或从实例连接信号。

\n
\n

过滤身体和通讯

\n

我们已经介绍了第一个过滤碰撞的工具:碰撞层和遮罩。

\n

现在,无论您通过什么方式检测碰撞,您都会获得发生碰撞的节点。但你需要区分它们。

\n

为此,我们通常使用三种类型的过滤器:

\n
    \n
  • 按类别过滤。
  • \n
  • 按组过滤。
  • \n
  • 按属性过滤。
  • \n
\n
\n

按类别过滤

\n

要按类别过滤,我们可以使用is运算符。例如:

\n

戈多3

\n
if body is KinematicBody2D:\n    print("Collided with a KinematicBody2D")\n
Run Code Online (Sandbox Code Playgroud)\n

戈多4

\n
if body is CharacterBody2D:\n    print("Collided with a CharacterBody2D")\n
Run Code Online (Sandbox Code Playgroud)\n

请记住,这也适用于用户定义的类。所以我们可以这样做:

\n
if body is PlayerCharacter:\n    print("Collided with a PlayerCharacter")\n
Run Code Online (Sandbox Code Playgroud)\n

前提是我们const PlayerCharacter := preload("player_character.gd")在脚本中添加了一个。class_name PlayerCharacter或者我们在玩家角色脚本中添加。

\n
\n

另一种方法是使用运算符as

\n
var player := body as PlayerCharacter\nif player != null:\n    print("Collided with a PlayerCharacter")\n
Run Code Online (Sandbox Code Playgroud)\n

这也为我们提供了类型安全。然后我们可以轻松访问它的属性:

\n
var player := body as PlayerCharacter\nif player == null:\n    return\n\nprint(player.some_custom_property)\n
Run Code Online (Sandbox Code Playgroud)\n
\n

按组过滤

\n

节点也有节点组。您可以从编辑器的“节点”面板 ->“组”选项卡中设置它们。或者您可以使用 , 从代码中操作它们add_to_group(\xe2\x80\xa6)remove_from_group(\xe2\x80\xa6)当然,我们可以检查一个对象是否在一个组中is_in_group(\xe2\x80\xa6)

\n
if body.is_in_group("player"):\n    print("Collided with a player")\n
Run Code Online (Sandbox Code Playgroud)\n
\n

按属性过滤

\n

当然,您可以按节点的某些属性进行过滤。首先,它的名称是:

\n
if body.name == "Player":\n    print("Collided with a player")\n
Run Code Online (Sandbox Code Playgroud)\n

或者您可以检查是否collision_layer有您感兴趣的标志:

\n
if body.collision_layer & layer_flag != 0:\n    print("Collided with a player")\n
Run Code Online (Sandbox Code Playgroud)\n

从图层名称中获取标志并不简单,但也是可能的。我在别处找到了一个例子。

\n
\n

沟通

\n

一旦识别了对象,您可能想与它进行交互。有时,向玩家角色显式添加一个方法 ( func) 是个好主意,以便与它发生碰撞的其他对象调用该方法。

\n

例如,在您的玩家角色中:

\n
func on_collision(body:Node) -> void:\n    print("Collided with ", body.name)\n
Run Code Online (Sandbox Code Playgroud)\n

在你的刚体中:

\n
func _on_body_entered(body:Node):\n    var player := body as PlayerCharacter\n    if player == null:\n        return\n\n    player.on_collision(self)\n
Run Code Online (Sandbox Code Playgroud)\n
\n

您可能还对信号总线感兴趣,我已在其他地方对此进行了解释。

\n
\n

面积的用途

\n

您可以通过添加区域节点作为子节点来改进运动/角色或静态主体的检测。赋予它与其父级相同的碰撞形状,并将其信号连接到它。这样你就可以得到"body_entered""body_exited"的运动/角色或静态身体。

\n

设置看起来像这样:

\n

戈多3

\n
KinematicBody2D\n\xe2\x94\x9c CollisionShape2D\n\xe2\x94\x9c Sprite\n\xe2\x94\x94 Area2D\n  \xe2\x94\x94 CollisionShape2D\n
Run Code Online (Sandbox Code Playgroud)\n

信号从 Area2D 连接到 KinematicBody2D。

\n

戈多4

\n
CharacterBody2D\n\xe2\x94\x9c CollisionShape2D\n\xe2\x94\x9c Sprite2D\n\xe2\x94\x94 Area2D\n  \xe2\x94\x94 CollisionShape2D\n
Run Code Online (Sandbox Code Playgroud)\n

信号从 Area2D 连接到 CharacterBody2D。

\n
\n

您可以为敌人添加一个区域并使其更大。这对于定义敌人可以检测到玩家的“视锥”很有用。您还可以将其与光线投射结合起来,以确保敌人具有视线。我推荐GDQuest 的视频《让敌人看到戈多的简单方法》。

\n

区域还允许您局部覆盖重力(由刚体使用)。为此,请使用“检查器”面板中“物理覆盖”下的属性。它们允许您拥有重力具有不同方向或强度的区域,甚至使重力点而不是方向。

\n

使用可收集物体的区域也是一个好主意,这不应引起任何物理反应(无弹跳、推动等),但您仍然需要检测玩家何时与其碰撞。

\n

当然,我认为区域的主要用途是:您可以在地图中定义区域,当玩家踩到该区域时,这些区域将触发某些事件(例如,玩家身后的门关闭)。

\n
\n

射线投射

\n

RayCast要检测( 2D/ )上的碰撞3D,请确保其enabled属性设置为true。您还可以指定collision_mask. 并确保将其设置cast_to为合理的值。

\n

光线投射将允许您查询段中的物理对象从其位置到指向的位置cast_tocast_to应用了光线投射变换的矢量。即cast_to相对于光线投射)。

\n

顺便说一句,无限cast_to是行不通的。这不仅仅是一个性能问题。问题是无限向量在变换时(特别是旋转时)表现不佳。

\n

您可以致电is_colliding()了解光线投射是否检测到某些内容。然后get_collider()去得到它。Godot 每个物理帧更新一次。

\n

例子:

\n
if $RayCast.is_colliding():\n    print("Detected: ", $RayCast.get_collider)\n
Run Code Online (Sandbox Code Playgroud)\n

如果您需要移动光线投射并在每个物理帧上进行多次检测,则需要调用force_update_transform()force_raycast_update()

\n

光线投射只会告诉您它遇到的第一个障碍。如果需要找到所有障碍物,则需要创建一个循环,将前一个障碍物添加为例外。当您不再需要异常时,不要忘记清除它们。

\n
\n

如果您想让敌人避免从平台上坠落,您可以使用光线投射来检测前方是否有地面(示例)。

\n
\n

3D 游戏还可以使用光线投射来检测玩家正在查看的内容或玩家点击的内容。intersect_point(\xe2\x80\xa6)在 2D 中,您可能需要我下面提到的方法。

\n

在 Godot 4 中,您还可以使用ShapeCast( 2D/ 3D),其工作方式类似,但不是检查射线(沿某个方向延伸的点),而是检查体积(沿某个方向延伸的形状)。

\n
\n

物理查询

\n

有时我们想向 Godot 物理引擎询问一些东西,没有任何碰撞或额外的节点(例如区域、光线投射或形状投射......这也可以)。

\n

首先,move_and_collide(\xe2\x80\xa6)有一个test_only参数,如果设置为true,将为您提供碰撞信息,但实际上不会移动运动学/角色身体。

\n

其次,您RigidBody2D有一种test_motion(\xe2\x80\xa6)方法可以告诉您刚体是否会发生碰撞或没有给定运动矢量。

\n

但是,第三\xe2\x80\xa6 我们不需要专用的RayCast2D/ 3D)。我们做得到:

\n\n

在 Godot 3 中,2D 版本direct_space_state还提供了intersect_point(\xe2\x80\xa6),它允许您检查特定点中的物理对象。还有一个intersect_point_on_canvas(\xe2\x80\xa6),它允许您指定画布 id,旨在匹配CanvasLayer

\n

在 Godot 4 中,2D 和 3D 版本direct_space_state都有intersect_point. intersect_point_on_canvas已被删除。

\n

您发现的其他方法direct_space_state是形状铸造。也就是说,它们不仅仅检查线段(如光线投射)或点(如intersect_point(\xe2\x80\xa6)光线投射),而是检查形状。

\n
\n

教程和资源

\n

请参阅Godot 官方文档中的“教程和资源”一文。

\n
\n

关于调试的注意事项

\n

虽然我上面所说的一切都是当出现问题时检查它们是否正确的事情。有一些调试工具和技术需要注意。

\n

首先,您可以设置断点(使用 F9 或使用关键字breakpoint)并单步执行代码(F10 跳过,F11 进入)。

\n

特别是对于调试物理,您需要从调试菜单中打开“可见碰撞形状”。

\n

此外,当 Godot 运行时,您可以转到“场景”面板,然后选择“远程”选项卡,这将允许您查看和编辑当前运行的游戏中的节点。

\n

您还可以使用“项目相机覆盖”选项(工具栏上的相机图标在游戏未运行时禁用)从编辑器控制正在运行的游戏的相机。

\n

最后,您可能熟悉用作print调试工具。它允许您记录事件发生的时间和变量的值。视觉上的等价物是生成视觉对象(精灵或网格实例),这些对象向您显示触发事件的时间、地点和事件(可能使用颜色来传达额外信息)。由于 Godot 3.5 包含Label3D您也可以使用它来写入一些值。

\n
\n

复制代码的注意事项

\n

您复制的代码不起作用的原因可能包括场景树的设置不同,或者某些信号未连接。然而,它也可能是空白。在 GDScript 中,空格很重要(您可以回顾GDScript 基础知识)。您可能需要调整缩进级别才能使其与您的代码兼容。也不要混合使用制表符和空格,特别是在旧版本的 Godot 中。

\n
\n

关于解释问题的说明

\n

所以你复制的代码不起作用。这意味着什么?它是做错了事还是什么也没做?是否有任何错误或警告?“它不起作用”并不是描述问题的好方法。

\n

如果您有想要解决的问题(例如某些不起作用的代码),这些问答网站会更好地工作。

\n

我想鼓励指出具体问题。可以作为此网站或类似网站上的新问题,也可以作为对给您带来麻烦的答案或教程的作者的评论。所以,是的,如果这里有什么不起作用,请在评论中告诉我,我将改进答案。但也请去打扰那些提供了您复制的不起作用的代码的人(即使那又是我)。给他们施加一些压力,让他们进步。

\n

  • 谢谢!这比大多数教程都要好 (7认同)
  • 多么棒的答案啊! (3认同)