设计小怪场景

在这一部分中,我们要为怪物编写代码,我们后续会称之为“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.webp

我们要调整一下它的大小,来更好地框住 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

extends CharacterBody3D

# 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


func _physics_process(_delta):
    move_and_slide()

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

我们需要定义另一个函数来计算 CharacterBody3D.velocity。该函数将怪物转向玩家,并随机化其运动角度和速度。

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

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

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -45 and +45 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

我们得到了一个随机位置,现在我们需要一个 random_speedrandi_range() 将很有用,因为它会给出随机的整数值,我们将使用 min_speedmax_speedrandom_speed 只是一个整数,我们只是用它来乘以我们的 CharacterBody3D.velocity。应用 random_speed 后,我们将 CharacterBody3D.velocity Vector3 旋转到朝向玩家的方向。

func initialize(start_position, player_position):
    # ...

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

离开屏幕

我们还需要在小怪离开屏幕后将其销毁。实现方法是将 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() 方法。这个函数会将调用它的实例销毁。

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

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

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

extends CharacterBody3D

# 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

func _physics_process(_delta):
    move_and_slide()

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -45 and +45 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()