在这一部分,我们将通过为游戏添加一些音乐和音效(SFX)来为游戏增添最后的生命力。我们希望在主菜单、暂停、死亡界面以及主场景中都有背景音乐。同时,我们还会为玩家和敌人添加射击和受伤的音效,以及对话音乐和拾取音效。

你将学习到的内容:

  • 如何使用 AudioStreamPlayer 节点。
  • 如何使用 AudioStreamPlayer2D 节点。
  • 如何在代码中播放、设置和停止音频流。
  • 如何在导入面板中循环播放音频文件。

在 Godot 中,你有三种主要的音频播放方式:

  1. AudioStreamPlayer:这是一个通用的音频播放器,适合播放音乐或任何非位置性的音频。当你不需要音频在游戏世界中具有位置时使用它。例如,背景音乐或对话音乐,无论角色或对象在游戏中的位置如何,都应该播放。
  2. AudioStreamPlayer2D:这是为 2D 游戏设计的,适用于需要位置音频的情况。它模拟声音在 2D 空间中的位置,声音离听者(通常是摄像机)越远,声音越小。当你制作 2D 游戏并希望音频在 2D 世界中具有位置时使用它(例如,屏幕上某个位置发出的音效)。
  3. AudioStreamPlayer3D:类似于 AudioStreamPlayer2D,但适用于 3D 游戏。它考虑了声音在三维空间中的位置。在 3D 游戏中,当你希望声音从 3D 世界中的特定位置发出时使用它。

在本教程中,我们将使用 AudioStreamPlayer 节点来处理稳定的声音,如背景音乐或对话音乐。而对于音效,我们将使用 AudioStreamPlayer2D 节点,因为我们希望声音有一定的声像变化,并且声音的响度取决于声音的位置。
1.jpg
2.jpg

主菜单音乐

打开你的 MainScene 场景,添加一个名为 AudioStreamPlayer 的新节点,并将其重命名为 BackgroundMusic
3.jpg
4.jpg
在检查器面板中,我们可以为该音频播放器节点分配要播放的音频文件。点击检查器面板中 Stream 属性旁边的 <empty>,选择“快速加载”。这将设置要播放的 AudioStream 对象。
5.jpg
我们希望背景音乐播放音频文件 We Ride at Dawn.wav。你可以在 Assets > Music 目录下找到所有音乐文件。
6.jpg
在检查器面板中,你可以设置其播放和自动播放的值。如果 playing 值为 true,音频正在播放或排队等待播放(参见 play)。如果 autoplaytrue,音频将在场景加载时播放。建议你阅读文档以了解其他属性的作用,例如 busmix target

我们希望音乐在游戏加载时立即播放,因此需要启用 autoplay。如果你想听音乐,可以启用 playing,但之后记得禁用它!
7.jpg
现在运行你的场景,音乐应该默认播放,但仅当你在主菜单场景时。

暂停菜单

Player 场景中,添加一个 AudioStreamPlayer 节点,并将其重命名为 PauseMenuMusic
8.jpg
让我们将所有音乐组织在一个名为 GameMusicNode2D 节点下。
9.jpg
我们还希望在暂停菜单打开时播放 We Ride at Dawn.wav。不要启用 autoplayplaying,因为我们将在代码中设置该节点仅在暂停菜单打开时播放。
10.jpg
要播放音频,我们只需引用节点并调用其 .play() 方法。该方法以秒为单位播放音频。我们将在游戏暂停后调用 ui_pause 输入时播放此音频。

# Player.gd

# 音频节点
@onready var pause_menu_music = $GameMusic/PauseMenuMusic

func _input(event):
    # 旧代码
    # 显示暂停菜单
    if !pause_screen.visible:
        if event.is_action_pressed("ui_pause"):
            # 播放音乐
            pause_menu_music.play()
            # 暂停游戏
            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
            get_tree().paused = true
            # 显示暂停菜单弹窗
            pause_screen.visible = true
            # 停止移动处理
            set_physics_process(false)
            # 设置暂停状态为 true
            paused = true
            # 如果玩家死亡,返回主菜单
            if health <= 0:
                get_node("/root/%s" % Global.current_scene_name).queue_free()
                Global.change_scene("res://Scenes/MainScene.tscn")
                get_tree().paused = false
                return

我们还需要将 PauseMenuMusic 节点的 process mode 设置为 When Paused,因为我们只希望在游戏处于暂停状态时处理该节点。
11.jpg
我们还需要在退出场景或恢复游戏时停止播放音乐,否则它会覆盖其他音频。我们可以通过 stop() 方法来实现。

# ----------------- 暂停菜单 ---------------------------
# 恢复游戏
func _on_resume_pressed():
    # 隐藏暂停菜单
    pause_screen.visible = false
    # 设置暂停状态为 false
    get_tree().paused = false
    paused = false
    # 接受移动和输入
    set_process_input(true)
    set_physics_process(true)
    Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
    # 停止音乐
    pause_menu_music.stop()

现在如果你运行场景并暂停/取消暂停,暂停音乐应该正确播放。

背景音乐

我们还需要在游戏循环中播放音乐。我们将此节点附加到 Player 场景,因为我们希望此声音跟随玩家移动。让我们在 Player 场景中添加另一个 AudioStreamPlayer 节点,并将其命名为 BackgroundMusic

我们希望此音频轨道为 We Don’t Need Railroads.wav。同时,启用 autoplay,因为我们希望此音频轨道默认播放。
12.jpg
13.jpg
我们需要在暂停菜单打开时停止播放此音乐,因此更新你的 ui_pause 输入以停止背景音乐。

# Player.gd

# 音频节点
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic

func _input(event):
    # 旧代码
    # 显示暂停菜单
    if !pause_screen.visible:
        if event.is_action_pressed("ui_pause"):
            # 暂停游戏
            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
            get_tree().paused = true
            # 显示暂停菜单弹窗
            pause_screen.visible = true
            # 停止移动处理
            set_physics_process(false)
            # 设置暂停状态为 true
            paused = true
            # 播放音乐
            background_music.stop()
            pause_menu_music.play()
            # 如果玩家死亡,返回主菜单
            if health <= 0:
                get_node("/root/%s" % Global.current_scene_name).queue_free()
                Global.change_scene("res://Scenes/MainScene.tscn")
                get_tree().paused = false
                return

我们的背景音乐还应在按下确认和恢复按钮后播放。

# Player.gd

# 关闭弹窗
func _on_confirm_pressed():
    level_popup.visible = false
    get_tree().paused = false
    Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
    background_music.play()

# ----------------- 暂停菜单 ---------------------------
# 恢复游戏
func _on_resume_pressed():
    # 隐藏暂停菜单
    pause_screen.visible = false
    # 设置暂停状态为 false
    get_tree().paused = false
    paused = false
    # 接受移动和输入
    set_process_input(true)
    set_physics_process(true)
    Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
    # 停止音乐
    pause_menu_music.stop()
    background_music.play()

如果游戏结束,我们将播放另一个声音,因此我们还需要在死亡条件中停止背景音乐。

# Player.gd

# ------------------- 伤害与死亡 ------------------------------
# 对玩家造成伤害
func hit(damage):
    health -= damage    
    health_updated.emit(health, max_health)
    if health > 0:
        # 伤害
        animation_player.play("damage")
        health_updated.emit(health, max_health)
    else:
        # 停止背景音乐
        background_music.stop()
        # 死亡
        set_process(false)
        get_tree().paused = true
        paused = true
        animation_player.play("game_over")

同样适用于我们的 update_xp 函数。

# Player.gd

# ----------------- 等级与经验 ------------------------------
# 更新玩家经验
func update_xp(value):
    xp += value
    # 检查玩家是否达到经验要求并升级
    if xp >= xp_requirements:
        # 停止背景音乐
        background_music.stop()

如果你运行场景,游戏中的音乐应该会播放。

游戏结束音乐

当玩家死亡时,我们还希望播放 GameOverMusic。为此,我们需要在 Player 场景中添加另一个 AudioStreamPlayer 节点。

我们希望此音频轨道为 Too Late To Save The Town.wav。同时,将其处理模式更改为 When Paused
14.jpg
更新你的 hit() 函数,在背景音乐停止后播放音频。

# Player.gd

# 音频节点
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic

# ------------------- 伤害与死亡 ------------------------------
# 对玩家造成伤害
func hit(damage):
    health -= damage    
    health_updated.emit(health, max_health)
    if health > 0:
        # 伤害
        animation_player.play("damage")
        health_updated.emit(health, max_health)
    else:
        # 死亡
        set_process(false)
        get_tree().paused = true
        paused = true
        animation_player.play("game_over")
        # 停止背景音乐
        background_music.stop()
        game_over_music.play()

如果你运行场景,玩家死亡时音乐应该会播放。

对话音乐

我们必须在两个地方设置对话音乐,即 PlayerDialogPopup 脚本中。如果玩家与 NPC 互动,对话音乐应该播放;如果玩家结束互动并关闭弹窗,背景音乐应该播放。

添加一个新的 AnimationPlayer 节点,音频轨道为 Whiskey Barn Dance (loop).wav。将其处理模式设置为 When Paused。你也可以使用 Imposter Syndrome (wav) 音频。
15.jpg
我们在 Player 脚本中播放音频,因为如果我们在 DialogPopupopen() 函数中播放,每次弹窗更改时音乐都会重新开始!按如下方式播放和停止对话音乐:

# Player.gd

# 音频节点
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
@onready var dialog_music = $GameMusic/DialogMusic

# ------------------- 伤害与死亡 ------------------------------
# 对玩家造成伤害
func _input(event):
    # 旧代码
    # 与世界互动         
    elif event.is_action_pressed("ui_interact"):
        var target = ray_cast.get_collider()
        if target != null:
            if target.is_in_group("NPC"):
                # 与 NPC 对话
                target.dialog()
                # 音乐
                background_music.stop()
                dialog_music.play()
                return
# DialogPopup.gd

extends CanvasLayer

# 节点引用
@onready var animation_player = $"../../AnimationPlayer"
@onready var player = $"../.."
@onready var background_music = $"../../GameMusic/BackgroundMusic"
@onready var dialog_music = $"../../GameMusic/DialogMusic"

# 关闭对话  
func close():
    get_tree().paused = false
    self.visible = false
    player.set_physics_process(true)
    Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
    # 音乐
    dialog_music.stop()
    background_music.play()

如果你现在运行场景,玩家与 NPC 互动时对话音乐应该会播放。

升级音乐

当玩家升级时,我们希望播放一段胜利音效。我们仍然使用 AnimationPlayer 节点来实现这一点,因为我们希望音频从中心点发出,而不是在左右耳之间进行声像变化。

添加一个新节点并将其命名为 LevelUpMusic。我们希望音频为 Retro PowerUP StereoUP 05.wav
16.jpg
现在,在我们的 update_xp() 函数中,我们希望播放 LevelUpMusic

# Player.gd

# 音频节点
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
@onready var dialog_music = $GameMusic/DialogMusic
@onready var level_up_music = $GameMusic/LevelUpMusic

# ----------------- 等级与经验 ------------------------------
# 更新玩家经验
func update_xp(value):
    xp += value
    # 检查玩家是否达到经验要求并升级
    if xp >= xp_requirements:
        # 停止背景音乐
        background_music.stop()
        level_up_music.play()

如果你运行场景,玩家在完成任务和射击敌人后升级时,升级音乐应该会播放。

拾取音效

如果玩家拾取弹药、饮料或任务物品,我们希望播放拾取音效。为此,我们将使用 AudioStreamPlayer2D 节点,因为我们希望音效有一定的衰减效果。这会让声音听起来更像环境背景噪音。

你可以将此节点附加到类似火焰场景中,根据你离火焰的远近,声音的音量会有所不同。由于我们将此声音添加到 Player 中,声音不会衰减太多,因为摄像机始终聚焦在玩家身上。

添加一个新的 AudioStreamPlayer2D 节点并将其命名为 PickupsMusic。将其音频文件设置为 stamfull.wav
17.jpg
在代码中,我们需要在玩家拾取物品时播放此音频。我们可以在 add_pickup() 函数中实现这一点。

# Player.gd

# 音频节点
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
@onready var dialog_music = $GameMusic/DialogMusic
@onready var level_up_music = $GameMusic/LevelUpMusic
@onready var pickups_sfx = $GameMusic/PickupsMusic

# ---------------------- 消耗品 ------------------------------------------
# 将拾取物添加到基于 GUI 的库存中
func add_pickup(item):
    if item == Global.Pickups.AMMO: 
        ammo_pickup = ammo_pickup + 3 # + 3 子弹
        ammo_pickups_updated.emit(ammo_pickup)
        print("ammo val:" + str(ammo_pickup))
    if item == Global.Pickups.HEALTH:
        health_pickup = health_pickup + 1 # + 1 生命饮料
        health_pickups_updated.emit(health_pickup)
        print("health val:" + str(health_pickup))
    if item == Global.Pickups.STAMINA:
        stamina_pickup = stamina_pickup + 1 # + 1 耐力饮料
        stamina_pickups_updated.emit(stamina_pickup)
        print("stamina val:" + str(stamina_pickup))
    # 音效
    pickups_sfx.play()
    update_xp(5)

现在如果你拾取物品,音频应该会播放!

消耗音效

我们希望每次玩家按下“1”或“2”消耗生命或耐力饮料时播放音效。

添加另一个 AudioStreamPlayer2D 节点并将其命名为 ConsumableMusic。将其音频文件设置为 stam1.wav
18.jpg
在输入代码中,更新 ui_consume_healthui_consume_stamina 输入以播放消耗音效。你还可以通过在播放前加载新的流资源到 ConsumableMusic 节点中,使这两个音效不同。

# Player.gd

# 音频节点
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
@onready var dialog_music = $GameMusic/DialogMusic
@onready var level_up_music = $GameMusic/LevelUpMusic
@onready var pickups_sfx = $GameMusic/PickupsMusic
@onready var consume_sfx = $GameMusic/ConsumableMusic

func _input(event):
    # 旧代码
    # 使用生命消耗品
    elif event.is_action_pressed("ui_consume_health"):
        if health > 0 && health_pickup > 0:
            health_pickup = health_pickup - 1
            health = min(health + 50, max_health)
            health_updated.emit(health, max_health)
            health_pickups_updated.emit(health_pickup) 
            # 音效
            consume_sfx.play()
    # 使用耐力消耗品      
    elif event.is_action_pressed("ui_consume_stamina"):
        if stamina > 0 && stamina_pickup > 0:
            stamina_pickup = stamina_pickup - 1
            stamina = min(stamina + 50, max_stamina)
            stamina_updated.emit(stamina, max_stamina)      
            stamina_pickups_updated.emit(stamina_pickup)
            # 音效
            consume_sfx.stream = load("res://Assets/FX/Music/Free Retro SFX by @inertsongs/SFX/stam0.wav")
            consume_sfx.play()

现在如果你运行并拾取物品并消耗它们,音频应该会播放!

子弹撞击音效
当子弹击中我们的玩家或敌人时,我们希望播放子弹撞击的音效。我们将在敌人和玩家场景中添加这个音频。

让我们使用AudioStreamPlayer2D节点播放“Retro Impact LoFi 09.wav”音效,并将其重命名为“BulletImpactMusic”。将这个节点添加到你的敌人和玩家场景中。
19.jpg
*注意:不是SprintingMusic,而是BulletImpactMusic
20.jpg
在我们与节点碰撞后,
21.jpg播放这个音效。

Enemy.gd

# 音频节点
@onready var bullet_sfx = $GameMusic/BulletImpactMusic

# 当敌人被击中时会受到伤害
func hit(damage):
    health -= damage
    if health > 0:
        # 伤害
        animation_player.play("damage")
        # 音效
        bullet_sfx.play()

Player.gd

# 音频节点
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
@onready var dialog_music = $GameMusic/DialogMusic
@onready var level_up_music = $GameMusic/LevelUpMusic
@onready var pickups_sfx = $GameMusic/PickupsMusic
@onready var consume_sfx = $GameMusic/ConsumableMusic
@onready var bullet_sfx = $GameMusic/BulletImpactMusic

# ------------------- 伤害与死亡 ------------------------------
# 对玩家造成伤害
func hit(damage):
    health -= damage    
    health_updated.emit(health, max_health)
    if health > 0:
        # 伤害
        animation_player.play("damage")
        health_updated.emit(health, max_health)
        # 音效
        bullet_sfx.play()

现在如果我们运行场景,并且我们击中敌人或敌人用子弹击中我们,音效应该会播放。

射击音效
当我们射击时,我们也希望枪支播放射击音效。我们将为敌人和玩家都实现这一点。

让我们使用AudioStreamPlayer2D节点播放“Retro Weapon Gun LoFi 03.wav”音效,并将其重命名为“ShootingMusic”。将这个节点添加到你的敌人和玩家场景中。
22.jpg
让我们在ui_attack输入时播放这个音效。

Player.gd

# 音频节点
@onready var pause_menu_music = $GameMusic/PauseMenuMusic
@onready var background_music = $GameMusic/BackgroundMusic
@onready var game_over_music = $GameMusic/GameOverMusic
@onready var dialog_music = $GameMusic/DialogMusic
@onready var level_up_music = $GameMusic/LevelUpMusic
@onready var pickups_sfx = $GameMusic/PickupsMusic
@onready var consume_sfx = $GameMusic/ConsumableMusic
@onready var bullet_sfx = $GameMusic/BulletImpactMusic
@onready var shooting_sfx = $GameMusic/ShootingMusic 
    
func _input(event):
    # 攻击输入事件,即射击
    if event.is_action_pressed("ui_attack"):
        # 获取当前时间
        var now = Time.get_ticks_msec()
        # 检查玩家是否可以射击,如果装填时间已过且我们有弹药
        if now >= bullet_fired_time and ammo_pickup > 0:
            # 音效
            shooting_sfx.play()
            # 射击动画
            is_attacking = true
            var animation  = "attack_" + returned_direction(new_direction)
            animation_sprite.play(animation)
            # 将子弹发射时间设置为当前时间加上装填时间
            bullet_fired_time = now + bullet_reload_time
            # 减少弹药并发出信号
            ammo_pickup = ammo_pickup - 1
            ammo_pickups_updated.emit(ammo_pickup)

对于敌人,我们需要更新它的process()函数,以便它也有一个装填时间,这样它就不会过度循环播放音效。我们在玩家脚本中已经这样做了。

Enemy.gd

# 音频节点
@onready var bullet_sfx = $GameMusic/BulletImpactMusic
@onready var shooting_sfx = $GameMusic/ShootingMusic 

#------------------------------------ 伤害与生命 ---------------------------------
func _process(delta):
    # 恢复敌人的生命值
    health = min(health + health_regen * delta, max_health)
    # 获取当前时间
    var now = Time.get_ticks_msec()
    # 检查敌人是否可以射击
    if now >= bullet_fired_time:
        # 目标是什么?
        var target = $RayCast2D.get_collider()
        if target != null:
            if target.name == "Player" and player.health > 0: 
                # 音效
                shooting_sfx.play()
                # 射击动画
                is_attacking = true
                var animation  = "attack_" + returned_direction(new_direction)
                animation_sprite.play(animation)
                # 将装填时间设置为子弹发射时间
                bullet_fired_time = now + bullet_reload_time

现在如果你运行场景,当你按下CTRL时,玩家的射击音效应该会播放,而当敌人攻击你时,敌人的射击音效也会播放。

敌人死亡音效
最后,我们还希望在敌人死亡时播放音效。

在你的EnemySpawner场景中添加一个新的AudioPlayer2D节点,并将其命名为“Death Music”。音频文件应该是“dmg0.wav”。
23.jpg
我们将在生成器减少敌人数量时播放这个音效。

EnemySpawner.gd

# 音频节点
@onready var death_sfx = $GameMusic/DeathMusic

# 移除敌人
func _on_enemy_death():
    enemy_count = enemy_count - 1
    death_sfx.play()

如果你运行场景并杀死敌人,音效应该会播放。

在主场景中播放不同的音乐
在我们的Main_2场景中,我们希望播放“Imposter Syndrome (wav)”音频轨道,而不是我们分配给玩家的背景音乐。为此,我们可以简单地重新分配我们的流资源,然后再次播放节点。

在我们这样做之前,我们必须重新导入“Imposter Syndrome (wav)”文件,使其成为一个循环音频文件。如果我们不这样做,它会在播放完毕后停止。要重新导入它,点击它并打开导入面板。将其导入模式设置为“Forward”,然后重新导入。

你会注意到我们还没有以循环模式导入任何其他音频文件,这是因为在Godot 4中,WAV文件的默认导入设置是识别并利用循环,而MP3设置则不是。因此,如果我们使用的是.mp3扩展名的音频文件,我们会导入它们以启用循环,但由于我们使用的是.wav音频文件,Godot引擎会自动识别哪些音频应该循环。
24.jpg

Main_2.gd

extends Node2D

@onready var background_music = $Player/GameMusic/BackgroundMusic

# 连接信号到函数 
func _ready():
    background_music.stream = load("res://Assets/FX/Music/Free Retro SFX by @inertsongs/Imposter Syndrome (theme).wav")
    background_music.play()
    
# 切换场景
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)

# 只有在场景切换后,我们才释放资源     
func _on_scene_changed():
    queue_free()

现在如果你在不同的场景之间来回切换,你的音乐应该会改变。

就是这样。你现在有了一个完整的游戏,包含音乐、GUI、敌人、NPC和一个基本任务!如果这是你的终点,你已经准备好进入下一个项目,那么我将在下一部分中向你展示如何测试、调试和导出你的项目。记得保存并备份,我们下一部分见。

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

添加新评论