生成怪物
在这一部分中,我们将沿着一条路径随机刷怪。在最后,怪物们就会在游戏区域到处乱跑了。
双击文件系统面板中的 Main.tscn
打开 Main
场景。
在绘制路径之前,我们要修改游戏的分辨率。我们的游戏默认的窗口大小是 1152x648
。我们要把它设成 720x540
,一个小巧的方块。
前往项目 -> 项目设置。
如果你仍然打开着*输入映射*,请切换到*通用*页签。
在左侧菜单中,找到 Display -> Window(显示 -> 窗口)。在右侧将 Width(宽度)设为 720
、Height(高度)设为 540
。
创建生成路径
与 2D 游戏教程中所做的一样,你要设计一条路径,使用 PathFollow3D 节点在路径上随机取位置。
不过在 3D 中,路径绘制起来会有一点复杂。我们希望它是围绕着游戏视图的,这样怪物就会在屏幕外出现。但绘制的路径也同样不会在摄像机预览中出现。
我们可以用一些占位网格来确定视图的界限。你的视口应该还是分成两个部分的,底部是相机预览。如果不是的话,请按 Ctrl + 2(macOS 上则是 Cmd + 2)将视图一分为二。选中 Camera3D 节点,然后点击底部视口的预览复选框。
添加占位圆柱体
让我们来添加一些占位网格。为 Main
节点新建一个 Node3D 节点作为子项,命名为 Cylinders
。我们会用它将圆柱体进行分组。添加一个 MeshInstance3D 节点作为其子项
在检查器中,为 Mesh(网格)属性赋值 CylinderMesh(圆柱体网格)。
使用视口左上角的菜单,将上面的视口设为正交顶视图。或者你也可以按小键盘的 7。
地面栅格可能有一点分散注意力。你可以在工具栏的视图菜单中点击查看栅格进行开关。
你现在要沿着地平面移动圆柱体,看底部视口的相机预览。我推荐使用网格捕捉来做这件事。你可以通过点击工具栏上的磁铁图标或按 Y 键来切换。
将圆柱体移到相机视图的左上角,使其正好在视野之外。
我们将创建网格的副本,并将它们放置在游戏区域周围。按 Ctrl + D(在 macOS 上则为 Cmd + D)来复制节点。你也可以在场景面板中右击节点,选择制作副本。沿着蓝色 Z 轴向下移动副本,直到它正好在摄像机的预览范围之外。
按住 Shift 键选择两个圆柱体,并点击未选择的那个圆柱体,然后复制它们。
拖拽红色的 X 轴,将它们移动到右侧。
白色的有点难以看清是吧?让我们给它们一个全新的材质,让它们凸显出来。
在 3D 中,材质可以定义表面的外观属性,比如颜色、如何反射光照等。我们可以用材质来修改网格的颜色。
我们可以同时更新所有四个圆柱体。在场景面板中选中所有网格实例。要实现全选,可以先点击第一个,然后按住 Shift 点击最后一个。
在检查器中,展开 Material(材质)部分,为 0 号插槽分配一个 StandardMaterial3D。
点击球体图标来打开材质资源。你会看到材质的预览和一长串充满属性的部分。你可以用这些来创建各种表面,从金属到岩石或水。
展开 Albedo(反照率)部分。
将颜色设为与背景色存在对比的颜色,比如亮橙色。
我们现在可以使用圆柱体作为参考。点击它们旁边的灰箭头,将它们折叠在场景面板中。你也可以通过点击 Cylinders 旁边的眼睛图标来切换它们的可见性。
添加一个 Path3D 节点作为 Main
的子节点。在工具栏中会出现四个图标。点击添加点工具,即带有绿色“+”号的图标。
备注
鼠标悬停在任意图标上,就可以看到描述该工具的工具提示。
单击每个圆柱体的中心以创建一个点。然后,单击工具栏中的闭合曲线图标以关闭路径。如果有任何一点偏离,你可以单击并拖动它以重新定位它。
你的路径看起来应该类似这样。
要对它的随机位置进行采样,我们需要一个 PathFollow3D 节点。添加 PathFollow3D 作为 Path3D
的子项。将两个节点分别重命名为 SpawnLocation
和 SpawnPath
。 这两个名字能够更明确地说明用途。
这样,我们就可以着手编写刷怪机制了。
随机生成怪物
右键点击 Main
节点,为它附加一个新脚本。
我们首先将一个变量导出到检查器中,这样我们就可以把 mob.tscn
或者其他任何怪物赋值给它。
GDScriptC#
extends Node
@export var mob_scene: PackedScene
using Godot;
public partial class Main : Node
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
[Export]
public PackedScene MobScene { get; set; }
}
我们希望以固定的时间间隔生成生物。为此,我们需要返回场景中并添加计时器。但是,在此之前,我们需要将 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_mob_timer_timeout()
函数。
让我们来编写刷怪的逻辑吧。我们要做的是:
实例化小怪的场景。
在生成路径上随机选取一个位置。
获取玩家的位置。
调用小怪的
initialize()
方法,传入随机位置和玩家的位置。将小怪添加为 Main 节点的子节点。
GDScriptC#
func _on_mob_timer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instantiate()
# 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.progress_ratio = randf()
var player_position = $Player.position
mob.initialize(mob_spawn_location.position, player_position)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
// We also specified this function name in PascalCase in the editor's connection window.
private void OnMobTimerTimeout()
{
// Create a new instance of the Mob scene.
Mob mob = MobScene.Instantiate<Mob>();
// Choose a random location on the SpawnPath.
// We store the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow3D>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.ProgressRatio = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Position;
mob.Initialize(mobSpawnLocation.Position, playerPosition);
// Spawn the mob by adding it to the Main scene.
AddChild(mob);
}
正如上面所示, randf ()
会生成一个介于 0
和 1
之间的随机值,这个数值是 PathFollow 节点的 progress_ratio
属性所期望的:0 代表路径的开始点,1 代表路径的终点。 我们之前设置的路径是围绕着相机视口的,因此任何 0 到 1 之间的随机值都代表着沿着视口边缘的随机位置!
注意:如果你从主场景中移除了 Player
,那接下来的几行会
GDScriptC#
var player_position = $Player.position
Vector3 playerPosition = GetNode<Player>("Player").Position;
由于此处没有 $Player 导致报错!
这是目前完整的 main.gd
脚本,仅供参考。
GDScriptC#
extends Node
@export var mob_scene: PackedScene
func _on_mob_timer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instantiate()
# 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.progress_ratio = randf()
var player_position = $Player.position
mob.initialize(mob_spawn_location.position, player_position)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
using Godot;
public partial class Main : Node
{
[Export]
public PackedScene MobScene { get; set; }
private void OnMobTimerTimeout()
{
// Create a new instance of the Mob scene.
Mob mob = MobScene.Instantiate<Mob>();
// Choose a random location on the SpawnPath.
// We store the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow3D>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.ProgressRatio = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Position;
mob.Initialize(mobSpawnLocation.Position, playerPosition);
// Spawn the mob by adding it to the Main scene.
AddChild(mob);
}
}
按 F6 即可测试该场景。你应该会看到有怪物刷了出来,然后会进行直线运动。
目前,它们会在路线的交叉点撞到一起滑来滑去。我们会在下一部分解决这个问题。