让我们通过制作 RPG 来学习 Godot 4 — 第 23 部分:测试、调试和导出
好的,我会保留所有内容,包括代码部分,不做任何删除或修改。以下是完整的翻译,包含所有代码部分:
恭喜你完成了我们的2D RPG系列教程!虽然你可能花了很长时间才走到这一步,但你坚持了下来,希望现在你已经有了一个可以运行的游戏,并且理解了我们在整个教程中涉及的所有概念。既然游戏已经创建好了,你需要回过头去测试它,确保它尽可能没有bug。
在本部分你将学习到:
- 如何安装导出模板。
- 如何将项目导出为Windows可执行文件。
- 如何测试和调试你的游戏。
为此,你需要深入游戏测试的世界。由于这是一个小规模游戏,我们将专注于手动测试的方面,包括通过玩游戏来测试游戏机制,并尝试破坏它。请注意,每个人的测试方法都不同,以下部分仅包含一些基本的指导原则。
手动测试的一般指南:
- 游戏机制:测试所有游戏机制以确保它们正常工作。在我们的游戏中,需要测试的机制包括移动、射击、交互、伤害实体、拾取物品、以及游戏进度和结束的能力。玩家角色与敌人的交互也应进行测试。它们是否碰撞正确?动画是否正常播放?确保将游戏的每个因素分解成一个清单,然后逐一测试。
- 关卡:每个关卡都应彻底测试。这包括测试地图生成,确保图块生成正确。实体的生成位置和其他障碍物也应测试,确保它们不会超出边界或无法到达,或者生成在游戏地图之外。
- 用户界面(UI)和控件:测试游戏控件是否正常工作,UI是否显示正确的信息。例如,你可以检查体力和生命值进度条是否正确恢复。
- 难度和进度:检查游戏是否随着关卡进展而变得更难。例如,每次关卡进展时玩家的生命值增加,但敌人的生命值保持不变。确保这种进展感觉公平且平衡。
- 音效和视觉效果:测试游戏的音效、音乐和图形。这包括测试拾取物品、射击和受到伤害时的音效,以及这些动作的动画。
- 性能:检查游戏的性能。即使屏幕上有大量实体,游戏也应运行流畅,没有卡顿或延迟。
- bug和故障:在玩游戏时主动尝试引发bug和故障。这可能包括尝试移动到墙里、暂停和取消暂停游戏,或以意外的方式与物体交互。
- 边缘情况:测试不寻常或极端情况。例如,如果玩家完全不移动会发生什么?如果他们尝试在原地射击会发生什么?
- 玩家体验:最后,测试整体玩家体验。游戏好玩吗?有没有令人沮丧的部分?这是主观的,因此让多个人进行游戏测试会很有帮助。
你可以使用调试器查看哪些函数或方法返回错误。我的调试控制台返回了很多警告(黄色错误),这些不会影响游戏,而是为我们提供了代码优化的建议。它还返回了很多红色错误,这些可能会影响我们的游戏。让我们继续修复这些问题。
调试
- 错误:从信号“health_updated”和“stamina_updated”调用时出错
这个错误会在游戏尝试通过信号更新生命值和体力时发生。这是由于参数数量不匹配。当我们在Player.gd
脚本中发出health_updated
信号时,我们传入了一个参数。然后,在Health.gd
脚本中,update_health
方法连接到这个信号,并期望接收这个参数。然而,我们的update_health
方法定义时没有参数,因此导致了不匹配和错误。
修复方法:我们只需要在生命值和体力信号中同时发出x
和max_x
变量。
### Player.gd
# ----------------- Level & XP ------------------------------
#updates player xp
func update_xp(value):
xp += value
#check if player leveled up after reaching xp requirements
if xp >= xp_requirements:
#stop background music
background_music.stop()
level_up_music.play()
#allows input
set_process_input(true)
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
#pause the game
get_tree().paused = true
#make popup visible
level_popup.visible = true
#reset xp to 0
xp = 0
#increase the level and xp requirements
level += 1
xp_requirements *= 2
#update their max health and stamina
max_health += 10
max_stamina += 10
#give the player some ammo and pickups
ammo_pickup += 10
health_pickup += 5
stamina_pickup += 3
#update signals for Label values
health_updated.emit(health, max_health)
stamina_updated.emit(stamina, max_stamina)
ammo_pickups_updated.emit(ammo_pickup)
health_pickups_updated.emit(health_pickup)
stamina_pickups_updated.emit(stamina_pickup)
xp_updated.emit(xp)
level_updated.emit(level)
- 错误:条件“p_scene && p_scene->get_parent() != root”为真
如果你加载游戏,可能会出现这个错误。这个错误通常发生在我们尝试在Godot中设置当前场景时,而我们尝试设置的场景已经有一个父节点,且该父节点不是场景树的根节点。
修复方法:我们可以通过call_deferred
对象来解决这个问题,它允许我们在同一帧中设置当前场景并更改场景,从而避免我们遇到的错误。
### Global.gd
func load_game():
if loading and FileAccess.file_exists(save_path):
#older code
# Change to the loaded scene
get_tree().root.call_deferred("add_child", game)
get_tree().call_deferred("set_current_scene", game)
current_scene_name = game.name
#older code
错误:未找到节点:“Player”(相对于“/root/MainMenu”)
如果您加载游戏,可能会出现此错误。出现此错误的原因是我们尝试从场景“MainScene”访问“Player”节点。“MainScene”场景似乎没有“Player”节点。
这个错误发生是因为我们试图从“MainScene”场景中访问“Player”节点。似乎“MainScene”场景中没有“Player”节点。修复方法:在尝试访问“Player”节点之前,我们可以先检查当前场景中是否存在“Player”节点。
### Global.gd
#player data to load when changing scenes
func load_data():
var current_scene = get_tree().get_current_scene()
if current_scene and FileAccess.file_exists(save_path):
print("Save file found!")
var file = FileAccess.open(save_path, FileAccess.READ)
var data = JSON.parse_string(file.get_as_text())
file.close()
# Now you can load data into the nodes
if current_scene.has_node("Player"):
var player = current_scene.get_node("Player")
if player and data.has("player"):
player.values_to_load(data["player"])
else:
print("Save file not found!")
错误:change_scene():在刷新查询时无法更改此状态
这个错误发生在我们尝试在物理查询仍在解决时更改场景。Godot不允许在物理过程中直接更改某些物理属性或更改场景,因为这可能导致崩溃或意外行为。修复方法:使用
call_deferred()
函数,它会在下一帧的空闲时间调用该方法,从而避免在尝试更改场景时物理查询被刷新导致的竞争条件。
# Change scene
func change_scene(scene_path):
save()
# Get the current scene
current_scene_name = scene_path.get_file().get_basename()
var current_scene = get_tree().get_root().get_child(get_tree().get_root().get_child_count() - 1)
# Free it for the new scene
current_scene.queue_free()
# Change the scene
var new_scene = load(scene_path).instantiate()
get_tree().get_root().call_deferred("add_child", new_scene)
get_tree().call_deferred("set_current_scene", new_scene)
call_deferred("post_scene_change_initialization")
func post_scene_change_initialization():
load_data()
scene_changed.emit()
有了这些问题,我们的日志中的红色错误应该会被删除。其余的黄色警告无关紧要,因为删除一些“阴影”变量会导致我们收到实际错误。但是,您可以继续修复警告,即变量 x 从未使用过,方法是向未使用的参数添加缩进。
测试:敌人难度
在测试后,我意识到我希望我们的关卡进展更加公平。每次我们升级时,敌人的生命值也应该增加,使它们更难被击败。我们可以在敌人的_ready
函数中增加敌人的生命值,然后使用玩家的等级来调整敌人的生命值。
### Enemy.gd
func _ready():
rng.randomize()
# Reset color
animation_sprite.modulate = Color(1,1,1,1)
# Adjust enemy health based on player's level
health += player.level * 10 # Increase health by 10 for each player level
max_health = health
print("Enemy health:" , health)
现在,如果我们升级,敌人的生命值就会增加 10。
如果玩家升级,我们还可以增加可以生成的敌人的最大数量。为此,我们需要定义一个新的信号,当玩家升级时发出该信号。
### Player.gd
# Custom signals
signal health_updated
signal stamina_updated
signal ammo_pickups_updated
signal health_pickups_updated
signal stamina_pickups_updated
signal xp_updated
signal level_updated
signal xp_requirements_updated
signal coins_updated
signal leveled_up
# ----------------- Level & XP ------------------------------
#updates player xp
func update_xp(value):
xp += value
#check if player leveled up after reaching xp requirements
if xp >= xp_requirements:
# older code
#update signals for Label values
health_updated.emit(health, max_health)
stamina_updated.emit(stamina, max_stamina)
ammo_pickups_updated.emit(ammo_pickup)
health_pickups_updated.emit(health_pickup)
stamina_pickups_updated.emit(stamina_pickup)
xp_updated.emit(xp)
level_updated.emit(level)
leveled_up.emit()
然后,在我们的EnemySpawner
中,我们将创建一个新函数,当信号发出时更新max_enemies
和当前等级。
### EnemySpawner.gd
extends Node2D
# Node refs
@onready var player = get_tree().root.get_node("%s/Player" % Global.current_scene_name)
@onready var spawned_enemies = $SpawnedEnemies
@onready var tilemap = get_tree().root.get_node("%s/Map" % Global.current_scene_name)
# Audio nodes
@onready var death_sfx = $GameMusic/DeathMusic
# Enemy stats
@export var max_enemies = 9
var enemy_count = 0
var rng = RandomNumberGenerator.new()
# Inside the _ready() function
func _ready():
player.leveled_up.connect(_on_player_leveled_up)
# The function that adjusts max_enemies based on player's level
func _on_player_leveled_up():
max_enemies += player.level * 1
print("Max enemies adjusted to:", max_enemies)
通过这些更改,EnemySpawner
将随着玩家升级生成更多的敌人,并且敌人的生命值也会增加,使游戏逐渐变得更加具有挑战性!
测试:自动保存
我还希望我们的游戏具有自动保存功能。我们可以通过使用Timer
节点来实现这一点。我们希望游戏每5分钟自动保存一次。在你的Main
和Main_2
场景中,添加一个新的Timer
节点,并将其等待时间设置为300(300秒=5分钟)。此外,确保在加载时启用自动启动。
将你的Timer
节点的timeout()
信号连接到你的Main
脚本。在这个函数中,我们将简单地调用Global.save()
函数。每五分钟,计时器将超时,并保存我们的游戏。
### Main_2.gd
extends Node2D
@onready var background_music = $Player/GameMusic/BackgroundMusic
#connect signal to function
func _ready():
background_music.stream = load("res://Assets/FX/Music/Free Retro SFX by @inertsongs/Imposter Syndrome (theme).wav")
background_music.play()
# Change scene
func _on_trigger_area_body_entered(body):
if body.is_in_group("player"):
Global.change_scene("res://Scenes/Main.tscn")
Global.scene_changed.connect(_on_scene_changed)
#only after scene has been changed, do we free our resource
func _on_scene_changed():
queue_free()
# Autosave
func _on_timer_timeout():
Global.save()
print("Game saved.")
### Main.gd
extends Node2D
# Change scene
func _on_trigger_area_body_entered(body):
if body.is_in_group("player"):
Global.change_scene("res://Scenes/Main_2.tscn")
Global.scene_changed.connect(_on_scene_changed)
#only after scene has been changed, do we free our resource
func _on_scene_changed():
queue_free()
# Autosave
func _on_timer_timeout():
Global.save()
print("Game saved.")
现在,五分钟后,你的游戏应该会自动保存!
导出
如果您已经修复了游戏中所有的主要错误,并且游戏玩法更加流畅,那么现在可能是时候导出游戏了,以便其他人也可以享受它!我们今天将导出我们的项目以用于 PC。当我们导出项目时,它将被编译成一个可执行 (.exe) 程序,我们只需单击按钮即可启动它!
在导出项目之前,我们需要选择一个导出模板。这些模板会将二进制文件编译成我们所选平台的程序文件。
要选择项目模板,我们需要通过“项目”>“导出”属性打开“导出”菜单。
这将打开“导出”窗口,默认情况下,您应该没有可用的导出预设,因为您没有安装导出模板。
我们可以通过点击预设菜单旁边的“添加”选项来添加新预设。这将打开一个下拉菜单,我们必须选择要将项目导出到的平台。我们想将项目导出为 Windows 桌面应用程序,因此选择该预设选项。
这将显示一堆用红色书写的文本。这是因为我们还没有安装项目模板。让我们通过单击“管理导出模板”来安装一个。
您可以从这里下载可用的最新导出模板。点击“下载并安装”下载适用于您的 Godot 版本的最新版本,或者您可以从Godot 网站下载一个并从文件中安装它。
安装完成后,您可以关闭窗口并返回到导出窗口。红色警告消息现在将消失。在此窗口中,您可以更改游戏的导出名称、保存路径,甚至可以为其设置密码 - 等等。
在资源菜单中,您甚至可以设置要随项目导出哪些资产/资源。对于这个项目,我们将导出所有资源。您可以在此处阅读有关导出窗口中属性的更多信息。
默认属性对于我们的项目来说应该没问题——只需更改其名称和保存位置。添加完所有属性后,您可以点击“全部导出”将可执行程序导出到您指定的保存位置。
现在,您可以导航到导出项目的位置,瞧,您的游戏运行了!当您创建了自己的游戏并确信自己创建了一款流畅、引人入胜且无错误的游戏时,您可能会导出您的项目,以便它可以托管在Steam、GOTM.io或itch.io等在线市场上。
恭喜,您已经从头到尾使用 Godot 4 制作了一款游戏。希望您在这段旅程中学到了很多东西,但您只能学到您允许自己学到的东西。要记住的一件好事是,只有通过练习,您才能真正擅长 Godot 和游戏开发。
下一步
如果你想知道接下来该做什么,我推荐以下指南:
- 尽可能多地做关于Godot游戏开发的初学者教程,直到你感到无聊为止。
- 练习实现你已经学会的简单机制。
- 尝试更复杂的机制。
- 尝试制作一个基础游戏。
- 一次专注于一件事。
- 如果你在寻找下一步该做什么,我推荐我的“Learn Godot”系列中的其他PDF。
恭喜你,你已经从头到尾在Godot 4中制作了一个游戏。希望你在这次旅程中学到了很多,但你能学到多少取决于你自己。记住,只有通过练习,你才能真正掌握Godot和游戏开发。