对于这个答案,我将首先解释碰撞层和掩模。然后继续检测碰撞。然后过滤身体和通讯。最后我会提到一些关于光线投射和区域的内容,以及其他查询。不,你不需要知道所有这些,但你要求“一切都经过深思熟虑”。
虽然问题是关于 2D 的,但对于 3D 来说,只要您使用节点的 3D 版本,情况就基本相同。我会提到它的不同之处。请注意,为了以防万一,请注意 3D 对象不能与 2D 对象发生碰撞。
如果我说Class
(2D
/3D
),我的意思是我所说的适用于两者Class
and Class2D
.
您还会发现我所说的“运动学/角色”指的是 Godot 3 中的运动体,或 Godot 4 中的角色体。我将指定何时特定于 Godot 3 或 Godot 4。
另外,由于我们谈论的是物理学,因此一般来说您将从事_physics_process(…)。当您需要在其他地方编写代码时,我会提到任何情况。
碰撞层和碰撞遮罩
首先,默认值collision_layer
and collision_mask
属性被设置为使得所有东西都互相碰撞。
否则,定义一些层可能会很有用。您可以转到“项目设置”->“常规”->“图层名称”->“2d 物理”(如果您在 3D 中工作,则为“3d 物理”),然后为其中的图层命名。
例如,您可能有这些层(取决于您正在做的游戏类型):
- 玩家角色
- 敌人角色
- 玩家投射物
- 敌方射弹
- 收藏品
- 地面和墙壁
类似的事情。然后你给每个物理对象它自己的collision_layer
取决于它们是什么。
In the collision_mask
您可以设置它们可以碰撞的对象※。有时想想他们不能碰撞的东西会更容易。例如,敌方射弹不应与其他敌方射弹碰撞(并告诉 Godot 不要检查这些碰撞可以帮助提高性能)。玩家角色可能不会与玩家射弹互动,敌方角色也不会与敌方射弹互动。同样,敌人角色可能不会与收藏品互动。所有东西都会与地面和墙壁碰撞。
※:实际上,一个对象会与它们在碰撞掩码中指定的任何对象以及在碰撞掩码中指定它们的任何对象发生碰撞。也就是说,碰撞是双向检查的。Godot 4.0 正在改变这种情况。
您总共有 32 个图层可供使用。对于某些人来说,这还不够,我们将不得不资源到其他过滤方式,我们稍后会看到。无论如何,尽量提高图层的效率。将它们用作广泛的类别。
如果您想设置collision_layer
and collision_mask
从代码中获取属性,您需要记住它们是二进制标志集。我已经解释过别处.
设置碰撞器
运动学/特性、静态和刚体,以及面积,需要CollisionShape
(2D
/3D
) or CollisionPolygon
(2D
/3D
)作为孩子。直接子节点。就物理学而言,这就是定义它们的大小和形状的因素。添加精灵或其他图形节点仅与图形有关,对物理没有影响。
如果你使用CollisionShape
(2D
/3D
),请确保设置shape
给你的CollisionShape
(2D
/3D
)。一旦选择了所需的形状类型,编辑器将允许您直观地修改它,或者您可以在检查器面板中设置其参数。
同样,如果您使用CollisionPolygon
(2D
/3D
),您需要编辑polygon
(这是一个点数组)您的属性CollisionPolygon
(2D
/3D
)。为此,编辑器将允许您绘制多边形,或者您可以修改检查器面板中每个点的坐标。
顺便说一下,您可以拥有多个这样的节点。也就是说,如果单个CollisionShape
(2D
/3D
) or CollisionPolygon
(2D
/3D
) 不足以指定对象的形状和大小,您可以添加更多。请注意,它们越简单,性能就越好。
如果您有图形节点(例如精灵),请查找选择后应出现在顶部(“视图”右侧)的工具菜单。在那里您可以找到生成CollisionShape
(2D
/3D
) or CollisionPolygon
(2D
/3D
)从你的图形节点。我发现这特别有用MeshInstance
in 3D.
一个简单的典型设置可能如下所示(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(…)
or move_and_slide(…)
. 我们还可以利用一个区域来进行更好的检测。我会回到这个话题。
move_and_collide(…)
当您使用移动运动/角色身体时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(…)
(or 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)
在《戈多4》中move_and_slide
不带任何参数。和velocity
是的财产CharacterBody
(2D
/3D
).
正如你所看到的,我们使用get_slide_count()
在《戈多 3》中,以及get_slide_collision_count()
在 Godot 4 中,计算出运动/角色身体在其运动过程中与多少个物体发生碰撞(包括滑动)。然后我们让每个人都利用get_slide_collision(slide_idx)
.
在刚体上检测
要对刚体碰撞做出反应,您需要设置其contact_monitor
财产真实并增加其contacts_reported
财产。这限制了刚体跟踪的碰撞次数。因此,即使您可能只对与运动学/角色身体的碰撞感兴趣,您也需要为墙壁和地板或当时可能发生的其他碰撞留出空间。
接下来您将使用"body_entered"
and "body_exited"
信号。您可以将它们连接到同一刚体中的脚本(有关如何执行此操作的信息,请参阅“关于连接信号”)。处理程序看起来像这样:
func _on_body_entered(body:Node):
print(body, " entered")
func _on_body_exited(body:Node):
print(body, " exited")
尽管他们被称为"body_entered"
and "body_exited"
,您可以将它们视为接触的开始和结束。如果你只关心碰撞的瞬间,那么你想要"body_entered"
:
func _on_body_entered(body:Node):
print("Collided with: ", body.name)
检测某个区域
区域节点是碰撞对象,但不是物理对象。它不会推动事物,事物也不会推动它。相反,他们穿过了。
他们默认监控碰撞(由monitoring
属性),并且还有"body_entered"
and "body_exited"
表明您可以使用与刚体中相同的方式。您可以设置一个collision_mask
来控制它。
此外,面积有"area_entered"
and "area_exited"
信号。也就是说,他们可以检测其他区域。它在哪里collision_layer
and monitorable
进来。
我会回到面积的使用。
Pickable
您还可以通过设置使用鼠标或定点设备拾取碰撞对象(静态、运动/角色、刚体或区域)input_pickable (or input_ray_pickable3D)到true
.
然后连接"input_event"
信号(或覆盖_input_event(…)
方法)的身体或区域来找出玩家何时点击它。
这就是如何_input_event(…)
对于 2D 节点,方法类似于:
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
这就是如何_input_event(…)
3D 节点的方法类似于:
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(…) and 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"
and "body_exited"
在您的运动或静态身体上。
设置看起来像这样:
Godot 3
KinematicBody2D
├ CollisionShape2D
├ Sprite
└ Area2D
└ CollisionShape2D
Godot 4
CharacterBody2D
├ CollisionShape2D
├ Sprite2D
└ Area2D
└ CollisionShape2D
信号从 Area2D 连接到 KinematicBody2D。
您可以为敌人添加一个区域并使其更大。这对于定义敌人可以检测到玩家的“视锥”很有用。您还可以将其与光线投射结合起来,以确保敌人具有视线。我推荐视频让敌人看到戈多的简单方法由 GDQuest 提供。
区域还允许您局部覆盖重力(由刚体使用)。为此,请使用“检查器”面板中“物理覆盖”下的属性。它们允许您拥有重力具有不同方向或强度的区域,甚至使重力点而不是方向。
使用可收集物体的区域也是一个好主意,这不应引起任何物理反应(无弹跳、推动等),但您仍然需要检测玩家何时与其碰撞。
当然,我认为区域的主要用途是:您可以在地图中定义区域,当玩家踩到该区域时,这些区域将触发某些事件(例如,玩家身后的门关闭)。
RayCast
检测碰撞RayCast
(2D
/3D
),确保其enabled
属性设置为true
。您还可以指定collision_mask
。并确保设置cast_to
一些明智的事情。
光线投射将允许您查询段中的物理对象从其位置到位置cast_to
指向(cast_to
应用了光线投射变换的向量。 IE。cast_to
是相对于光线投射的)。
注意:不,无限cast_to
无法工作。这不仅仅是一个性能问题。问题是无限向量在变换时(尤其是旋转时)表现不佳。
您可以致电is_colliding()
查明光线投射是否检测到某些东西。进而get_collider()
为拿到它,为实现它。Godot 每个物理帧更新一次。
Example:
if $RayCast.is_colliding():
print("Detected: ", $RayCast.get_collider)
如果您需要移动光线投射并在每个物理帧进行多次检测,则需要调用force_update_transform()
and force_raycast_update()
on it.
如果你想让敌人避免从平台上掉下来,你可以使用光线投射来检测前方是否有地面(example).
3D 游戏还可以使用光线投射来检测玩家正在查看的内容或玩家点击的内容。在 2D 中,您可能需要该方法intersect_point(…)
我在下面提到。
物理查询
有时我们想向 Godot 物理引擎询问一些东西,没有任何碰撞或额外的节点(例如区域和光线投射)。
首先,move_and_collide(…)
has a test_only
参数,如果设置为true
,将为您提供碰撞信息,但不会实际移动运动学/角色身体。
其次,你的RigidBody2D
have a test_motion(…)
方法将告诉您刚体是否会发生碰撞或未给出运动矢量。
但是,第三……我们不需要专门的人RayCast
(2D
/3D
)。我们做得到:
- 3D:
get_world().direct_space_state.intersect_ray(start, end)
. See PhysicsDirectSpaceState(戈多 3)或PhysicsDirectSpaceState3D(戈多4)。
- 2D:
get_world_2D().direct_space_state.intersect_ray(start, end)
. See Physics2DDirectSpaceState(戈多 3)或PhysicsDirectSpaceState2D(戈多4)。
在戈多3中,2D版本direct_space_state
还给你intersect_point(…)这将允许您检查特定点上的物理对象还有一个intersect_point_on_canvas(…),它允许您指定一个画布 ID,用于匹配CanvasLayer.
在 Godot 4 中,2D 和 3D 版本direct_space_state
have intersect_point
. And intersect_point_on_canvas
已被删除。
以及您在中找到的其他方法direct_space_state
是形状铸件。也就是说,它们不仅仅检查一个段(如光线投射)或一个点(如intersect_point(…)
确实如此),但有一个形状。
教程和资源
看文章“教程和资源”关于 Godot 的官方文档。
关于调试的注意事项
虽然我上面所说的一切都是当出现问题时检查它们是否正确的事情。有一些调试工具和技术需要注意。
首先,您可以设置断点(使用 F9 或使用breakpoint
关键字)并单步执行代码(F10 跳过,F11 进入)。
特别是对于调试物理,您需要从调试菜单中打开“可见碰撞形状”。
此外,当 Godot 运行时,您可以转到“场景”面板,然后选择“远程”选项卡,这将允许您查看和编辑当前运行的游戏中的节点。
您还可以使用“项目相机覆盖”选项(工具栏上的相机图标在游戏未运行时禁用)从编辑器控制正在运行的游戏的相机。
最后,您可能熟悉使用print
作为调试工具。它允许您记录事件发生的时间和变量的值。视觉上的等价物是生成视觉对象(精灵或网格实例),这些对象向您显示触发事件的时间、地点和事件(可能使用颜色来传达额外信息)。由于 Godot 3.5 包括Label3D
你也可以用它来写一些值。
复制代码的注意事项
您复制的代码不起作用的原因可能包括场景树的设置不同,或者某些信号未连接。然而,它也可能是空白。在 GDScript 中,空格很重要(您可以查看GDScript 基础知识)。您可能需要调整缩进级别才能使其与您的代码兼容。也不要混合使用制表符和空格,特别是在旧版本的 Godot 中。
关于解释问题的说明
所以你复制的代码不起作用。这意味着什么?它是做错了事还是什么也没做?是否有任何错误或警告? “它不起作用”并不是描述问题的好方法。
如果您有想要解决的问题(例如某些不起作用的代码),这些问答网站会更好地工作。
我想鼓励指出具体问题。可以作为此网站或类似网站上的新问题,也可以作为对给您带来麻烦的答案或教程的作者的评论。所以,是的,如果这里有什么不起作用,请在评论中告诉我,我会改进答案。但也要去打扰那些提供了你复制的不起作用的代码的人(即使那又是我)。给他们施加一些压力,让他们进步。