对于这个答案,我将从解释碰撞层和遮罩开始。然后继续检测碰撞。然后过滤实体和通信。最后我会提到一些关于光线投射和区域的东西,以及其他查询。 不,你不需要知道所有这些,但你要求“一切都非常周到”。
虽然问题是关于 2D 的,但对于 3D,只要您使用节点的 3D 版本,它基本上是相同的。我会提到它的不同之处。请注意,以防万一,请注意 3D 对象不能与 2D 对象发生碰撞。
如果我说Class(2D),我的意思是我所说的同时适用于Class 和Class2D。
另外,由于我们谈论的是物理学,一般来说你将在_physics_process 工作。当您需要在其他地方编写代码时,我会提到任何情况。
碰撞层和碰撞遮罩
首先,设置collision_layer 和collision_mask 属性的默认值,以便一切都与一切发生冲突。
否则,定义一些层会很有用。您可以转到项目设置 -> 常规 -> 图层名称 -> 2d 物理(或 3d 物理,如果您在 3D 中工作),并为那里的图层命名。
例如,您可能有这些层(取决于您正在玩的游戏类型):
- 玩家角色
- 敌人角色
- 玩家射弹
- 敌人的射弹
- 收藏品
- 地面和墙壁
类似的东西。然后根据它们是什么,为每个物理对象赋予其自己的collision_layer。
在collision_mask中你可以设置他们可以碰撞的东西※。有时更容易考虑他们不能碰撞的东西。例如,一个敌方弹丸不应与其他敌方弹丸发生碰撞(告诉 Godot 不要检查这些碰撞可以帮助提高性能)。玩家角色可能不会与玩家弹丸交互,而敌方角色也不会与敌方弹丸交互。同样,敌方角色可能不会与收藏品互动。一切都与地面和墙壁相撞。
※:实际上一个对象会与它们在其碰撞掩码中指定的任何对象以及在其碰撞掩码中指定它们的任何对象发生碰撞。也就是说,碰撞检查是双向的。 Godot 4.0 正在改变。
您总共有 32 个图层可供使用。对于某些人来说,这还不够,我们将不得不寻找其他过滤方式,我们稍后会看到。无论如何,请尝试提高图层的效率。将它们用作广泛的类别。
如果您想从代码中设置collision_layer 和collision_mask 属性,您需要记住它们是二进制标志集。我已经解释了elsewhere。
设置碰撞器
运动学、静态和刚体以及区域需要CollisionShape(2D) 或CollisionPolygon(2D) 作为子级。直接节点子节点。就物理学而言,这就是定义它们的大小和形状的原因。 添加精灵或其他图形节点只与图形有关,对物理没有影响。
如果您使用CollisionShape(2D),请确保将shape 设置为您的CollisionShape(2D)。一旦你选择了你想要的形状类型,编辑器将允许你直观地修改它,或者你可以在 Inspector 面板中设置它的参数。
同样,如果您使用CollisionPolygon(2D),您需要编辑CollisionPolygon(2D) 的polygon(点数组)属性。为此,编辑器将允许您绘制多边形,或者您可以在 Inspector 面板中修改每个点的坐标。
顺便说一下,您可以拥有多个这样的节点。也就是说,如果单个CollisionShape(2D) 或CollisionPolygon(2D) 不足以指定对象的形状和大小,则可以添加更多。在该节点上,请注意,它们越简单,性能就越好。
如果您有一个图形节点(例如精灵),请寻找一个工具菜单,当您选择它时,它应该出现在顶部(“视图”的右侧)。在那里您可以找到从图形节点生成CollisionShape(2D) 或CollisionPolygon(2D) 的选项。 我发现这对MeshInstance 在 3D 中特别有用。
一个简单的典型设置可能如下所示:
KinematicBody2D
├ CollisionShape2D
└ Sprite
或者像这样(3D):
KinematicBody
├ CollisionShape
└ MeshInstance
检测碰撞
我们有两个物体发生碰撞。它们中的任何一个都可以检测到碰撞。
在运动体上检测
运动体只能检测到由于自身运动而遇到的物体。也就是说,如果另一个物体撞到它,它可能无法检测到它。
根据您使用的move_and_collide 或move_and_collide,我们有两种方法。 我们还可以利用一个区域进行更好的检测。我会回到那个。
move_and_collide
当您使用move_and_collide 移动运动体时,它会返回一个KinematicCollision(2D) 对象,告诉您它与什么碰撞的信息。您可以通过检查其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 移动运动体。在这种情况下,您应该调用get_slide_collision,这也给了我们一个KinematicCollision(2D) 对象。这里是一个例子:
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)
如您所见,我们使用get_slide_count 来计算运动体在其运动中碰撞(包括滑动)的对象数量。然后我们让每个人都利用get_slide_collision。
在刚体上检测
要让代码对刚体碰撞做出反应,您需要将其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 的用武之地。
我会回到区域的用途。
可挑选
您还可以通过将input_pickable(或3D 中的input_ray_pickable)设置为true,使碰撞对象(静态、运动学、刚体或区域)可通过鼠标或指点设备拾取。
然后连接静态体的input_event 信号(或重写_input_event 方法)以找出玩家点击它的时间。
这就是 _input_event 方法对于 2D 节点的样子:
func _input_event(viewport: Object, event: InputEvent, shape_idx: int) -> void:
pass
这就是_input_event 方法对于 3D 节点的样子:
func _input_event(camera: Object, event: InputEvent, position: Vector3, normal: Vector3, shape_idx: int) -> void
pass
不要将这些与_input 混淆。
关于连接信号
您可以从编辑器中连接信号,在节点面板 -> 信号选项卡中,您将找到所选节点的信号。从那里您可以将它们连接到同一场景中附加了脚本的任何节点。 因此,您应该事先在要将信号连接到的节点上附加一个脚本。
一旦你告诉 Godot 连接一个信号,它会要求你选择你将连接它的节点,并允许你指定将处理它的方法的名称(默认生成一个名称)。 在“高级”下,您还可以添加要传递给方法的额外参数,无论信号是否可以/将等待下一帧(“延迟”),以及它是否会在触发后自行断开(“oneshot ")。
通过按下连接按钮,Godot 将相应地连接信号,如果目标节点的脚本不存在,则使用提供的名称创建一个方法。
也可以从代码中连接和断开信号。为此,请使用 connect、disconnect 和 is_connected 方法。 因此,例如,您可以从代码中实例化一个场景,然后使用connect 方法将信号与实例连接起来。
过滤实体和通信
我们已经完成了您过滤碰撞的第一个工具:碰撞层和遮罩。
现在,无论您以何种方式检测碰撞,您都将获得发生碰撞的节点。但是你需要区分它们。
为此,我们通常使用三种类型的过滤器:
按类别过滤
要按类过滤,我们可以使用is 运算符。例如:
if body is KinematicBody2D:
print("Collided with a KinematicBody2D")
请记住,这也适用于用户定义的类。所以我们可以这样做:
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")
从图层名称中获取标志并不简单,但可能。我找到了一个例子elsewhere。
通讯
一旦您确定了对象,您可能希望与它进行交互。有时,向玩家角色显式添加一个方法 (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)
您可能还对信号总线感兴趣,我已经解释过elsewhere。
区域用途
您可以通过添加一个区域节点作为子节点来改进对运动学或静态体的检测。给它与父对象相同的碰撞形状,并将其信号连接到它。这样你就可以在你的运动体或静态体上获得"body_entered" 和"body_exited"。
设置如下所示:
KinematicBody2D
├ CollisionShape2D
├ Sprite
└ Area2D
└ CollisionShape2D
信号从 Area2D 连接到 KinematicBody2D。
您可以为敌人添加一个区域并使其变大。这对于定义敌人可以检测到玩家的“视锥”很有用。您还可以将其与光线投射结合使用,以确保敌人有视线。 我推荐 GDQuest 的视频The Easy Way to Make Enemies See in Godot。
区域还允许您在本地覆盖重力(由刚体使用)。为此,请使用 Inspector 面板中“Physics Overrides”下的属性。它们允许您拥有重力具有不同方向或强度的区域,甚至可以制作重力点而不是方向。
为可收集对象使用区域也是一个好主意,这不应引起任何物理反应(没有弹跳、推动等),但您仍然需要检测玩家何时与它发生碰撞。
当然,我认为区域的主要用途是:您可以在地图中定义区域,当玩家踩到该区域时会触发某些事件(例如,玩家身后的门关闭)。
光线投射
要检测RayCast(2D) 上的冲突,请确保将其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。
如果你想让敌人避免从平台上掉下来,你可以使用光线投射来检测前方是否有地面 (example)。
3D 游戏也可以使用光线投射来检测玩家正在观看的内容或点击的内容。在 2D 中,您可能需要我在下面提到的方法 intersect_point。
物理查询
有时我们想向 Godot 物理引擎询问一些东西,没有任何碰撞或额外的节点(例如区域和光线投射)。
首先,move_and_collide 有一个 test_only 参数,如果设置为 true,则会提供碰撞信息,但不会实际移动运动体。
其次,你的刚体有一个test_motion 方法,它会告诉你刚体是否会发生碰撞或没有给出运动矢量。
但是,第三……我们不需要专门用于光线投射的节点。我们可以这样做:
direct_space_state 的 2D 版本还为您提供intersect_point,这将允许您检查特定点中的物理对象。 还有一个未记录的intersect_point_on_canvas,它允许你指定一个画布ID,我相信它是为了匹配一个CanvasLayer,但我还没有研究过。
您在direct_space_state 中找到的其他方法是塑形。也就是说,它们不仅仅检查一个片段(如光线投射)或一个点(如intersect_point),而是检查一个形状。
教程和资源
请参阅 Godot 官方文档上的文章"Tutorials and resources"。
关于复制代码的注意事项
您复制的代码不起作用的原因可能包括场景树的设置不同,或者某些信号未连接。但是,它也可能是空格。在 GDScript 中,空格很重要(您可以查看 GDScript basics)。您可能需要调整缩进级别以使其适用于您的代码。 也不要混合制表符和空格,尤其是在旧版本的 Godot 中。
关于解释问题的说明
所以你复制的代码没有用。那是什么意思?它做错了什么还是什么也没做?是否有任何错误或警告? “它不起作用”不是描述问题的好方法。
如果您想解决一些问题,例如一些不起作用的代码,这些问答网站会更好地工作。
我想鼓励指出具体问题。作为此站点或类似站点上的新问题,或者作为给您带来麻烦的答案或教程的作者的评论。 所以,是的,如果这里有什么不工作的地方,请在评论中告诉我,我会改进答案。但也要打扰提供您复制的无效代码的人(即使那又是我)。给他们一些改进的压力。