设计小怪场景

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

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

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

../../_images/drag_drop_mob.webp

你可以将新创建的 mob 节点重命名成 Character

image0

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

image1

添加一个 CollisionShape3D

image2

检查器中为 Shape(形状)属性分配一个 BoxShape3D

../../_images/08.create_box_shape3D.jpg

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

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

image4

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

移除离屏的怪物

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

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

备注

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

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

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

image5

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

image6

为小怪的移动编写代码

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

Mob 附加脚本。

image7

这是最初的移动代码。我们定义了两个属性 min_speedmax_speed(最小速度和最大速度)来定义随机速度的范围,后面我们会用这两个属性来定义 CharacterBody3D.velocity

GDScriptC#

  1. extends CharacterBody3D
  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. func _physics_process(_delta):
  7. move_and_slide()
  1. using Godot;
  2. public partial class Mob : CharacterBody3D
  3. {
  4. // Don't forget to rebuild the project so the editor knows about the new export variable.
  5. // Minimum speed of the mob in meters per second
  6. [Export]
  7. public int MinSpeed { get; set; } = 10;
  8. // Maximum speed of the mob in meters per second
  9. [Export]
  10. public int MaxSpeed { get; set; } = 18;
  11. public override void _PhysicsProcess(double delta)
  12. {
  13. MoveAndSlide();
  14. }
  15. }

与玩家类似,在每一帧我们都会通过调用 CharacterBody3D.move_and_slide() 方法来移动小怪。这一回,我们不会再每帧更新 velocity 了:我们希望怪物匀速移动,然后离开屏幕,即便碰到障碍物也一样。

我们需要再定义一个函数来计算初始的速度。这个函数会让怪物面朝玩家,并将其运动角度和速度随机化。

这个函数接受小怪的生成位置 start_position 以及玩家的位置 player_position 作为参数。

我们首先将小怪定位在 start_position 并用 look_at_from_position() 方法将它转向玩家,并通过围绕 Y 轴旋转随机量来随机化角度。下面,rand_range() 输出一个介于 -PI / 4 弧度和 PI / 4 弧度的随机值。

GDScriptC#

  1. # This function will be called from the Main scene.
  2. func initialize(start_position, player_position):
  3. # We position the mob by placing it at start_position
  4. # and rotate it towards player_position, so it looks at the player.
  5. look_at_from_position(start_position, player_position, Vector3.UP)
  6. # Rotate this mob randomly within range of -45 and +45 degrees,
  7. # so that it doesn't move directly towards the player.
  8. rotate_y(randf_range(-PI / 4, PI / 4))
  1. // This function will be called from the Main scene.
  2. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  3. {
  4. // We position the mob by placing it at startPosition
  5. // and rotate it towards playerPosition, so it looks at the player.
  6. LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
  7. // Rotate this mob randomly within range of -45 and +45 degrees,
  8. // so that it doesn't move directly towards the player.
  9. RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
  10. }

我们已经获取到了一个随机的位置,现在我们需要一个 random_speedrandi_range() 可以给我们需要的随机整数,并且我们要使用 min_speedmax_speedrandom_speed 是一个整数,我们只是使用它与我们的 CharacterBody3D.velocity 相乘。在乘完 random_speed 之后,我们将 random_speed 旋转至朝向玩家的方向。

GDScriptC#

  1. func initialize(start_position, player_position):
  2. # ...
  3. # We calculate a random speed (integer)
  4. var random_speed = randi_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 velocity vector based on the mob's Y rotation
  8. # in order to move in the direction the mob is looking.
  9. velocity = velocity.rotated(Vector3.UP, rotation.y)
  1. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  2. {
  3. // ...
  4. // We calculate a random speed (integer).
  5. int randomSpeed = GD.RandRange(MinSpeed, MaxSpeed);
  6. // We calculate a forward velocity that represents the speed.
  7. Velocity = Vector3.Forward * randomSpeed;
  8. // We then rotate the velocity vector based on the mob's Y rotation
  9. // in order to move in the direction the mob is looking.
  10. Velocity = Velocity.Rotated(Vector3.Up, Rotation.Y);
  11. }

离开屏幕

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

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

image8

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

image9

将信号连接到 Mob

image10

这样会使你回到脚本编辑器,并且添加一个新的函数: _on_visible_on_screen_notifier_3d_screen_exited()。请在里面调用 queue_free() 方法。这个函数会将调用它的实例销毁。

GDScriptC#

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

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

Here is the complete mob.gd script for reference.

GDScriptC#

  1. extends CharacterBody3D
  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. func _physics_process(_delta):
  7. move_and_slide()
  8. # This function will be called from the Main scene.
  9. func initialize(start_position, player_position):
  10. # We position the mob by placing it at start_position
  11. # and rotate it towards player_position, so it looks at the player.
  12. look_at_from_position(start_position, player_position, Vector3.UP)
  13. # Rotate this mob randomly within range of -45 and +45 degrees,
  14. # so that it doesn't move directly towards the player.
  15. rotate_y(randf_range(-PI / 4, PI / 4))
  16. # We calculate a random speed (integer)
  17. var random_speed = randi_range(min_speed, max_speed)
  18. # We calculate a forward velocity that represents the speed.
  19. velocity = Vector3.FORWARD * random_speed
  20. # We then rotate the velocity vector based on the mob's Y rotation
  21. # in order to move in the direction the mob is looking.
  22. velocity = velocity.rotated(Vector3.UP, rotation.y)
  23. func _on_visible_on_screen_notifier_3d_screen_exited():
  24. queue_free()
  1. using Godot;
  2. public partial class Mob : CharacterBody3D
  3. {
  4. // Minimum speed of the mob in meters per second.
  5. [Export]
  6. public int MinSpeed { get; set; } = 10;
  7. // Maximum speed of the mob in meters per second.
  8. [Export]
  9. public int MaxSpeed { get; set; } = 18;
  10. public override void _PhysicsProcess(double delta)
  11. {
  12. MoveAndSlide();
  13. }
  14. // This function will be called from the Main scene.
  15. public void Initialize(Vector3 startPosition, Vector3 playerPosition)
  16. {
  17. // We position the mob by placing it at startPosition
  18. // and rotate it towards playerPosition, so it looks at the player.
  19. LookAtFromPosition(startPosition, playerPosition, Vector3.Up);
  20. // Rotate this mob randomly within range of -45 and +45 degrees,
  21. // so that it doesn't move directly towards the player.
  22. RotateY((float)GD.RandRange(-Mathf.Pi / 4.0, Mathf.Pi / 4.0));
  23. // We calculate a random speed (integer).
  24. int randomSpeed = GD.RandRange(MinSpeed, MaxSpeed);
  25. // We calculate a forward velocity that represents the speed.
  26. Velocity = Vector3.Forward * randomSpeed;
  27. // We then rotate the velocity vector based on the mob's Y rotation
  28. // in order to move in the direction the mob is looking.
  29. Velocity = Velocity.Rotated(Vector3.Up, Rotation.Y);
  30. }
  31. // We also specified this function name in PascalCase in the editor's connection window.
  32. private void OnVisibilityNotifierScreenExited()
  33. {
  34. QueueFree();
  35. }
  36. }