通过制作 2D 平台游戏来学习 Godot 4 — 第 8 部分:炸弹生成器
现在我们的游戏中有一个炸弹,但我们仍然需要一个刷怪箱。刷怪箱将在特定路径上生成炸弹 — 每次炸弹与玩家或墙壁/瓦片地图碰撞时,它都会重新生成一个新的炸弹。不是我们自己在地图上放置炸弹,而是刷怪箱会为我们处理它。
您将在这部分中学到什么:
如何使用 Path2D 节点。
如何使用 PathFollow2D 节点。
如何使用 AnimationPlayer 节点。
我们将使用 Path2D 节点为炸弹创建预定义路径。这个 Path2D 节点允许我们在地图上绘制点,这些点将用作我们的炸弹路径。创建路径点后,我们将使用 PathFollow2D 节点沿此路径移动生成的 Bomb 场景。我们需要使用 AnimationPlayer 节点来为炸弹沿着此路径移动的动画。
让我们为 Bomb Spawner 创建一个新场景!我们的根节点应该是 Node2D 节点 — 您可以将其重命名为 “BombSpawner”。将此场景保存在 Scenes 文件夹下。
这个 BombSpawner 场景需要一个 Timer 节点,每次炸弹数量低于零时,该节点都会生成一个炸弹(如果没有炸弹,计时器将生成一个新的炸弹)。
在 Timer 节点的 Inspector 面板中,我们需要启用 “Autostart” 选项。当 BombSpawner 进入 Main scene 树时,这将自动启动计时器。另一个选项“one-shot”触发计时器一次,计时器将在达到 0 时停止。
最后,此场景将需要一个 AnimatedSprite2D 节点。此 AnimatedSprite2D 节点将包含将发射炸弹的大炮的动画。我们将在下一部分添加 cannon handler!
让我们为大炮创建两个新的动画,分别名为 “cannon_idle” 和 “cannon_fired”。
对于我们的cannon_idle动画,请导航到“res://Assets/Kings and Pigs/Sprites/10-Cannon/Idle.png”,然后将唯一的帧添加到您的动画中。暂时将 looping 和 FPS 值保留为默认值。
对于我们的cannon_fired动画,请导航到“res://Assets/Kings and Pigs/Sprites/10-Cannon/Shoot (44x28).png”,并将所有 4 帧添加到动画中。将第一帧添加两次,以便 cannon 动画以相同的帧开始和结束。关闭循环,并将其 FPS 值更改为 4。
我们还希望大炮面向相反的方向(右),因此在 AnimatedSprite2D 节点的 Inspector 面板中,在 Offset 下启用“Flip H”属性。
我们稍后会回到 BombSpawner 场景,但现在,让我们回到主场景,并向场景添加新的 Node2D 节点。将其重命名为 “BombPath”。此外,删除您之前从场景中实例化的任何 Bomb 场景。
添加 Path2D 节点作为 BombPath 节点的子节点。此节点将允许我们为炸弹的移动绘制路径点。
如果选择 Path2D 节点,您将在 Inspector 面板中看到有一个添加 Points 的选项。如果向 Points 属性添加元素,您将看到它在游戏窗口的 (x: 0, y: 0) 处创建了一个菱形。
您可以将此点拖动到地图上您希望炸弹生成的任意位置。这是炸弹路径的起点。您可以稍后移动它。
如果添加另一个元素,它将创建另一个点。
您现在也可以单击线条来添加点,如果您想移动点,您只需选择它们并将它们拖动到您想要放置它们的位置即可。要删除某个点,您可以用鼠标右键单击它或在 Inspector 面板中将其删除。
让我们为炸弹创建一条从顶部到底部的路径。炸弹每次生成时都会沿着这条路径移动,如果它到达这条路径的尽头或与我们的玩家发生碰撞,它将从顶部重生并再次下降到终点。确保为玩家留出足够的空间以便能够跳过炸弹,如果炸弹与 Tilemap 碰撞(选项 #1)- 确保有足够的空间让炸弹流动而不会爆炸。
为了让我们的炸弹在生成后沿着这条路径移动,我们需要添加一个 PathFollow2D 节点作为 Path2D 节点的子节点。
我们还需要添加一个 AnimationPlayer 节点作为 Path2D 节点的子节点。动画播放器用于 Animation 资源的通用播放。它包含 AnimationLibrary 资源的字典和动画过渡之间的自定义混合时间。每当需要对非 sprite 项(如标签或颜色)进行动画处理时,都可以使用此 AnimationPlayer 节点。
我们在下面的 Animation 面板中向 AnimationPlayer 节点添加动画。
要添加新的 Animation,请单击 Animation 属性并选择 “New” 选项。将此动画称为 “bomb_movement”。
这将创建一个由四个部分组成的动画:
- 控件(即添加、加载、保存和删除动画)
- 曲目列表。
- 带有关键帧的时间轴 — 定义属性在某个时间点的值。
- 时间轴和轨道控件,例如,您可以在其中缩放时间轴和编辑轨道。您还可以通过按住 CRTL 并使用鼠标滚轮滚动来放大和缩小时间轴。
图 9:动画面板概述
让我们将动画长度更改为 5。这将是我们的炸弹从 Path2D 节点上的第一个点移动到最后一个点所需的时间。我们可以稍后更改此设置,使其变慢或更快。
让我们创建一个新的 Track。每个轨迹都存储指向节点及其 affected 属性的路径。点击 “Add Track” 并选择 “Property Track”。我们选择 Property Track 是因为我们想更改某个节点的属性值(我们可以在 Inspector 面板中找到的属性)。
我们希望对 PathFollow2D 节点的进度比率属性进行动画处理,以便我们可以根据该进度比率在路径上移动 Bomb。由于我们想要为 PathFollow2D 节点制作动画,因此我们需要在出现的弹出窗口中选择它。
现在,我们可以选择要进行动画处理的 PathFollow2D 节点的属性。选择 “progress ratio”。此进度比率将沿路径的距离称为 0.0(对于第一个顶点)到 1.0(对于最后一个顶点)范围内的数字。这只是表示 path 中进度的另一种方式,因为提供的 offset 在内部乘以 path 的长度。
让我们添加两个关键帧:一个位于动画时间轴的开头,另一个位于动画时间轴的结尾。要添加新的关键帧,请右键单击鼠标并选择 “Insert Key”。在时间轴上的点 0 和点 5 处添加键。0 是我们的开始位置,炸弹将在这里生成并开始移动,5 是结束位置,炸弹将被摧毁并停止移动。
现在,动画不会移动,因为进度比率正在从值 0 移动到值 0。如果选择关键帧,则可以在 Inspector 面板中看到此值。我们需要将关键帧 “5” 的 Value 属性更改为 1。这意味着我们的动画将从 0 到 1 播放,因此我们的炸弹将沿着路径移动,而不仅仅是卡在原地。选择放置在时间 5 上的关键帧,并将其值更改为 1。我们还不能测试这个动画,但我们需要它,否则我们的炸弹不会在我们的路径上移动!
在 Main_2 场景中的第二个关卡中重做与上述相同的步骤(针对我们的 BombPath)。
现在,回到我们的 BombSpawner 场景,我们需要向场景添加新脚本。将其保存在 Scripts 文件夹中。
在我们新创建的脚本中,我们需要定义一些变量。首先,我们需要创建对 Bomb 场景的引用。我们可以通过全局 preload 方法来实现这一点,该方法尽早加载我们的 Scene 资源,以提前加载 “loading”作,并避免在性能敏感代码中加载资源。预加载可确保在游戏开始之前将场景加载到内存中,这有助于避免以后的延迟。
### BombSpawner.gd
extends Node2D
#bomb scene reference
var bomb = preload("res://Scenes/Bomb.tscn")
由于我们有多个关卡,因此需要找到一种方法来动态更改当前场景的名称,以便我们可以相应地获取其路径。换句话说,如果我们在主场景中,我们希望炸弹在 “/root/Main/BombPath/Path2D/PathFollow2D” 路径上生成,如果我们在 Main_2 场景中,我们希望炸弹在 “/root/Main_2/BombPath/Path2D/PathFollow2D” 路径上生成。
我们可以使用 Global 脚本来跟踪当前处于活动状态的主场景。我们将使用 ready() 函数来获取 Player 所在的当前场景的名称(Main 或 Main_2)。我们将通过 current_scene() 对象来做到这一点。
### Global.gd
extends Node
#movement states
var is_attacking = false
var is_climbing = false
var is_jumping = false
#current scene
var current_scene_name
func _ready():
# Sets the current scene's name
current_scene_name = get_tree().get_current_scene().name
现在,回到我们的 BombSpawner 脚本中,让我们定义一些变量,这些变量将存储我们的current_scene_path值 (Main 或 Main_2)、我们的炸弹路径 (current_scene_path + /BombPath/Path2D/PathFollow2D) 和我们的炸弹动画路径 (current_scene_path + /BombPath/Path2D/AnimationPlayer)。
### Bombspawner.gd
extends Node2D
#Bomb scene reference
var bomb = preload("res://Scenes/Bomb.tscn")
#references to our scene, PathFollow2D path, and AnimationPlayer path
var current_scene_path
var bomb_path
var bomb_animation
我们将在 ready() 函数中启动这些变量的值。我们还需要将大炮的默认加载动画设置为 “cannon_idle”。
### Bombspawner.gd
#older code
#when it's loaded into the scene
func _ready():
#default animation on load
$AnimatedSprite2D.play("cannon_idle")
#initiates paths
current_scene_path = "/root/" + Global.current_scene_name + "/" #current scene
bomb_path = get_node(current_scene_path + "/BombPath/Path2D/PathFollow2D") #PathFollow2D
bomb_animation = get_node(current_scene_path + "/BombPath/Path2D/AnimationPlayer") #AnimationPlayer
要生成我们的炸弹,我们需要创建一个新函数,该函数将创建炸弹场景的实例并返回它。要创建场景引用的实例,我们使用 instantiate() 方法。我们还需要播放 cannon_fired 动画,每次调用我们的函数以生成新炸弹时,该动画都会播放。
### Bombspawner.gd
#older code
#spawns bomb instance
func shoot():
#play cannon shoot animation each time the function is fired off
$AnimatedSprite2D.play("cannon_fired")
#returns spawned bomb
var spawned_bomb = bomb.instantiate()
return spawned_bomb
要通过 shoot 函数生成炸弹,我们需要将 Timer 节点的 timeout() 信号连接到脚本。
在 _on_timer_timeout() 函数中,我们需要检查场景中当前是否没有生成炸弹,如果为 true,我们将调用 shoot() 函数并在我们的 bomb_path 上生成炸弹。我们可以通过 get_child 或 get_child_count 方法检查 “/BombPath/Path2D/PathFollow2D/” 节点的子节点。您可以使用 add_child 方法将节点添加到定义的路径(例如我们的 bomb_path)。这将在我们的路径下创建一个 Bomb,因此我们的路径最终将看起来像 “/BombPath/Path2D/PathFollow2D/Bomb”。
### Bombspawner.gd
#older code
#shoot and spawn bomb onto path
func _on_timer_timeout():
#reset animation before shooting
$AnimatedSprite2D.play("cannon_idle")
#spawns a bomb onto our path if there are no bombs available
if bomb_path.get_child_count() <= 0:
bomb_path.add_child(shoot())
最后,我们需要检查路径是否有任何更改。如果炸弹已到达路径的末尾(当动画达到 1 值时),我们需要将路径值重置回 0,以便动画可以重新启动,并生成新的炸弹。
为了使其正常工作,我们需要创建一个 Global 变量,该变量将改变炸弹移动的状态。如果生成了炸弹,它应该在移动,如果它与玩家碰撞,则它不应该移动。
### Global.gd
extends Node
#movement states
var is_attacking = false
var is_climbing = false
var is_jumping = false
#current scene
var current_scene_name
#bomb movement state
var is_bomb_moving = false
func _ready():
# Sets the current scene's name
current_scene_name = get_tree().get_current_scene().name
### Bomb.gd
#older code
func _on_body_entered(body):
#if the bomb collides with the player, play the explosion animation and start the timer
if body.name == "Player":
$AnimatedSprite2D.play("explode")
$Timer.start()
Global.is_bomb_moving = false
#OPTION 1
#if the bomb collides with our Level Tilemap (floor and walls).
if body.name == "Level" and !body.name.begins_with("Platform"):
$AnimatedSprite2D.play("explode")
$Timer.start()
Global.is_bomb_moving = false
#OPTION 2
#if the bomb collides with our Wall scene, explode and remove
if body.name.begins_with("Wall"):
$AnimatedSprite2D.play("explode")
$Timer.start()
Global.is_bomb_moving = false
### BombSpawner.gd
#older code
#spawns bomb instance
func shoot():
#play cannon shoot animation each time the function is fired off
$AnimatedSprite2D.play("cannon_fired")
#sets the bomb to moving and plays bomb animation
Global.is_bomb_moving = true
bomb_animation.play("bomb_movement")
#returns spawned bomb
var spawned_bomb = bomb.instantiate()
return spawned_bomb
现在,在我们的 _on_timer_timeout() 函数中,我们可以检查我们的炸弹是否没有移动,如果为 true,我们需要移除所有炸弹并停止播放动画,以便我们的进度比率可以重置回 0。我们可以通过运行 For 循环来做到这一点,该循环将计算我们 bomb_path 中的子项,如果存在,则将它们全部删除。
### BombSpawner.gd
#older code
#shoot and spawn bomb onto path
func _on_timer_timeout():
#reset animation before shooting
$AnimatedSprite2D.play("cannon_idle")
#spawns a bomb onto our path if there are no bombs available
if bomb_path.get_child_count() <= 0:
bomb_path.add_child(shoot())
# Clear all existing bombs
if Global.is_bomb_moving == false:
for bombs in bomb_path.get_children():
bomb_path.remove_child(bombs)
bombs.queue_free()
bomb_animation.stop()
最后,我们需要关卡场景中的 AnimationPlayer 在开始时播放我们的 bomb_movement 动画。这将允许炸弹在 BombSpawner 进入场景时移动。
### Bombspawner.gd
#older code
#when it's loaded into the scene
func _ready():
#default animation on load
$AnimatedSprite2D.play("cannon_idle")
#initiates paths
current_scene_path = "/root/" + Global.current_scene_name + "/" #current scene
bomb_path = get_node(current_scene_path + "/BombPath/Path2D/PathFollow2D") #PathFollow2D
bomb_animation = get_node(current_scene_path + "/BombPath/Path2D/AnimationPlayer") #AnimationPlayer
#starts bomb movement
bomb_animation.play("bomb_movement")
现在,在您的 Main 场景中,实例化您的 BombSpawner 场景。你想把你的大炮放在你的炸弹路径前面。
Main.tscn 中:
Main_2.tscn 中:
现在,如果你运行你的场景,你的炸弹应该会生成,如果它沿着路径爆炸,它应该会重新生成。炸弹移动得有点太快了,所以让我们将主场景长度中的 bomb_movement 动画改高一点。您必须根据路径的长度测试此值。
Main.tscn 中:
Main_2.tscn 中:
如果您注意到玩家不断与炸弹碰撞(即,他们无法跳过炸弹),请考虑将玩家的 jump_height 变量更改为稍低的值,例如 -110。您还可以将 Bomb (炸弹) 节点的碰撞形状更改为略小一些。您也可以对 Player 的碰撞形状执行相同的作。
让我们为这部分做最后一件事。如果 Bomb 正在移动,让我们让 Bomb 滚动。我们可以通过在 physics_process() 函数中为其 rotate 值添加来做到这一点。在 Bomb 脚本中,创建一个新变量,并将其 speed 设置为您希望 Bomb 旋转的速度。
### Bomb.gd
extends Area2D
var rotation_speed = 10
然后,在 physics_process() 函数中,如果炸弹正在移动,则让我们旋转炸弹。
### Bomb.gd
#older code
#rolls the bomb
func _physics_process(delta):
# Rotate the bomb if it hasn't exploded
if Global.is_bomb_moving == true:
$AnimatedSprite2D.rotation += rotation_speed * delta
您的代码应如下所示。
现在,如果你运行你的场景,你的炸弹应该滚到端点。如果它与您在 Bomb 脚本中定义的碰撞体发生碰撞,它应该将自身从场景中移除,并且生成器应该重新生成一个新的炸弹。您的玩家也应该能够跳过炸弹。游戏开始时大炮第一次开火时会有一点生成延迟,但这并没有破坏交易。
恭喜您创建了炸弹生成器!在下一部分中,我们将创建一个 Cannon Handler,它将有一个对话气泡,它会在其中嘲讽我们 — 除了它在游戏中的目的纯粹是为了视觉效果之外!
现在是保存工程并备份工程的好时机,以便在发生任何破坏游戏的错误时可以恢复到此部分。在继续这个系列之前,请回去修改你学到的知识,一旦你准备好了,我们下一节见!