如何在Godot中检测KinematicBody2D(玩家)节点和RigidBody2D节点(敌人)之间的碰撞?

8

我有三个场景。一个名为“KinematicBody2D.tscn”的是KinematicBody2D节点,它是一个移动从屏幕左到右的玩家。 我还有一个名为“mob.tscn”的场景,其中包含一个Rigidbody2D节点。这个场景只有一个精灵和一小段代码,使得mob在离开屏幕时使用可见性通知器删除自己(我还关闭了遮罩方块,以便没有物理效果)。最后,我有一个主场景,其中包含玩家场景,并每隔一段时间实例化mob场景,以在屏幕顶部生成mobs。

我想检测mob何时碰触玩家并输出结果。

请非常详细地解释,因为过去几天我一直在尝试弄清楚但大多数我看过的地方我不知道该怎么做,当我复制代码时它也无法正常工作。我希望更清楚的事例是:

在哪里和如何添加CollisionShape2D或Area2D等节点。

连接和编写代码的位置和方式。

预先感谢您!


1
如果这个答案展现了比该网站上95%的回答更多的努力(也可能是更好的质量),请将其标记为已接受,如果它回答了你的问题或帮助了你。如果没有,请添加一条评论来解释为什么没有帮助。 - Kostas Mouratidis
@Syoma 如果您看到此消息,请将答案标记为正确,这样未来寻找答案的人们可以更轻松地找到它,同时也适用于搜索引擎。 - erikbstack
2个回答

42
对于这个问题,我将从解释碰撞层和掩码开始。然后转向检测碰撞。然后是过滤物理体和通信。最后,我会提到一些关于射线投射和区域的内容,以及其他一些问题。嗯,你不需要了解所有这些,但你要求“非常详细地”解释。
虽然这个问题是关于2D的,但对于3D来说,大部分都是相同的,只要你使用节点的3D版本。我会提到它们的区别。另外,顺便提一下,3D对象不能与2D对象发生碰撞。
如果我说“Class(2D/3D)”,我是指我所说的适用于Class和Class2D。
你还会发现我说“运动学/角色”,这是指在Godot 3中的运动学体,或者在Godot 4中的角色体。当某些内容只适用于Godot 3或Godot 4时,我会进行说明。
另外,既然我们在谈论物理学,通常你会在_physics_process(…)中工作。我会在需要在其他地方编写代码的情况下提到。
碰撞层和碰撞掩码
首先,碰撞层(collision_layer)和碰撞掩码(collision_mask)的默认值被设置为使所有物体都会发生碰撞。
否则,定义一些层级可能会很有用。您可以转到项目设置->常规->层名称->2D物理(或3D物理,如果您在3D中工作),并在那里为层级命名。
例如,根据您所做的游戏类型,您可能会有以下层级:
- 玩家角色 - 敌人角色 - 玩家发射物 - 敌人发射物 - 可收集物品 - 地面和墙壁
类似这样。然后,根据物体的属性,为每个物理对象分配其自己的“collision_layer”。
在“collision_mask”中,您设置它们可以与哪些物体发生碰撞※。有时,想一想它们不能与哪些物体发生碰撞会更容易。例如,敌人发射物不应与其他敌人发射物发生碰撞(告诉Godot不检查这些碰撞可以提高性能)。玩家角色可能不会与玩家发射物互动,敌人角色也不会与敌人发射物互动。同样,敌人角色可能不会与可收集物品互动。而所有物体都会与地面和墙壁发生碰撞。
※:实际上,一个对象会与其在碰撞掩码中指定的任何对象发生碰撞,而那些对象也会与其在碰撞掩码中指定的对象发生碰撞。也就是说,碰撞是双向检查的。这在Godot 4.0中将会改变。
您总共有32个图层可供使用。对于一些人来说,这可能不够,我们将不得不采用其他过滤方式,稍后我们将会看到。无论如何,请尽量高效地使用您的图层。将它们用作广泛的类别。
如果你想从代码中设置collision_layercollision_mask属性,你需要记住它们是一组二进制标志。我在其他地方已经解释过了。

设置碰撞体

运动/角色、静态和刚体,以及区域,都需要作为子节点的CollisionShape2D/3D)或CollisionPolygon2D/3D)。直接作为节点的子节点。这是定义它们在物理上大小和形状的方式。添加精灵或其他图形节点只涉及图形,对物理没有影响。

如果使用CollisionShape2D/3D),请确保将shape设置为您的CollisionShape2D/3D)。一旦选择了所需的形状类型,编辑器将允许您在可视化界面中进行修改,或者您可以在检查器面板中设置其参数。

同样地,如果你使用CollisionPolygon2D/3D),你需要编辑polygon(一个点数组)属性的CollisionPolygon2D/3D)。为此,编辑器将允许你绘制多边形,或者你可以在检查器面板中修改每个点的坐标。
顺便说一下,你可以拥有多个这些节点。也就是说,如果单个CollisionShape2D/3D)或CollisionPolygon2D/3D)无法完全指定对象的形状和大小,你可以添加更多。需要注意的是,它们越简单,性能就越好。
如果你有一个图形节点(例如一个精灵),当你选择它时,你应该会看到一个工具菜单出现在顶部(在“视图”右侧)。在那里,你可以找到选项来从你的图形节点生成一个CollisionShape2D/3D)或CollisionPolygon2D/3D)。我发现这在3D中特别适用于MeshInstance 一个简单的典型设置可能是这样的(2D): Godot 3
KinematicBody2D
├ CollisionShape2D
└ Sprite

Godot 4

CharacterBody2D
├ CollisionShape2D
└ Sprite2D

或者像这样(3D):
《Godot 3》
KinematicBody
├ CollisionShape
└ MeshInstance

Godot 4

CharacterBody3D
├ CollisionShape3D
└ MeshInstance3D

我想强调将图形(精灵/网格)作为物理体的子对象是很重要的。我们将移动物理体,因为它们与物理学相互作用或反应(所以它们与环境发生碰撞),我们希望图形随之移动。通常情况下,子对象会随父对象一起移动。 我会在其他地方详细介绍常见情况下使用哪种物理体。
检测碰撞
我们有两个物体发生碰撞。其中任何一个都可以检测到碰撞。

在运动体上进行检测

运动体/角色体只能检测到由其自身运动引起的碰撞。也就是说,如果另一个物体撞击它,它可能无法检测到。

根据您使用的是move_and_collide(…)还是move_and_slide(…),我们有两种方法。我们还可以利用一个区域来提高检测效果。稍后我会详细介绍。


使用move_and_collide(…)函数移动运动学/角色体时,它会返回一个KinematicCollision(2D/3D)对象,该对象提供了与之碰撞的信息。你可以通过检查其collider属性来获取碰撞的对象。
var collision := move_and_collide(direction * delta)
if collision != null:
    var body := collision.collider
    print("Collided with: ", body.name)

move_and_slide(…)

在更常见的情况下,您可以使用move_and_slide(…)(或move_and_slide_with_snap(…))移动运动学/角色体。在这种情况下,您应该调用get_slide_collision(slide_idx),它还会给我们一个KinematicCollision2D/3D)对象。以下是一个示例:

Godot 3

velocity = move_and_slide(velocity)
for index in get_slide_count():
    ​var collision := get_slide_collision(index)
    var body := collision.collider
   ​ print("Collided with: ", body.name)

Godot 4

move_and_slide()
for index in get_slide_collision_count():
    ​var collision := get_slide_collision(index)
    var body := collision.get_collider()
   ​ print("Collided with: ", body.name)

在Godot 4中,move_and_slide不需要参数。而velocityCharacterBody2D/3D)的一个属性。
正如你所看到的,在Godot 3中,我们使用get_slide_count(),而在Godot 4中,我们使用get_slide_collision_count()来确定运动中运动体/角色体与多少个对象发生了碰撞(包括滑动)。然后,我们利用get_slide_collision(slide_idx)来获取每个碰撞对象。

在刚体上检测

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

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

func _on_body_entered(body:Node):
    print(body, " entered")

func _on_body_exited(body:Node):
    print(body, " exited")

即使它们被称为"body_entered""body_exited",你可以将它们视为接触的开始和结束。如果你只关心碰撞的瞬间,那么你需要"body_entered"
func _on_body_entered(body:Node):
    ​print("Collided with: ", body.name)

在区域上进行检测

区域节点是一个碰撞对象,但不是一个物理对象。它不会推动其他物体,也不会被其他物体推动。相反,它们会穿过它。

它们默认情况下会监测碰撞(由monitoring属性控制),并且还有"body_entered""body_exited"信号,您可以像刚体中的信号一样使用它们。您可以设置collision_mask来控制这一点。

此外,区域还具有"area_entered""area_exited"信号。也就是说,它们可以检测其他区域。这就是它的collision_layermonitorable的作用所在。

我将回到区域的用途。


Pickable

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

然后,将身体或区域的"input_event"信号(或覆盖_input_event(…)方法)连接起来,以了解玩家何时点击它。

这是一个2D节点的_input_event(…)方法的示例:

Godot 3

func _input_event(viewport: Object, event: InputEvent, shape_idx: int) -> void:
    pass

Godot 4

func _input_event(viewport: Viewport, event: InputEvent, shape_idx: int) -> void:
    pass

这就是一个3D节点的_input_event(…)方法的样子: Godot 3
func _input_event(camera: Object, event: InputEvent, position: Vector3, normal: Vector3, shape_idx: int) -> void
    pass

Godot 4

func _input_event(camera: Camera3D, event: InputEvent, position: Vector3, normal: Vector3, shape_idx: int) -> void
    pass

不要把这些与“_input”混淆。

关于连接信号

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

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

通过按下“连接”按钮,Godot将相应地连接信号,并在目标节点的脚本中创建一个具有提供的名称的方法(如果该方法不存在)。

也可以通过代码连接和断开信号。要做到这一点,使用connect(…)disconnect(…)is_connected(…)方法。例如,您可以从代码中实例化一个场景,然后使用connect(…)方法将信号连接到实例中,或者从实例中断开信号。

过滤物理体和通信

我们已经讨论了用于过滤碰撞的第一个工具:碰撞层和掩码。

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

为了做到这一点,我们通常使用三种类型的过滤器:

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

按类别筛选

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

Godot 3

if body is KinematicBody2D:
    print("Collided with a KinematicBody2D")

Godot 4

if body is CharacterBody2D:
    print("Collided with a CharacterBody2D")

记住,这也适用于用户定义的类。所以我们可以这样做:
if body is PlayerCharacter:
    print("Collided with a PlayerCharacter")

只要我们在脚本中添加了const PlayerCharacter := preload("player_character.gd"),或者在我们的玩家角色脚本中添加了class_name PlayerCharacter
另一种方法是使用as运算符:
var player := body as PlayerCharacter
if player != null:
    print("Collided with a PlayerCharacter")

这也给了我们类型安全。然后我们可以轻松访问它的属性:
var player := body as PlayerCharacter
if player == null:
    return

print(player.some_custom_property)

按组筛选

节点也可以有节点组。您可以从节点面板中的编辑器设置它们 -> 组选项卡。或者您可以通过代码进行操作,使用add_to_group(…)remove_from_group(…)。当然,我们还可以使用is_in_group(…)来检查对象是否在组中:

if body.is_in_group("player"):
    print("Collided with a player")

按属性筛选

当然,您可以按节点的某些属性进行筛选。首先,可以按名称进行筛选:

if body.name == "Player":
    print("Collided with a player")

或者你可以检查一下collision_layer是否有你感兴趣的标志。
if body.collision_layer & layer_flag != 0:
    print("Collided with a player")

从图层名称获取标志并不直接,但是有可能。我在其他地方找到了一个例子。

沟通

一旦你确定了目标,你可能想要与之互动。有时候,为了让其他与之碰撞的对象能够调用,向玩家角色显式地添加一个方法(func)是一个好主意。

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

func on_collision(body:Node) -> void:
    print("Collided with ", body.name)

在你的刚性身体中:
func _on_body_entered(body:Node):
    var player := body as PlayerCharacter
    if player == null:
        return

    player.on_collision(self)

你可能也对一个信号总线感兴趣,我在其他地方已经解释过了。

区域的用途

通过将区域节点作为子节点添加到一个运动/角色或静态物体上,可以改善对其的检测。给它与父节点相同的碰撞形状,并将其信号连接到父节点。这样,您可以在运动/角色或静态物体上获得"body_entered"和"body_exited"的信号。

设置大致如下:

Godot 3

KinematicBody2D
├ CollisionShape2D
├ Sprite
└ Area2D
  └ CollisionShape2D

Godot 4

CharacterBody2D
├ CollisionShape2D
├ Sprite2D
└ Area2D
  └ CollisionShape2D

从Area2D连接到KinematicBody2D的信号。
你可以给敌人添加一个区域并使其变大。这对于定义敌人可以检测到玩家的“视锥”非常有用。你还可以将其与光线投射相结合,以确保敌人有视线。
我推荐观看GDQuest的视频《在Godot中让敌人看到的简单方法》。
区域还允许你在本地覆盖重力(由刚体使用)。要做到这一点,请在检查器面板的“物理覆盖”下使用属性。它们允许你在重力方向或强度上有不同的区域,甚至可以使重力指向而不是方向。
对于可收集物体来说,使用区域也是一个好主意,这些物体不应该引起任何物理反应(不弹跳、推动等),但你仍然需要检测玩家与其碰撞的情况。
当然,我认为区域的主要用途是在地图中定义一些区域,当玩家踩上它时会触发某些事件(例如,门在玩家身后关闭)。

RayCast

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

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

注意:不,无限的cast_to是无法工作的。这不仅仅是性能问题。问题在于无限向量在变换时(尤其是旋转时)的行为不佳。

您可以调用is_colliding()来查看射线投射是否检测到了某物体。然后可以使用get_collider()来获取它。这由Godot每个物理帧更新一次。

示例:

if $RayCast.is_colliding():
    print("Detected: ", $RayCast.get_collider)

如果您需要在每个物理帧之外多次移动射线并进行检测,您需要在其上调用force_update_transform()force_raycast_update()
如果你想避免从平台上掉下来,可以使用射线投射来检测前方是否有地面(示例)。
3D游戏也可以使用光线投射来检测玩家正在看什么,或者玩家点击了什么。在2D中,你可能想要使用我下面提到的intersect_point(…)方法。

物理查询

有时候我们想向Godot物理引擎询问一些问题,而不涉及任何碰撞或额外的节点(如区域和射线)。

首先,move_and_collide(…)方法有一个test_only参数,如果设置为true,将提供碰撞信息,但不会实际移动运动学/角色体。

其次,你的RigidBody2D有一个test_motion(…)方法,可以告诉你在给定运动向量的情况下刚体是否会发生碰撞。

但是,第三点...我们不需要一个专用的RayCast2D/3D)。我们可以这样做:

3D: 使用get_world().direct_space_state.intersect_ray(start, end)。请参考PhysicsDirectSpaceState(Godot 3)或PhysicsDirectSpaceState3D(Godot 4)。
2D: 使用get_world_2D().direct_space_state.intersect_ray(start, end)。请参考Physics2DDirectSpaceState(Godot 3)或PhysicsDirectSpaceState2D(Godot 4)。
在Godot 3中,direct_space_state的2D版本还提供了intersect_point(…),它允许您检查特定点上的物理对象。此外,还有一个intersect_point_on_canvas(…),它允许您指定一个画布ID,该ID旨在与CanvasLayer匹配。
在Godot 4中,2D和3D版本的direct_space_state都有intersect_point。而intersect_point_on_canvas已被移除。
而你在direct_space_state中找到的其他方法是形状转换。也就是说,它们不仅仅检查一个线段(像raycast一样)或一个点(像intersect_point(...)一样),而是检查一个形状。

教程和资源

请参阅Godot官方文档中的文章"教程和资源"


关于调试的注意事项

虽然我上面说的都是在出现问题时要检查的正确性,但还有一些调试工具和技术需要注意。

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

特别是在调试物理问题时,您可以在调试菜单中打开“可见碰撞形状”。

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

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

最后,你可能熟悉使用print作为调试工具。它允许你记录事件发生的时间和变量的值。在可视化方面,可以生成可视对象(精灵或网格实例),以显示事件触发的时间和位置(可能使用颜色传达额外信息)。由于Godot 3.5包含Label3D,你也可以使用它来写入一些值。
关于复制代码的说明
复制的代码无法正常工作的原因可能包括场景树的设置不同,或者某些信号未连接。然而,可能也是由于空格的问题。在GDScript中,空格很重要(你可以查看GDScript基础知识)。你可能需要调整缩进级别,使其与你的代码配合工作。此外,不要混合使用制表符和空格,尤其是在旧版本的Godot中。
关于解释问题的说明
所以你复制的代码没有起作用。这是什么意思?它做错了事情还是什么都没做?有没有出现任何错误或警告?"没有起作用"不是描述问题的好方式。
如果你有什么需要解决的问题,这些问答网站会更有效。比如一些代码无法正常工作。
我想鼓励指出具体的问题。可以作为一个新问题在这个或类似的网站上提问,或者作为对给你带来麻烦的答案或教程作者的评论。所以,是的,如果这里有什么不起作用的地方,请在评论中告诉我,我会改进答案。但也要去打扰一下提供你复制的代码但没有起作用的人(即使那个人又是我)。给他们一些压力,让他们改进。

6
谢谢!这比大多数其他教程都要好。 - NamiW
2
多么棒的答案! - Greg Hilston
你的解释给了我一个比我在其他任何地方找到的都更好的答案,所以我只是想说谢谢,特别是因为它已经更新到了Godot 4。真不可思议,这个答案到现在还没有被接受,但是没办法。 - undefined

0
我认为这种方法会有效,只需在它们上面放置一个2D区域,然后您可以在两个区域上执行此操作,因此您只需使用称为area_entered(area)的信号,然后将其连接到2D区域(确保制作一个gdsipt来连接信号到2D区域),然后您可以使它们都检测到碰撞。
对于我的拼写不好,请谅解。
爱你的Alan

1
目前你的回答不够清晰。请编辑并添加更多细节,以帮助其他人理解它如何回答所提出的问题。你可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接