生成怪物

在这一部分中,我们将沿着一条路径随机刷怪。在最后,怪物们就会在游戏区域到处乱跑了。

image0

双击文件系统面板中的 Main.tscn 打开 Main 场景。

在绘制路径之前,我们要修改游戏的分辨率。我们的游戏默认的窗口大小是 1152x648。我们要把它设成 720x540,一个小巧的方块。

前往项目 -> 项目设置

image1

如果你仍然打开着*输入映射*,请切换到*通用*页签。

在左侧菜单中,找到 Display -> Window(显示 -> 窗口)。在右侧将 Width(宽度)设为 720Height(高度)设为 540

image2

创建生成路径

与 2D 游戏教程中所做的一样,你要设计一条路径,使用 PathFollow3D 节点在路径上随机取位置。

不过在 3D 中,路径绘制起来会有一点复杂。我们希望它是围绕着游戏视图的,这样怪物就会在屏幕外出现。但绘制的路径也同样不会在摄像机预览中出现。

我们可以用一些占位网格来确定视图的界限。你的视口应该还是分成两个部分的,底部是相机预览。如果不是的话,请按 Ctrl + 2(macOS 上则是 Cmd + 2)将视图一分为二。选中 Camera3D 节点,然后点击底部视口的预览复选框。

image3

添加占位圆柱体

让我们来添加一些占位网格。为 Main 节点新建一个 Node3D 节点作为子项,命名为 Cylinders。我们会用它将圆柱体进行分组。添加一个 MeshInstance3D 节点作为其子项

image4

检查器中,为 Mesh(网格)属性赋值 CylinderMesh(圆柱体网格)。

image5

使用视口左上角的菜单,将上面的视口设为正交顶视图。或者你也可以按小键盘的 7。

image6

地面栅格可能有一点分散注意力。你可以在工具栏的视图菜单中点击查看栅格进行开关。

image7

你现在要沿着地平面移动圆柱体,看底部视口的相机预览。我推荐使用网格捕捉来做这件事。你可以通过点击工具栏上的磁铁图标或按 Y 键来切换。

image8

将圆柱体移到相机视图的左上角,使其正好在视野之外。

image9

我们将创建网格的副本,并将它们放置在游戏区域周围。按 Ctrl + D(在 macOS 上则为 Cmd + D)来复制节点。你也可以在场景面板中右击节点,选择制作副本。沿着蓝色 Z 轴向下移动副本,直到它正好在摄像机的预览范围之外。

按住 Shift 键选择两个圆柱体,并点击未选择的那个圆柱体,然后复制它们。

image10

拖拽红色的 X 轴,将它们移动到右侧。

image11

白色的有点难以看清是吧?让我们给它们一个全新的材质,让它们凸显出来。

在 3D 中,材质可以定义表面的外观属性,比如颜色、如何反射光照等。我们可以用材质来修改网格的颜色。

我们可以同时更新所有四个圆柱体。在场景面板中选中所有网格实例。要实现全选,可以先点击第一个,然后按住 Shift 点击最后一个。

image12

检查器中,展开 Material(材质)部分,为 0 号插槽分配一个 StandardMaterial3D

image13

../../_images/standard_material.webp

点击球体图标来打开材质资源。你会看到材质的预览和一长串充满属性的部分。你可以用这些来创建各种表面,从金属到岩石或水。

展开 Albedo(反照率)部分。

../../_images/albedo_section.webp

将颜色设为与背景色存在对比的颜色,比如亮橙色。

image14

我们现在可以使用圆柱体作为参考。点击它们旁边的灰箭头,将它们折叠在场景面板中。你也可以通过点击 Cylinders 旁边的眼睛图标来切换它们的可见性。

image15

添加一个 Path3D 节点作为 Main 的子节点。在工具栏中会出现四个图标。点击添加点工具,即带有绿色“+”号的图标。

image16

备注

鼠标悬停在任意图标上,就可以看到描述该工具的工具提示。

单击每个圆柱体的中心以创建一个点。然后,单击工具栏中的闭合曲线图标以关闭路径。如果有任何一点偏离,你可以单击并拖动它以重新定位它。

image17

你的路径看起来应该类似这样。

image18

要对它的随机位置进行采样,我们需要一个 PathFollow3D 节点。添加 PathFollow3D 作为 Path3D 的子项。将两个节点分别重命名为 SpawnLocationSpawnPath 。 这两个名字能够更明确地说明用途。

image19

这样,我们就可以着手编写刷怪机制了。

随机生成怪物

右键点击 Main 节点,为它附加一个新脚本。

我们首先将一个变量导出到检查器中,这样我们就可以把 mob.tscn 或者其他任何怪物赋值给它。

GDScriptC#

  1. extends Node
  2. @export var mob_scene: PackedScene
  1. using Godot;
  2. public partial class Main : Node
  3. {
  4. // Don't forget to rebuild the project so the editor knows about the new export variable.
  5. [Export]
  6. public PackedScene MobScene { get; set; }
  7. }

我们希望以固定的时间间隔生成生物。为此,我们需要返回场景中并添加计时器。但是,在此之前,我们需要将 mob.tscn 文件分配给 mob_scene 属性

回到 3D 屏幕,选中 Main 节点。将 mob.tscn文件系统面板拖到检查器Mob Scene 槽中。

image20

Main 新建一个 Timer 节点作为子节点。将其命名为 MobTimer

image21

检查器中,将其 Wait Time(等待时间)设为 0.5 秒,然后打开 Autostart(自动开始),这样我们运行游戏它就会自动开始。

image22

计时器在每次到达 Wait Time 时都会发出 timeout 信号。计时器默认会自动重启,循环触发信号。我们可以将 Main 节点连接到这个信号,每 0.5 秒生成一只怪物。

保持选中 MobTimer,在右侧的节点面板中双击 timeout 信号。

image23

将它连接到 Main 节点。

image24

这样你就会被带回脚本,其中新建了一个空的 _on_mob_timer_timeout() 函数。

让我们来编写刷怪的逻辑吧。我们要做的是:

  1. 实例化小怪的场景。

  2. 在生成路径上随机选取一个位置。

  3. 获取玩家的位置。

  4. 调用小怪的 initialize() 方法,传入随机位置和玩家的位置。

  5. 将小怪添加为 Main 节点的子节点。

GDScriptC#

  1. func _on_mob_timer_timeout():
  2. # Create a new instance of the Mob scene.
  3. var mob = mob_scene.instantiate()
  4. # Choose a random location on the SpawnPath.
  5. # We store the reference to the SpawnLocation node.
  6. var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
  7. # And give it a random offset.
  8. mob_spawn_location.progress_ratio = randf()
  9. var player_position = $Player.position
  10. mob.initialize(mob_spawn_location.position, player_position)
  11. # Spawn the mob by adding it to the Main scene.
  12. add_child(mob)
  1. // We also specified this function name in PascalCase in the editor's connection window.
  2. private void OnMobTimerTimeout()
  3. {
  4. // Create a new instance of the Mob scene.
  5. Mob mob = MobScene.Instantiate<Mob>();
  6. // Choose a random location on the SpawnPath.
  7. // We store the reference to the SpawnLocation node.
  8. var mobSpawnLocation = GetNode<PathFollow3D>("SpawnPath/SpawnLocation");
  9. // And give it a random offset.
  10. mobSpawnLocation.ProgressRatio = GD.Randf();
  11. Vector3 playerPosition = GetNode<Player>("Player").Position;
  12. mob.Initialize(mobSpawnLocation.Position, playerPosition);
  13. // Spawn the mob by adding it to the Main scene.
  14. AddChild(mob);
  15. }

正如上面所示, randf () 会生成一个介于 01 之间的随机值,这个数值是 PathFollow 节点的 progress_ratio 属性所期望的:0 代表路径的开始点,1 代表路径的终点。 我们之前设置的路径是围绕着相机视口的,因此任何 0 到 1 之间的随机值都代表着沿着视口边缘的随机位置!

注意:如果你从主场景中移除了 Player,那接下来的几行会

GDScriptC#

  1. var player_position = $Player.position
  1. Vector3 playerPosition = GetNode<Player>("Player").Position;

由于此处没有 $Player 导致报错!

这是目前完整的 main.gd 脚本,仅供参考。

GDScriptC#

  1. extends Node
  2. @export var mob_scene: PackedScene
  3. func _on_mob_timer_timeout():
  4. # Create a new instance of the Mob scene.
  5. var mob = mob_scene.instantiate()
  6. # Choose a random location on the SpawnPath.
  7. # We store the reference to the SpawnLocation node.
  8. var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
  9. # And give it a random offset.
  10. mob_spawn_location.progress_ratio = randf()
  11. var player_position = $Player.position
  12. mob.initialize(mob_spawn_location.position, player_position)
  13. # Spawn the mob by adding it to the Main scene.
  14. add_child(mob)
  1. using Godot;
  2. public partial class Main : Node
  3. {
  4. [Export]
  5. public PackedScene MobScene { get; set; }
  6. private void OnMobTimerTimeout()
  7. {
  8. // Create a new instance of the Mob scene.
  9. Mob mob = MobScene.Instantiate<Mob>();
  10. // Choose a random location on the SpawnPath.
  11. // We store the reference to the SpawnLocation node.
  12. var mobSpawnLocation = GetNode<PathFollow3D>("SpawnPath/SpawnLocation");
  13. // And give it a random offset.
  14. mobSpawnLocation.ProgressRatio = GD.Randf();
  15. Vector3 playerPosition = GetNode<Player>("Player").Position;
  16. mob.Initialize(mobSpawnLocation.Position, playerPosition);
  17. // Spawn the mob by adding it to the Main scene.
  18. AddChild(mob);
  19. }
  20. }

按 F6 即可测试该场景。你应该会看到有怪物刷了出来,然后会进行直线运动。

image25

目前,它们会在路线的交叉点撞到一起滑来滑去。我们会在下一部分解决这个问题。