设计小怪场景
在这一部分中,我们要为怪物编写代码,我们后续会称之为“mob”(小怪)。在下一节课中,我们会在游戏区域周围随机生成它们。
让我们在一个新场景中设计这些怪物。节点结构和 Player 场景类似。
还是用 KinematicBody 节点作为根节点来创建场景。命名为 Mob。添加一个 Spatial 节点作为其子项,将其命名为 Pivot。将 mob.glb
文件从文件系统面板拖放到 Pivot 上,这样就把怪物的 3D 模型添加到了场景之中。你可以把新创建的 mob 节点重命名为 Character。
我们的实体要添加碰撞形状后才能正常工作。右键单击场景的根节点 Mob,然后单击添加子节点。
添加 CollisionShape。
在检查器中为 Shape(形状)属性分配一个 BoxShape(盒子形状)。
我们要调整一下它的大小,来更好地框住 3D 模型。可以单击并拖动橙色的小点来进行。
碰撞盒应该接触地面,并且比模型稍微瘦一点点。即便玩家的球体只接触了这个碰撞盒的角落,物理引擎也会判定发生了碰撞。如果盒子比 3D 模型要大一点,你可能距离怪物还有一定的距离就死了,玩家就会觉得不公平。
请注意,我的盒子要比怪物稍高。在这个游戏里是没问题的,因为我们是从游戏场景的上方用固定角度观察的。碰撞形状不必精确匹配模型。决定碰撞形状形式和大小的关键是你在试玩游戏时的手感。
移除离屏的怪物
我们要在游戏关卡中按照一定的时间间隔刷怪。如果你不小心,它们的数量可能就会无限地增长下去,我们可不想那样。每个小怪实例都需要付出一定的内存和处理代价,我们不希望为屏幕之外的小怪买单。
怪物离开屏幕之后,我们就不再需要它了,所以我们可以把它删除。Godot 有一个可以检测对象离开屏幕的节点,VisibilityNotifier,我们就要用它来销毁我们的小怪。
备注
如果要在游戏中不断实例化同一种对象,可以通过一种叫“池化”(pooling)的技术来避免持续地创建和销毁实例。做法是预先创建一个该对象的数组,然后去不断地重用里面的元素。
使用 GDScript 时,你不必担心这个问题。用对象池的主要目的是避免 C# 或 Lua 等带垃圾回收的语言带来的停滞。GDScript 管理内存的技术和它们是不同的,用的是引用计数,不会产生那种问题。你可以在这里了解更多相关内容:内存管理。
选中 Mob 节点,并为其添加一个 VisibilityNotifier 作为子项。这回出现的就是一个粉色的框。这个框完全离开屏幕后,该节点就会发出信号。
使用橙色的点来调整大小,让它覆盖住整个 3D 模型。
为小怪的移动编写代码
让我们来实现怪物的运动。我们要分两步来实现。首先,我们要为 Mob 编写脚本,定义初始化怪物的函数。然后我们会在 Main 场景中编写随机刷怪的机制并进行调用。
为 Mob 附加脚本。
这是最初的移动代码。我们定义了两个属性 min_speed
和 max_speed
(最小速度和最大速度)来定义随机速度的范围。我们还定义并初始化了 velocity
(速度)。
GDScriptC#
extends KinematicBody
# Minimum speed of the mob in meters per second.
export var min_speed = 10
# Maximum speed of the mob in meters per second.
export var max_speed = 18
var velocity = Vector3.ZERO
func _physics_process(_delta):
move_and_slide(velocity)
public class Mob : KinematicBody
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed = 18;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
MoveAndSlide(_velocity);
}
}
与玩家类似,在每一帧我们都会通过调用 KinematicBody
的 move_and_slide()
方法来移动小怪。这一回,我们不会再每帧更新 velocity
了:我们希望怪物匀速移动,然后离开屏幕,即便碰到障碍物也一样。
你可能会看到 GDScript 会产生一个警告,说你没有使用 move_and_slide()` 的返回值。这是正常现象。你可以直接忽略这个警告,或者如果你想完全将其隐藏的话,就在 ``move_and_slide(velocity)
这一行的上一行加上 # warning-ignore:return_value_discarded
的注释。更多关于 GDScript 警告系统的内容请参阅 GDScript 警告系统。
我们需要再定义一个函数来计算初始的速度。这个函数会让怪物面朝玩家,并将其运动角度和速度随机化。
这个函数接受小怪的生成位置 start_position
以及 player_position
作为参数。
我们首先将小怪定位在 start_position
并用 look_at_from_position()
方法将它转向玩家,并通过围绕 Y 轴旋转随机量来随机化角度。下面,rand_range()
输出一个介于 -PI / 4
弧度和 PI / 4
弧度的随机值。
GDScriptC#
# We will call this function from the Main scene.
func initialize(start_position, player_position):
# We position the mob and turn it so that it looks at the player.
look_at_from_position(start_position, player_position, Vector3.UP)
# And rotate it randomly so it doesn't move exactly toward the player.
rotate_y(rand_range(-PI / 4, PI / 4))
// We will call this function from the Main scene
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
// We position the mob and turn it so that it looks at the player.
LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
// And rotate it randomly so it doesn't move exactly toward the player.
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
}
然后我们再次使用 rand_range()
来计算随机速度,用它来计算速度向量。
我们首先创建一个指向前方的 3D 向量,将其乘以我们的 random_speed
,最后使用 Vector3
类的 rotated()
方法进行旋转。
GDScriptC#
func initialize(start_position, player_position):
# ...
# We calculate a random speed.
var random_speed = rand_range(min_speed, max_speed)
# We calculate a forward velocity that represents the speed.
velocity = Vector3.FORWARD * random_speed
# We then rotate the vector based on the mob's Y rotation to move in the direction it's looking.
velocity = velocity.rotated(Vector3.UP, rotation.y)
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
// ...
// We calculate a random speed.
float randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
// We calculate a forward velocity that represents the speed.
_velocity = Vector3.Forward * randomSpeed;
// We then rotate the vector based on the mob's Y rotation to move in the direction it's looking
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
}
离开屏幕
我们还需要在小怪离开屏幕后将其销毁。实现方法是将 VisibilityNotifier 节点的 screen_exited
信号连接到 Mob 上。
单击编辑器顶部的 3D 标签回到 3D 视窗。你也可以按 Ctrl + F2(macOS 上则是 Alt + 2)。
选中 VisibilityNotifier 节点,然后在界面右侧打开节点面板。双击 screen_exited() 信号。
将信号连接到 Mob。
这样你就会被带回到脚本编辑器,并且帮你添加了一个新的函数 _on_VisibilityNotifier_screen_exited()
。请在里面调用 queue_free()
方法。这样 VisibilityNotifier 的框离开屏幕时就会将小怪的实例销毁。
GDScriptC#
func _on_VisibilityNotifier_screen_exited():
queue_free()
// We also specified this function name in PascalCase in the editor's connection window
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
我们的怪物已经准备好进入游戏了!在下一部分,你将在游戏关卡中生成怪物。
这是仅供参考的完整 Mob.gd
脚本。
GDScriptC#
extends KinematicBody
# Minimum speed of the mob in meters per second.
export var min_speed = 10
# Maximum speed of the mob in meters per second.
export var max_speed = 18
var velocity = Vector3.ZERO
func _physics_process(_delta):
move_and_slide(velocity)
func initialize(start_position, player_position):
look_at_from_position(start_position, player_position, Vector3.UP)
rotate_y(rand_range(-PI / 4, PI / 4))
var random_speed = rand_range(min_speed, max_speed)
velocity = Vector3.FORWARD * random_speed
velocity = velocity.rotated(Vector3.UP, rotation.y)
func _on_VisibilityNotifier_screen_exited():
queue_free()
public class Mob : KinematicBody
{
// Minimum speed of the mob in meters per second
[Export]
public int MinSpeed = 10;
// Maximum speed of the mob in meters per second
[Export]
public int MaxSpeed = 18;
private Vector3 _velocity = Vector3.Zero;
public override void _PhysicsProcess(float delta)
{
MoveAndSlide(_velocity);
}
// We will call this function from the Main scene
public void Initialize(Vector3 startPosition, Vector3 playerPosition)
{
LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
var randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
_velocity = Vector3.Forward * randomSpeed;
_velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
}
// We also specified this function name in PascalCase in the editor's connection window
public void OnVisibilityNotifierScreenExited()
{
QueueFree();
}
}