设计小怪场景

在这一部分中,我们要为怪物编写代码,我们后续会称之为“mob”(小怪)。在下一节课中,我们会在游戏区域周围随机生成它们。

让我们在一个新场景中设计这些怪物。节点结构和 Player 场景类似。

还是用 KinematicBody 节点作为根节点来创建场景。命名为 Mob。添加一个 Spatial 节点作为其子项,将其命名为 Pivot。将 mob.glb 文件从文件系统面板拖放到 Pivot 上,这样就把怪物的 3D 模型添加到了场景之中。你可以把新创建的 mob 节点重命名为 Character

image0

我们的实体要添加碰撞形状后才能正常工作。右键单击场景的根节点 Mob,然后单击添加子节点

image1

添加 CollisionShape

image2

检查器中为 Shape(形状)属性分配一个 BoxShape(盒子形状)。

image3

我们要调整一下它的大小,来更好地框住 3D 模型。可以单击并拖动橙色的小点来进行。

碰撞盒应该接触地面,并且比模型稍微瘦一点点。即便玩家的球体只接触了这个碰撞盒的角落,物理引擎也会判定发生了碰撞。如果盒子比 3D 模型要大一点,你可能距离怪物还有一定的距离就死了,玩家就会觉得不公平。

image4

请注意,我的盒子要比怪物稍高。在这个游戏里是没问题的,因为我们是从游戏场景的上方用固定角度观察的。碰撞形状不必精确匹配模型。决定碰撞形状形式和大小的关键是你在试玩游戏时的手感。

移除离屏的怪物

我们要在游戏关卡中按照一定的时间间隔刷怪。如果你不小心,它们的数量可能就会无限地增长下去,我们可不想那样。每个小怪实例都需要付出一定的内存和处理代价,我们不希望为屏幕之外的小怪买单。

怪物离开屏幕之后,我们就不再需要它了,所以我们可以把它删除。Godot 有一个可以检测对象离开屏幕的节点,VisibilityNotifier,我们就要用它来销毁我们的小怪。

备注

如果要在游戏中不断实例化同一种对象,可以通过一种叫“池化”(pooling)的技术来避免持续地创建和销毁实例。做法是预先创建一个该对象的数组,然后去不断地重用里面的元素。

使用 GDScript 时,你不必担心这个问题。用对象池的主要目的是避免 C# 或 Lua 等带垃圾回收的语言带来的停滞。GDScript 管理内存的技术和它们是不同的,用的是引用计数,不会产生那种问题。你可以在这里了解更多相关内容:内存管理

选中 Mob 节点,并为其添加一个 VisibilityNotifier 作为子项。这回出现的就是一个粉色的框。这个框完全离开屏幕后,该节点就会发出信号。

image5

使用橙色的点来调整大小,让它覆盖住整个 3D 模型。

image6

为小怪的移动编写代码

让我们来实现怪物的运动。我们要分两步来实现。首先,我们要为 Mob 编写脚本,定义初始化怪物的函数。然后我们会在 Main 场景中编写随机刷怪的机制并进行调用。

Mob 附加脚本。

image7

这是最初的移动代码。我们定义了两个属性 min_speedmax_speed(最小速度和最大速度)来定义随机速度的范围。我们还定义并初始化了 velocity(速度)。

GDScriptC#

  1. extends KinematicBody
  2. # Minimum speed of the mob in meters per second.
  3. export var min_speed = 10
  4. # Maximum speed of the mob in meters per second.
  5. export var max_speed = 18
  6. var velocity = Vector3.ZERO
  7. func _physics_process(_delta):
  8. move_and_slide(velocity)
  1. public class Mob : KinematicBody
  2. {
  3. // Don't forget to rebuild the project so the editor knows about the new export variable.
  4. // Minimum speed of the mob in meters per second
  5. [Export]
  6. public int MinSpeed = 10;
  7. // Maximum speed of the mob in meters per second
  8. [Export]
  9. public int MaxSpeed = 18;
  10. private Vector3 _velocity = Vector3.Zero;
  11. public override void _PhysicsProcess(float delta)
  12. {
  13. MoveAndSlide(_velocity);
  14. }
  15. }

与玩家类似,在每一帧我们都会通过调用 KinematicBodymove_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#

  1. # We will call this function from the Main scene.
  2. func initialize(start_position, player_position):
  3. # We position the mob and turn it so that it looks at the player.
  4. look_at_from_position(start_position, player_position, Vector3.UP)
  5. # And rotate it randomly so it doesn't move exactly toward the player.
  6. rotate_y(rand_range(-PI / 4, PI / 4))
  1. // We will call this function from the Main scene
  2. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  3. {
  4. // We position the mob and turn it so that it looks at the player.
  5. LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
  6. // And rotate it randomly so it doesn't move exactly toward the player.
  7. RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
  8. }

然后我们再次使用 rand_range() 来计算随机速度,用它来计算速度向量。

我们首先创建一个指向前方的 3D 向量,将其乘以我们的 random_speed,最后使用 Vector3 类的 rotated() 方法进行旋转。

GDScriptC#

  1. func initialize(start_position, player_position):
  2. # ...
  3. # We calculate a random speed.
  4. var random_speed = rand_range(min_speed, max_speed)
  5. # We calculate a forward velocity that represents the speed.
  6. velocity = Vector3.FORWARD * random_speed
  7. # We then rotate the vector based on the mob's Y rotation to move in the direction it's looking.
  8. velocity = velocity.rotated(Vector3.UP, rotation.y)
  1. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  2. {
  3. // ...
  4. // We calculate a random speed.
  5. float randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
  6. // We calculate a forward velocity that represents the speed.
  7. _velocity = Vector3.Forward * randomSpeed;
  8. // We then rotate the vector based on the mob's Y rotation to move in the direction it's looking
  9. _velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
  10. }

离开屏幕

我们还需要在小怪离开屏幕后将其销毁。实现方法是将 VisibilityNotifier 节点的 screen_exited 信号连接到 Mob 上。

单击编辑器顶部的 3D 标签回到 3D 视窗。你也可以按 Ctrl + F2(macOS 上则是 Alt + 2)。

image8

选中 VisibilityNotifier 节点,然后在界面右侧打开节点面板。双击 screen_exited() 信号。

image9

将信号连接到 Mob

image10

这样你就会被带回到脚本编辑器,并且帮你添加了一个新的函数 _on_VisibilityNotifier_screen_exited()。请在里面调用 queue_free() 方法。这样 VisibilityNotifier 的框离开屏幕时就会将小怪的实例销毁。

GDScriptC#

  1. func _on_VisibilityNotifier_screen_exited():
  2. queue_free()
  1. // We also specified this function name in PascalCase in the editor's connection window
  2. public void OnVisibilityNotifierScreenExited()
  3. {
  4. QueueFree();
  5. }

我们的怪物已经准备好进入游戏了!在下一部分,你将在游戏关卡中生成怪物。

这是仅供参考的完整 Mob.gd 脚本。

GDScriptC#

  1. extends KinematicBody
  2. # Minimum speed of the mob in meters per second.
  3. export var min_speed = 10
  4. # Maximum speed of the mob in meters per second.
  5. export var max_speed = 18
  6. var velocity = Vector3.ZERO
  7. func _physics_process(_delta):
  8. move_and_slide(velocity)
  9. func initialize(start_position, player_position):
  10. look_at_from_position(start_position, player_position, Vector3.UP)
  11. rotate_y(rand_range(-PI / 4, PI / 4))
  12. var random_speed = rand_range(min_speed, max_speed)
  13. velocity = Vector3.FORWARD * random_speed
  14. velocity = velocity.rotated(Vector3.UP, rotation.y)
  15. func _on_VisibilityNotifier_screen_exited():
  16. queue_free()
  1. public class Mob : KinematicBody
  2. {
  3. // Minimum speed of the mob in meters per second
  4. [Export]
  5. public int MinSpeed = 10;
  6. // Maximum speed of the mob in meters per second
  7. [Export]
  8. public int MaxSpeed = 18;
  9. private Vector3 _velocity = Vector3.Zero;
  10. public override void _PhysicsProcess(float delta)
  11. {
  12. MoveAndSlide(_velocity);
  13. }
  14. // We will call this function from the Main scene
  15. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  16. {
  17. LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
  18. RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
  19. var randomSpeed = (float)GD.RandRange(MinSpeed, MaxSpeed);
  20. _velocity = Vector3.Forward * randomSpeed;
  21. _velocity = _velocity.Rotated(Vector3.Up, Rotation.y);
  22. }
  23. // We also specified this function name in PascalCase in the editor's connection window
  24. public void OnVisibilityNotifierScreenExited()
  25. {
  26. QueueFree();
  27. }
  28. }