让我们通过制作 RPG 来学习 Godot 4 — 第 22 部分:音乐和音效
在这一部分,我们将通过为游戏添加一些音乐和音效(SFX)来为游戏增添最后的生命力。我们希望在主菜单、暂停、死亡界面以及主场景中都有背景音乐。同时,我们还会为玩家和敌人添加射击和受伤的音效,以及对话音乐和拾取音效。
你将学习到的内容:
- 如何使用
AudioStreamPlayer
节点。 - 如何使用
AudioStreamPlayer2D
节点。 - 如何在代码中播放、设置和停止音频流。
- 如何在导入面板中循环播放音频文件。
在 Godot 中,你有三种主要的音频播放方式:
- AudioStreamPlayer:这是一个通用的音频播放器,适合播放音乐或任何非位置性的音频。当你不需要音频在游戏世界中具有位置时使用它。例如,背景音乐或对话音乐,无论角色或对象在游戏中的位置如何,都应该播放。
- AudioStreamPlayer2D:这是为 2D 游戏设计的,适用于需要位置音频的情况。它模拟声音在 2D 空间中的位置,声音离听者(通常是摄像机)越远,声音越小。当你制作 2D 游戏并希望音频在 2D 世界中具有位置时使用它(例如,屏幕上某个位置发出的音效)。
- AudioStreamPlayer3D:类似于
AudioStreamPlayer2D
,但适用于 3D 游戏。它考虑了声音在三维空间中的位置。在 3D 游戏中,当你希望声音从 3D 世界中的特定位置发出时使用它。
在本教程中,我们将使用 AudioStreamPlayer
节点来处理稳定的声音,如背景音乐或对话音乐。而对于音效,我们将使用 AudioStreamPlayer2D
节点,因为我们希望声音有一定的声像变化,并且声音的响度取决于声音的位置。
主菜单音乐
打开你的 MainScene
场景,添加一个名为 AudioStreamPlayer
的新节点,并将其重命名为 BackgroundMusic
。
在检查器面板中,我们可以为该音频播放器节点分配要播放的音频文件。点击检查器面板中 Stream
属性旁边的 <empty>
,选择“快速加载”。这将设置要播放的 AudioStream
对象。
我们希望背景音乐播放音频文件 We Ride at Dawn.wav
。你可以在 Assets > Music
目录下找到所有音乐文件。
在检查器面板中,你可以设置其播放和自动播放的值。如果 playing
值为 true
,音频正在播放或排队等待播放(参见 play
)。如果 autoplay
为 true
,音频将在场景加载时播放。建议你阅读文档以了解其他属性的作用,例如 bus
或 mix target
。
我们希望音乐在游戏加载时立即播放,因此需要启用 autoplay
。如果你想听音乐,可以启用 playing
,但之后记得禁用它!
现在运行你的场景,音乐应该默认播放,但仅当你在主菜单场景时。
暂停菜单
在 Player
场景中,添加一个 AudioStreamPlayer
节点,并将其重命名为 PauseMenuMusic
。
让我们将所有音乐组织在一个名为 GameMusic
的 Node2D
节点下。
我们还希望在暂停菜单打开时播放 We Ride at Dawn.wav
。不要启用 autoplay
或 playing
,因为我们将在代码中设置该节点仅在暂停菜单打开时播放。
要播放音频,我们只需引用节点并调用其 .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
,因为我们只希望在游戏处于暂停状态时处理该节点。
我们还需要在退出场景或恢复游戏时停止播放音乐,否则它会覆盖其他音频。我们可以通过 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
,因为我们希望此音频轨道默认播放。
我们需要在暂停菜单打开时停止播放此音乐,因此更新你的 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
。
更新你的 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()
如果你运行场景,玩家死亡时音乐应该会播放。
对话音乐
我们必须在两个地方设置对话音乐,即 Player
和 DialogPopup
脚本中。如果玩家与 NPC 互动,对话音乐应该播放;如果玩家结束互动并关闭弹窗,背景音乐应该播放。
添加一个新的 AnimationPlayer
节点,音频轨道为 Whiskey Barn Dance (loop).wav
。将其处理模式设置为 When Paused
。你也可以使用 Imposter Syndrome (wav)
音频。
我们在 Player
脚本中播放音频,因为如果我们在 DialogPopup
的 open()
函数中播放,每次弹窗更改时音乐都会重新开始!按如下方式播放和停止对话音乐:
# 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
。
现在,在我们的 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
。
在代码中,我们需要在玩家拾取物品时播放此音频。我们可以在 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
。
在输入代码中,更新 ui_consume_health
和 ui_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”。将这个节点添加到你的敌人和玩家场景中。
*注意:不是SprintingMusic
,而是BulletImpactMusic
。
在我们与节点碰撞后,播放这个音效。
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”。将这个节点添加到你的敌人和玩家场景中。
让我们在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”。
我们将在生成器减少敌人数量时播放这个音效。
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引擎会自动识别哪些音频应该循环。
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和一个基本任务!如果这是你的终点,你已经准备好进入下一个项目,那么我将在下一部分中向你展示如何测试、调试和导出你的项目。记得保存并备份,我们下一部分见。