生成怪物
在这一部分中,我们将沿着一条路径随机刷怪。在最后,怪物们就会在游戏区域到处乱跑了。
双击文件系统停靠面板中的 Main.tscn
打开 Main 场景。
在绘制路径之前,我们要修改游戏的分辨率。我们的游戏默认的窗口大小是 1024x600
。我们要把它设成 720x540
,一个小巧的方块。
前往项目 -> 项目设置。
在左侧菜单中,找到 Display -> Window(显示 -> 窗口)。在右侧将 Width(宽度)设为 720
、Height(高度)设为 540
。
创建生成路径
与 2D 游戏教程中所做的一样,你要设计一条路径,使用 PathFollow 节点在路径上随机取位置。
不过在 3D 中,路径绘制起来会有一点复杂。我们希望它是围绕着游戏视图的,这样怪物就会在屏幕外出现。但绘制的路径也同样不会在摄像机预览中出现。
我们可以用一些占位网格来确定视图的界限。你的视窗应该还是分成两个部分的,底部是摄像机预览。如果不是的话,请按 Ctrl + 2(macOS 上则是 Cmd + 2)将视图一分为二。选中 Camera 节点,然后点击底部视窗的预览复选框。
添加占位圆柱体
让我们来添加一些占位网格。为 Main 节点新建一个 Spatial 节点作为子项,命名为 Cylinders(圆柱体们)。我们会用它将圆柱体进行分组。添加一个 MeshInstance 节点作为其子项。
在检查器中,为 Mesh(网格)属性赋值 CylinderMesh(圆柱体网格)。
使用视窗左上角的菜单,将上面的视窗设为正交顶视图。或者你也可以按小键盘的 7。
我觉得地面网格有一点分散注意力。你可以在工具栏的视图菜单中点击查看网格进行开关。
你现在要沿着地平面移动圆柱体,看底部视口的相机预览。我推荐使用网格捕捉来做这件事。你可以通过点击工具栏上的磁铁图标或按 Y 键来切换。
将圆柱体刚好放在摄像机视图的左上角之外。
我们将创建网格的副本,并将它们放置在游戏区域周围。按 Ctrl + D(在 macOS 上则为 Cmd + D)来复制节点。你也可以在场景面板中右击节点,选择制作副本。沿着蓝色 Z 轴向下移动副本,直到它正好在摄像机的预览范围之外。
按住 Shift 键选择两个圆柱体,并点击未选择的那个圆柱体,然后复制它们。
拖拽红色的 X 轴,将它们移动到右侧。
白色的有点难以看清是吧?让我们给它们一个全新的材质,让它们凸显出来。
在 3D 中,材质可以定义表面的外观属性,比如颜色、如何反射光照等。我们可以用材质来修改网格的颜色。
我们可以同时更新所有四个圆柱体。在场景面板中选中所有网格实例。要实现全选,可以先点击第一个,然后按住 Shift 点击最后一个。
在检查器中,展开 Material(材质)部分,为 0 号槽分配一个 SpatialMaterial。
点击球体图标来打开材质资源。你会看到材质的预览和一长串充满属性的部分。你可以用这些来创建各种表面,从金属到岩石或水。
展开 Albedo(反照率)部分,将颜色设为与背景色存在对比的颜色,比如亮橙色。
我们现在可以使用圆柱体作为参考。点击它们旁边的灰箭头,将它们折叠在场景面板中。你也可以通过点击 Cylinders 旁边的眼睛图标来切换它们的可见性。
添加一个 Path 节点作为 Main 的一个子节点。在工具栏中,出现四个图标。点击添加点工具,即带有绿色“+”号的图标。
备注
鼠标悬停在任意图标上,就可以看到描述该工具的工具提示。
单击每个圆柱体的中心以创建一个点。然后,单击工具栏中的闭合曲线图标以关闭路径。如果有任何一点偏离,您可以单击并拖动它以重新定位它。
你的路径看起来应该类似这样。
要对它的随机位置进行采样,我们需要一个 PathFollow 节点。添加 PathFollow 作为 Path 的子项。将两个节点分别重命名为 SpawnPath 和 SpawnLocation。 它更明确地说明我们如何利用它们。
这样,我们就可以着手编写刷怪机制了。
随机生成怪物
右键点击 Main 节点,为它附加一个新脚本。
我们首先将一个变量导出到检查器中,这样我们就可以把 Mob.tscn
或者其他任何怪物赋值给它。
然后,由于我们要按程序生成怪物,所以我们要在每次玩游戏时随机化数字。如果我们不这样做,怪物将总是按照相同的顺序产生。
GDScriptC#
extends Node
export (PackedScene) var mob_scene
func _ready():
randomize()
public class Main : Node
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
#pragma warning disable 649
// We assign this in the editor, so we don't need the warning about not being assigned.
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public override void _Ready()
{
GD.Randomize();
}
}
我们希望以固定的时间间隔生成生物。为此,我们需要返回场景中并添加计时器。但是,在此之前,我们需要将 Mob.tscn
文件分配给 mob_scene
属性。
回到 3D 屏幕,选择 Main 节点。将 Mob.tscn
从文件系统面板拖到检查器的 Mob Scene 槽中。
为 Main 新建一个 Timer 节点作为子节点。将其命名为 MobTimer。
在检查器中,将其 Wait Time(等待时间)设为 0.5
秒,然后打开 Autostart(自动开始),这样我们运行游戏它就会自动开始。
计时器在每次到达 Wait Time 时都会发出 timeout
信号。计时器默认会自动重启,循环触发信号。我们可以将 Main 节点连接到这个信号,每 0.5
秒生成一只怪物。
保持选中 MobTimer,在右侧的节点面板中双击 timeout
信号。
将它连接到 Main 节点。
然后你就会被带回脚本,其中新建了一个空的 _on_MobTimer_timeout()
函数。
让我们来编写刷怪的逻辑吧。我们要做的是:
实例化小怪的场景。
在生成路径上随机选取一个位置。
获取玩家的位置。
调用小怪的
initialize()
方法,传入随机位置和玩家的位置。将小怪添加为 Main 节点的子节点。
GDScriptC#
func _on_MobTimer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instance()
# Choose a random location on the SpawnPath.
# We store the reference to the SpawnLocation node.
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
# And give it a random offset.
mob_spawn_location.unit_offset = randf()
var player_position = $Player.transform.origin
mob.initialize(mob_spawn_location.translation, player_position)
add_child(mob)
// We also specified this function name in PascalCase in the editor's connection window
public void OnMobTimerTimeout()
{
// Create a new instance of the Mob scene.
Mob mob = (Mob)MobScene.Instance();
// Choose a random location on the SpawnPath.
// We store the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
AddChild(mob);
}
上面的 randf()
会生成 0
和 1
之间的随机数,也是 PathFollow 节点的 unit_offset
(单位偏移量)所需的值。
这是目前完整的 Main.gd
脚本,仅供参考。
GDScriptC#
extends Node
export (PackedScene) var mob_scene
func _ready():
randomize()
func _on_MobTimer_timeout():
var mob = mob_scene.instance()
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
mob_spawn_location.unit_offset = randf()
var player_position = $Player.transform.origin
mob.initialize(mob_spawn_location.translation, player_position)
add_child(mob)
public class Main : Node
{
#pragma warning disable 649
[Export]
public PackedScene MobScene;
#pragma warning restore 649
public override void _Ready()
{
GD.Randomize();
}
public void OnMobTimerTimeout()
{
Mob mob = (Mob)MobScene.Instance();
var mobSpawnLocation = GetNode<PathFollow>("SpawnPath/SpawnLocation");
mobSpawnLocation.UnitOffset = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Transform.origin;
mob.Initialize(mobSpawnLocation.Translation, playerPosition);
AddChild(mob);
}
}
按 F6 即可测试该场景。你应该会看到有怪物刷了出来,然后会进行直线运动。
目前,它们会在路线的交叉点撞到一起滑来滑去。我们会在下一部分解决这个问题。