对于这个问题,我将从解释碰撞层和掩码开始。然后转向检测碰撞。然后是过滤物理体和通信。最后,我会提到一些关于射线投射和区域的内容,以及其他一些问题。嗯,你不需要了解所有这些,但你要求“非常详细地”解释。
虽然这个问题是关于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_layer
和
collision_mask
属性,你需要记住它们是一组二进制标志。我在
其他地方已经解释过了。
设置碰撞体
运动/角色、静态和刚体,以及区域,都需要作为子节点的CollisionShape
(2D
/3D
)或CollisionPolygon
(2D
/3D
)。直接作为节点的子节点。这是定义它们在物理上大小和形状的方式。添加精灵或其他图形节点只涉及图形,对物理没有影响。
如果使用CollisionShape
(2D
/3D
),请确保将shape
设置为您的CollisionShape
(2D
/3D
)。一旦选择了所需的形状类型,编辑器将允许您在可视化界面中进行修改,或者您可以在检查器面板中设置其参数。
同样地,如果你使用
CollisionPolygon
(
2D
/
3D
),你需要编辑
polygon
(一个点数组)属性的
CollisionPolygon
(
2D
/
3D
)。为此,编辑器将允许你绘制多边形,或者你可以在检查器面板中修改每个点的坐标。
顺便说一下,你可以拥有多个这些节点。也就是说,如果单个
CollisionShape
(
2D
/
3D
)或
CollisionPolygon
(
2D
/
3D
)无法完全指定对象的形状和大小,你可以添加更多。需要注意的是,它们越简单,性能就越好。
如果你有一个图形节点(例如一个精灵),当你选择它时,你应该会看到一个工具菜单出现在顶部(在“视图”右侧)。在那里,你可以找到选项来从你的图形节点生成一个
CollisionShape
(
2D
/
3D
)或
CollisionPolygon
(
2D
/
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)
,它还会给我们一个KinematicCollision
(2D
/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
不需要参数。而
velocity
是
CharacterBody
(
2D
/
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_layer
和monitorable
的作用所在。
我将回到区域的用途。
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
要在RayCast
(2D
/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(…)
方法,可以告诉你在给定运动向量的情况下刚体是否会发生碰撞。
但是,第三点...我们不需要一个专用的RayCast
(2D
/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中。
关于解释问题的说明
所以你复制的代码没有起作用。这是什么意思?它做错了事情还是什么都没做?有没有出现任何错误或警告?"没有起作用"不是描述问题的好方式。
如果你有什么需要解决的问题,这些问答网站会更有效。比如一些代码无法正常工作。
我想鼓励指出具体的问题。可以作为一个新问题在这个或类似的网站上提问,或者作为对给你带来麻烦的答案或教程作者的评论。所以,是的,如果这里有什么不起作用的地方,请在评论中告诉我,我会改进答案。但也要去打扰一下提供你复制的代码但没有起作用的人(即使那个人又是我)。给他们一些压力,让他们改进。