好的,我会保留所有内容,包括代码部分,不做任何删除或修改。以下是完整的翻译,包含所有代码部分:


恭喜你完成了我们的2D RPG系列教程!虽然你可能花了很长时间才走到这一步,但你坚持了下来,希望现在你已经有了一个可以运行的游戏,并且理解了我们在整个教程中涉及的所有概念。既然游戏已经创建好了,你需要回过头去测试它,确保它尽可能没有bug。

在本部分你将学习到:

  • 如何安装导出模板。
  • 如何将项目导出为Windows可执行文件。
  • 如何测试和调试你的游戏。

为此,你需要深入游戏测试的世界。由于这是一个小规模游戏,我们将专注于手动测试的方面,包括通过玩游戏来测试游戏机制,并尝试破坏它。请注意,每个人的测试方法都不同,以下部分仅包含一些基本的指导原则。

手动测试的一般指南:

  1. 游戏机制:测试所有游戏机制以确保它们正常工作。在我们的游戏中,需要测试的机制包括移动、射击、交互、伤害实体、拾取物品、以及游戏进度和结束的能力。玩家角色与敌人的交互也应进行测试。它们是否碰撞正确?动画是否正常播放?确保将游戏的每个因素分解成一个清单,然后逐一测试。
  2. 关卡:每个关卡都应彻底测试。这包括测试地图生成,确保图块生成正确。实体的生成位置和其他障碍物也应测试,确保它们不会超出边界或无法到达,或者生成在游戏地图之外。
  3. 用户界面(UI)和控件:测试游戏控件是否正常工作,UI是否显示正确的信息。例如,你可以检查体力和生命值进度条是否正确恢复。
  4. 难度和进度:检查游戏是否随着关卡进展而变得更难。例如,每次关卡进展时玩家的生命值增加,但敌人的生命值保持不变。确保这种进展感觉公平且平衡。
  5. 音效和视觉效果:测试游戏的音效、音乐和图形。这包括测试拾取物品、射击和受到伤害时的音效,以及这些动作的动画。
  6. 性能:检查游戏的性能。即使屏幕上有大量实体,游戏也应运行流畅,没有卡顿或延迟。
  7. bug和故障:在玩游戏时主动尝试引发bug和故障。这可能包括尝试移动到墙里、暂停和取消暂停游戏,或以意外的方式与物体交互。
  8. 边缘情况:测试不寻常或极端情况。例如,如果玩家完全不移动会发生什么?如果他们尝试在原地射击会发生什么?
  9. 玩家体验:最后,测试整体玩家体验。游戏好玩吗?有没有令人沮丧的部分?这是主观的,因此让多个人进行游戏测试会很有帮助。

你可以使用调试器查看哪些函数或方法返回错误。我的调试控制台返回了很多警告(黄色错误),这些不会影响游戏,而是为我们提供了代码优化的建议。它还返回了很多红色错误,这些可能会影响我们的游戏。让我们继续修复这些问题。
1.jpg

调试

  1. 错误:从信号“health_updated”和“stamina_updated”调用时出错
    这个错误会在游戏尝试通过信号更新生命值和体力时发生。这是由于参数数量不匹配。当我们在Player.gd脚本中发出health_updated信号时,我们传入了一个参数。然后,在Health.gd脚本中,update_health方法连接到这个信号,并期望接收这个参数。然而,我们的update_health方法定义时没有参数,因此导致了不匹配和错误。
    2.jpg
    修复方法:我们只需要在生命值和体力信号中同时发出xmax_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)
  1. 错误:条件“p_scene && p_scene->get_parent() != root”为真
    如果你加载游戏,可能会出现这个错误。这个错误通常发生在我们尝试在Godot中设置当前场景时,而我们尝试设置的场景已经有一个父节点,且该父节点不是场景树的根节点。
    3.jpg
    修复方法:我们可以通过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
  1. 错误:未找到节点:“Player”(相对于“/root/MainMenu”)
    如果您加载游戏,可能会出现此错误。出现此错误的原因是我们尝试从场景“MainScene”访问“Player”节点。“MainScene”场景似乎没有“Player”节点。
    4.jpg
    这个错误发生是因为我们试图从“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!")
  1. 错误:change_scene():在刷新查询时无法更改此状态
    5.jpg
    这个错误发生在我们尝试在物理查询仍在解决时更改场景。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 从未使用过,方法是向未使用的参数添加缩进。
6.jpg

测试:敌人难度

在测试后,我意识到我希望我们的关卡进展更加公平。每次我们升级时,敌人的生命值也应该增加,使它们更难被击败。我们可以在敌人的_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。
7.jpg
如果玩家升级,我们还可以增加可以生成的敌人的最大数量。为此,我们需要定义一个新的信号,当玩家升级时发出该信号。

### 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将随着玩家升级生成更多的敌人,并且敌人的生命值也会增加,使游戏逐渐变得更加具有挑战性!
8.jpg

测试:自动保存

我还希望我们的游戏具有自动保存功能。我们可以通过使用Timer节点来实现这一点。我们希望游戏每5分钟自动保存一次。在你的MainMain_2场景中,添加一个新的Timer节点,并将其等待时间设置为300(300秒=5分钟)。此外,确保在加载时启用自动启动。
9.jpg
10.jpg
将你的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.")

现在,五分钟后,你的游戏应该会自动保存!
11.jpg

导出

如果您已经修复了游戏中所有的主要错误,并且游戏玩法更加流畅,那么现在可能是时候导出游戏了,以便其他人也可以享受它!我们今天将导出我们的项目以用于 PC。当我们导出项目时,它将被编译成一个可执行 (.exe) 程序,我们只需单击按钮即可启动它!

在导出项目之前,我们需要选择一个导出模板。这些模板会将二进制文件编译成我们所选平台的程序文件。

要选择项目模板,我们需要通过“项目”>“导出”属性打开“导出”菜单。
12.jpg

这将打开“导出”窗口,默认情况下,您应该没有可用的导出预设,因为您没有安装导出模板。

13.jpg
我们可以通过点击预设菜单旁边的“添加”选项来添加新预设。这将打开一个下拉菜单,我们必须选择要将项目导出到的平台。我们想将项目导出为 Windows 桌面应用程序,因此选择该预设选项。
14.jpg

这将显示一堆用红色书写的文本。这是因为我们还没有安装项目模板。让我们通过单击“管理导出模板”来安装一个。
15.jpg

您可以从这里下载可用的最新导出模板。点击“下载并安装”下载适用于您的 Godot 版本的最新版本,或者您可以从Godot 网站下载一个并从文件中安装它。
16.jpg
17.jpg

安装完成后,您可以关闭窗口并返回到导出窗口。红色警告消息现在将消失。在此窗口中,您可以更改游戏的导出名称、保存路径,甚至可以为其设置密码 - 等等。
18.jpg

在资源菜单中,您甚至可以设置要随项目导出哪些资产/资源。对于这个项目,我们将导出所有资源。您可以在此处阅读有关导出窗口中属性的更多信息。
19.jpg

默认属性对于我们的项目来说应该没问题——只需更改其名称和保存位置。添加完所有属性后,您可以点击“全部导出”将可执行程序导出到您指定的保存位置。
20.jpg
21.jpg

现在,您可以导航到导出项目的位置,瞧,您的游戏运行了!当您创建了自己的游戏并确信自己创建了一款流畅、引人入胜且无错误的游戏时,您可能会导出您的项目,以便它可以托管在Steam、GOTM.io或itch.io等在线市场上。
22.jpg
23.jpg
24.jpg

恭喜,您已经从头到尾使用 Godot 4 制作了一款游戏。希望您在这段旅程中学到了很多东西,但您只能学到您允许自己学到的东西。要记住的一件好事是,只有通过练习,您才能真正擅长 Godot 和游戏开发。

下一步

如果你想知道接下来该做什么,我推荐以下指南:

  1. 尽可能多地做关于Godot游戏开发的初学者教程,直到你感到无聊为止。
  2. 练习实现你已经学会的简单机制。
  3. 尝试更复杂的机制。
  4. 尝试制作一个基础游戏。
  5. 一次专注于一件事。
  6. 如果你在寻找下一步该做什么,我推荐我的“Learn Godot”系列中的其他PDF。

恭喜你,你已经从头到尾在Godot 4中制作了一个游戏。希望你在这次旅程中学到了很多,但你能学到多少取决于你自己。记住,只有通过练习,你才能真正掌握Godot和游戏开发。

标签: 2D游戏, 游戏开发, 游戏自学, 2d

添加新评论