生成怪物

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

image0

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

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

前往项目 -> 项目设置

image1

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

image2

创建生成路径

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

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

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

image3

添加占位圆柱体

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

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 号槽分配一个 SpatialMaterial

image13

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

展开 Albedo(反照率)部分,将颜色设为与背景色存在对比的颜色,比如亮橙色。

image14

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

image15

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

image16

备注

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

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

image17

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

image18

要对它的随机位置进行采样,我们需要一个 PathFollow 节点。添加 PathFollow 作为 Path 的子项。将两个节点分别重命名为 SpawnPathSpawnLocation。 它更明确地说明我们如何利用它们。

image19

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

随机生成怪物

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

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

然后,由于我们要按程序生成怪物,所以我们要在每次玩游戏时随机化数字。如果我们不这样做,怪物将总是按照相同的顺序产生。

GDScriptC#

  1. extends Node
  2. export (PackedScene) var mob_scene
  3. func _ready():
  4. randomize()
  1. public class Main : Node
  2. {
  3. // Don't forget to rebuild the project so the editor knows about the new export variable.
  4. #pragma warning disable 649
  5. // We assign this in the editor, so we don't need the warning about not being assigned.
  6. [Export]
  7. public PackedScene MobScene;
  8. #pragma warning restore 649
  9. public override void _Ready()
  10. {
  11. GD.Randomize();
  12. }
  13. }

我们希望以固定的时间间隔生成生物。为此,我们需要返回场景中并添加计时器。但是,在此之前,我们需要将 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_MobTimer_timeout() 函数。

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

  1. 实例化小怪的场景。

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

  3. 获取玩家的位置。

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

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

GDScriptC#

  1. func _on_MobTimer_timeout():
  2. # Create a new instance of the Mob scene.
  3. var mob = mob_scene.instance()
  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.unit_offset = randf()
  9. var player_position = $Player.transform.origin
  10. mob.initialize(mob_spawn_location.translation, player_position)
  11. add_child(mob)
  1. // We also specified this function name in PascalCase in the editor's connection window
  2. public void OnMobTimerTimeout()
  3. {
  4. // Create a new instance of the Mob scene.
  5. Mob mob = (Mob)MobScene.Instance();
  6. // Choose a random location on the SpawnPath.
  7. // We store the reference to the SpawnLocation node.
  8. var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
  9. // And give it a random offset.
  10. mobSpawnLocation.UnitOffset = GD.Randf();
  11. Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
  12. mob.Initialize(mobSpawnLocation.Translation, playerPosition);
  13. AddChild(mob);
  14. }

上面的 randf() 会生成 01 之间的随机数,也是 PathFollow 节点的 unit_offset(单位偏移量)所需的值。

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

GDScriptC#

  1. extends Node
  2. export (PackedScene) var mob_scene
  3. func _ready():
  4. randomize()
  5. func _on_MobTimer_timeout():
  6. var mob = mob_scene.instance()
  7. var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
  8. mob_spawn_location.unit_offset = randf()
  9. var player_position = $Player.transform.origin
  10. mob.initialize(mob_spawn_location.translation, player_position)
  11. add_child(mob)
  1. public class Main : Node
  2. {
  3. #pragma warning disable 649
  4. [Export]
  5. public PackedScene MobScene;
  6. #pragma warning restore 649
  7. public override void _Ready()
  8. {
  9. GD.Randomize();
  10. }
  11. public void OnMobTimerTimeout()
  12. {
  13. Mob mob = (Mob)MobScene.Instance();
  14. var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
  15. mobSpawnLocation.UnitOffset = GD.Randf();
  16. Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
  17. mob.Initialize(mobSpawnLocation.Translation, playerPosition);
  18. AddChild(mob);
  19. }
  20. }

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

image25

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