20 KiB
12 · 音频系统
命名空间
BaseGames.Audio
所属文档集 ← 返回索引 · 总览
依赖BaseGames.Core.Events· Unity Audio · AudioMixer
目录
- 设计原则
- AudioMixer 架构
- AudioManager
- BGMController — 自适应音乐
- 音乐状态机
- AudioZone — 区域音乐触发
- AudioEventSO — SFX 集成
- GlobalSFXPlayer
- 音频资产规范
- AudioConfigSO
- SettingsManager — 音量持久化
- 事件频道
- 编辑器友好设计
1. 设计原则
- 分层混音:所有音频通过 AudioMixer 路由,统一控制音量分组,不直接设置
AudioSource.volume - 自适应音乐:BGM 根据游戏状态(探索/战斗/Boss)动态切换,保持氛围沉浸感
- 零耦合:
BGMController只订阅事件频道,不持有GameManager、EnemyBase等引用 - 像素风音效:优先使用复古 8-bit / 16-bit 风格音效,随机 Pitch 变化(±5%~10%)防止重复感
- 2D 空间音频:本游戏为 2D 横版,不使用 3D 音效空间化(
AudioSource.spatialBlend = 0),仅靠左右声道区分近远
2. AudioMixer 架构
MainMixer.mixer 资产路径:Assets/Audio/MainMixer.mixer
混音组层级
Master
├── BGM (背景音乐:探索BGM、Boss BGM、主菜单BGM)
├── SFX (音效:战斗、UI、环境互动)
│ ├── SFX_Player (玩家动作音效,子混音组)
│ ├── SFX_Enemy (敌人音效,子混音组)
│ └── SFX_World (世界互动音效,子混音组)
└── Ambient (环境音:风声、水声、洞穴回声等)
Exposed Parameters(暴露给代码控制的参数名)
| 参数名 | 对应混音组 | 范围 | 说明 |
|---|---|---|---|
MasterVolume |
Master | -80 ~ 0 dB | 总音量 |
BGMVolume |
BGM | -80 ~ 0 dB | 背景音乐音量 |
SFXVolume |
SFX | -80 ~ 0 dB | 音效总音量 |
AmbientVolume |
Ambient | -80 ~ 0 dB | 环境音音量 |
线性值转分贝(Settings 滑条映射):
public static float LinearToDecibel(float linear)
=> linear > 0.0001f ? 20f * Mathf.Log10(linear) : -80f;
AudioMixer 快照(Snapshots)
| 快照名 | 用途 | 关键差异 |
|---|---|---|
Default |
正常游玩 | 所有组正常音量 |
Paused |
游戏暂停 | BGM / SFX 均降低 -12 dB,添加低通滤波(Cutoff 500Hz) |
Dead |
玩家死亡 | BGM 渐出 -80 dB(1.5s 过渡) |
BossFight |
Boss 战 | Ambient 降低 -20 dB(突出 Boss BGM) |
快照切换:_mixer.TransitionToSnapshots(new[] { snapshot }, new[] { 1f }, transitionTime)
3. AudioManager
AudioManager 常驻 Persistent 场景,管理 BGM AudioSource 和 SFX 全局播放:
namespace BaseGames.Audio
{
[DefaultExecutionOrder(-500)]
public class AudioManager : MonoBehaviour
{
[Header("AudioMixer")]
[SerializeField] AudioMixer _mixer;
[Header("BGM Sources")]
[SerializeField] AudioSource _bgmSourceA; // 双 Source 交叉淡入淡出
[SerializeField] AudioSource _bgmSourceB;
[Header("SFX Source")]
[SerializeField] AudioSource _globalSFXSource; // 一次性 SFX 播放
[Header("Config")]
[SerializeField] AudioConfigSO _config;
// 对外接口
public void Initialize(); // SettingsManager 读取 → 应用音量
public void SetVolume(string exposedParam, float linear); // 滑条回调
public void PlayBGM(AudioClip clip, float fadeOutDur = 1f, float fadeInDur = 1f);
public void StopBGM(float fadeOutDur = 1f);
public void PlaySFX(AudioClip clip, float volumeScale = 1f);
public void TransitionToSnapshot(string snapshotName, float transitionTime);
}
}
BGM 交叉淡入淡出
使用双 AudioSource(A/B)实现无缝交叉淡变:
当前播放 → Source A(淡出 fadeOutDur)
新曲目 → Source B(淡入 fadeInDur)
淡出结束 → 交换 A/B 角色,下次切换时复用
IEnumerator CrossFade(AudioClip newClip, float fadeOutDur, float fadeInDur)
{
var outSource = _activeSource;
var inSource = _inactiveSource;
inSource.clip = newClip;
inSource.volume = 0f;
inSource.Play();
float t = 0f;
while (t < Mathf.Max(fadeOutDur, fadeInDur))
{
t += Time.unscaledDeltaTime;
outSource.volume = Mathf.Lerp(1f, 0f, t / fadeOutDur);
inSource.volume = Mathf.Lerp(0f, 1f, t / fadeInDur);
yield return null;
}
outSource.Stop();
(_activeSource, _inactiveSource) = (inSource, outSource);
}
4. BGMController — 自适应音乐
BGMController 订阅各种游戏事件,自动切换 BGM 曲目:
public class BGMController : MonoBehaviour
{
[SerializeField] AudioManager _audioManager;
[SerializeField] AudioConfigSO _config;
// 监听的事件频道
[SerializeField] GameStateEventChannelSO _onGameStateChanged;
[SerializeField] StringEventChannelSO _onRegionEntered;
[SerializeField] BoolEventChannelSO _onBossFightToggled;
string _currentRegion = "Forest";
void OnGameStateChanged(GameState state)
{
switch (state)
{
case GameState.MainMenu:
_audioManager.PlayBGM(_config.mainMenuBGM, fadeOut: 0.5f, fadeIn: 1.0f);
break;
case GameState.BossFight:
// BGMController 不直接切换,等待 OnBossFightToggled
break;
case GameState.Dead:
_audioManager.TransitionToSnapshot("Dead", 1.5f);
break;
case GameState.Paused:
_audioManager.TransitionToSnapshot("Paused", 0.2f);
break;
case GameState.Gameplay:
_audioManager.TransitionToSnapshot("Default", 0.3f);
PlayRegionBGM(_currentRegion);
break;
}
}
void OnRegionEntered(string regionId)
{
_currentRegion = regionId;
if (_musicState == MusicState.Exploration)
PlayRegionBGM(regionId);
}
void OnBossFightToggled(bool started)
{
if (started)
{
_musicState = MusicState.Boss;
_audioManager.PlayBGM(_config.GetBossBGM(_currentRegion), fadeOut: 1f, fadeIn: 0.5f);
_audioManager.TransitionToSnapshot("BossFight", 0.5f);
}
else
{
// Boss 击败:播放胜利 Sting,然后恢复探索 BGM
StartCoroutine(PlayVictoryThenRestore());
}
}
IEnumerator PlayVictoryThenRestore()
{
_musicState = MusicState.Victory;
_audioManager.PlayBGM(_config.victoryStingBGM, fadeOut: 0.3f, fadeIn: 0.1f);
yield return new WaitForSecondsRealtime(_config.victoryStingDuration);
_musicState = MusicState.Exploration;
PlayRegionBGM(_currentRegion);
_audioManager.TransitionToSnapshot("Default", 1.0f);
}
}
5. 音乐状态机
MusicState 枚举:
Exploration ← 默认:区域探索 BGM
Boss ← Boss 战:Boss 主题 BGM
Victory ← Boss 击败后短暂胜利音乐
None ← 过场/死亡/主菜单时由 BGMController 直接切换
状态转换:
Exploration ──[OnBossFightToggled(true)]──► Boss
◄─[OnBossFightToggled(false)]──
Boss ──[Boss 击败动画完成]──────────► Victory
Victory ──[VictorySting 播放完毕]───────► Exploration
Exploration ──[OnRegionEntered]────────────► Exploration(切换同状态内不同曲目)
6. AudioZone — 区域音乐触发
AudioZone 挂载在每个区域的入口触发器上,通知 BGMController 切换对应 BGM:
public class AudioZone : MonoBehaviour
{
[SerializeField] string _regionId; // 如 "Forest", "Cave", "Ruins"
[SerializeField] StringEventChannelSO _onRegionEntered;
void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
_onRegionEntered.Raise(_regionId);
}
}
区域 BGM 配置在 AudioConfigSO 中(见 §10),BGMController 通过 regionId 查表获取对应 AudioClip。
7. AudioEventSO — SFX 集成
AudioEventSO 已在 07_FeedbackSystem.md 中定义,本系统扩展其与 AudioMixer 的集成:
[CreateAssetMenu(menuName = "Audio/AudioEvent")]
public class AudioEventSO : ScriptableObject
{
[SerializeField] AudioClip[] _clips; // 随机选取
[SerializeField] AudioMixerGroup _mixerGroup; // 指定路由到哪个子混音组
[Range(0f, 1f)]
[SerializeField] float _baseVolume = 1f;
[SerializeField] Vector2 _pitchRange = new(0.9f, 1.1f); // 随机 Pitch 范围
public void Play(AudioSource source)
{
if (_clips.Length == 0) return;
source.outputAudioMixerGroup = _mixerGroup;
source.clip = _clips[Random.Range(0, _clips.Length)];
source.volume = _baseVolume;
source.pitch = Random.Range(_pitchRange.x, _pitchRange.y);
source.Play();
}
public void PlayOneShot(AudioSource source)
{
if (_clips.Length == 0) return;
source.outputAudioMixerGroup = _mixerGroup;
float pitch = Random.Range(_pitchRange.x, _pitchRange.y);
source.pitch = pitch;
source.PlayOneShot(_clips[Random.Range(0, _clips.Length)], _baseVolume);
}
}
Feel 的 MMF_AudioSource 持有 AudioEventSO 引用,调用 Play() 即可。
8. GlobalSFXPlayer
对于不依附于具体 GameObject 的一次性 SFX(如 UI 按钮音效),使用 GlobalSFXPlayer:
// AudioManager 的便捷方法(内部持有专用 AudioSource)
public void PlaySFXGlobal(AudioEventSO audioEvent)
=> audioEvent.PlayOneShot(_globalSFXSource);
UI 按钮的 onClick.AddListener(() => AudioManager.Instance.PlaySFXGlobal(btnClickSFX))。
9. 音频资产规范
文件路径
Assets/Audio/
├── BGM/
│ ├── BGM_MainMenu.ogg
│ ├── BGM_Forest_Exploration.ogg
│ ├── BGM_Cave_Exploration.ogg
│ ├── BGM_Ruins_Exploration.ogg
│ ├── BGM_Boss_Forest.ogg
│ ├── BGM_Boss_Cave.ogg
│ └── BGM_Victory_Sting.ogg
├── SFX/
│ ├── Player/
│ │ ├── SFX_Player_Jump.wav
│ │ ├── SFX_Player_Dash.wav
│ │ ├── SFX_Player_Attack_01/02/03.wav (连击三段各一个)
│ │ ├── SFX_Player_Hurt.wav
│ │ ├── SFX_Player_Death.wav
│ │ └── SFX_Parry_Success.wav
│ ├── Enemy/
│ │ ├── SFX_Enemy_Hit.wav
│ │ ├── SFX_Enemy_Death.wav
│ │ └── SFX_Enemy_Alert.wav
│ ├── World/
│ │ ├── SFX_Collectible_Geo.wav
│ │ ├── SFX_SavePoint_Activate.wav
│ │ ├── SFX_Door_Open.wav
│ │ └── SFX_Platform_Crumble.wav
│ └── UI/
│ ├── SFX_UI_Confirm.wav
│ ├── SFX_UI_Cancel.wav
│ └── SFX_UI_Navigate.wav
└── Ambient/
├── AMB_Forest_Wind.ogg
├── AMB_Cave_Drip.ogg
└── AMB_Ruins_Echo.ogg
音频导入设置规范
| 类型 | 格式 | Load Type | 说明 |
|---|---|---|---|
| BGM | .ogg |
Streaming | 大文件流式读取,节省内存 |
| SFX(短促) | .wav |
Decompress on Load | 最低延迟,内存占用接受 |
| SFX(循环环境) | .ogg |
Compressed in Memory | 中等大小,压缩存储 |
| Ambient | .ogg |
Streaming | 长音频流式读取 |
10. AudioConfigSO
所有 BGM 和关键 SFX 引用集中配置,避免硬编码引用:
[CreateAssetMenu(menuName = "Config/AudioConfig")]
public class AudioConfigSO : ScriptableObject
{
[Header("BGM")]
public AudioClip mainMenuBGM;
public AudioClip victoryStingBGM;
public float victoryStingDuration = 3f;
[Header("Region BGMs")]
public RegionBGM[] regionBGMs; // RegionBGM { string regionId; AudioClip clip; AudioClip bossBGM; }
[Header("Fade Settings")]
public float defaultFadeOut = 1.0f;
public float defaultFadeIn = 1.0f;
public AudioClip GetRegionBGM(string regionId)
=> Array.Find(regionBGMs, r => r.regionId == regionId)?.clip;
public AudioClip GetBossBGM(string regionId)
=> Array.Find(regionBGMs, r => r.regionId == regionId)?.bossBGM;
}
资产路径:Assets/ScriptableObjects/Config/AudioConfigSO.asset
11. SettingsManager — 音量持久化
设置独立于存档系统,写入 Application.persistentDataPath/settings.json:
[Serializable]
public class SettingsData
{
public float masterVolume = 1.0f;
public float bgmVolume = 0.8f;
public float sfxVolume = 1.0f;
public float ambientVolume = 0.6f;
public bool haptics = true;
// P1: keybindings[]
}
public class SettingsManager : MonoBehaviour
{
public void Initialize()
{
_data = Load() ?? new SettingsData();
Apply(_data);
}
public void Apply(SettingsData data)
{
_audioManager.SetVolume("MasterVolume", data.masterVolume);
_audioManager.SetVolume("BGMVolume", data.bgmVolume);
_audioManager.SetVolume("SFXVolume", data.sfxVolume);
_audioManager.SetVolume("AmbientVolume",data.ambientVolume);
}
public void Save() => File.WriteAllText(_savePath, JsonUtility.ToJson(_data));
SettingsData Load() => File.Exists(_savePath) ? JsonUtility.FromJson<SettingsData>(File.ReadAllText(_savePath)) : null;
}
12. 事件频道
新增频道(Assets/ScriptableObjects/Events/Audio/):
| 资产名 | 类型 | 用途 |
|---|---|---|
OnRegionEntered.asset |
StringEventChannelSO |
AudioZone 触发,传递 regionId |
OnBGMChangeRequested.asset |
StringEventChannelSO |
P1:显式请求切换 BGM(过场等) |
OnBossFightToggled.asset、OnGameStateChanged.asset在其他频道组已定义,BGMController 直接订阅。
13. 编辑器友好设计
AudioManagerCustom Inspector:实时显示当前 BGM 名称、Source A/B 音量、当前 Snapshot- AudioConfigSO 提供
[播放预览]按钮(Editor Only),在 Inspector 中点击可直接预听对应 BGM/SFX(UI ToolkitButton,CreateInspectorGUI()中添加) AudioZoneGizmo:在 Scene View 显示音频区域范围(半透明绿色圆圈 + regionId 文字标签)BGMControllerInspector:显示当前MusicState枚举状态 + 当前区域 ID
14. 脚步声材质分层(Footstep Material System)
脚步声不使用单一 SFX,而是根据脚下地面材质动态切换,增加环境真实感。
14.1 地面材质枚举
public enum FootstepMaterial
{
Stone, // 石板地(默认)
Dirt, // 泥土/草地
Wood, // 木板
Metal, // 金属格栅
Water, // 浅水区(溅水声)
Sand, // 沙地(细碎声)
Grass, // 草丛(沙沙声)
Cave, // 洞穴(回响加强)
}
14.2 FootstepAudioConfigSO
[CreateAssetMenu(menuName = "BaseGames/Audio/FootstepAudioConfig")]
public class FootstepAudioConfigSO : ScriptableObject
{
[System.Serializable]
public struct MaterialEntry
{
public FootstepMaterial material;
public AudioClip[] clips; // 随机选一个,防止重复感
[Range(0f, 1f)] public float volume;
[Range(0.8f, 1.2f)] public float pitchVariance; // 每次随机 pitch 偏移范围
}
public MaterialEntry[] entries;
}
14.3 地面标记
在 Tilemap 的每个 Tile 上(或碰撞体所在 GameObject 上)添加 FootstepMaterialMarker 组件:
public class FootstepMaterialMarker : MonoBehaviour
{
public FootstepMaterial material;
}
玩家落地/行走时,通过当前碰撞的地面 GameObject 获取 FootstepMaterialMarker.material,驱动脚步声选择。若地面无标记,默认使用 Stone。
14.4 播放时机
- 落地:
PlayerController.OnLanded()播放较响的落地音效(同一 MaterialEntry,但音量×1.5) - 行走:Animancer 动画事件(
FootstepL/FootstepRevent tag)触发脚步声播放 - 冲刺起步:
Dash动画第2帧触发DashSFX(不用 Footstep,用单独 SFX)
15. 水下音效处理(Underwater Audio)
进入 LiquidZone(见 40_LiquidSwimSystem)时,全局音效自动应用水下 DSP 处理。
15.1 水下 AudioMixer Snapshot
Snapshot: "Underwater"
BGM Bus: Low-Pass Filter 切割频率 800 Hz(水下声音沉闷)
SFX Bus: Low-Pass Filter 1200 Hz + Volume ×0.7
Ambient Bus: Volume ×0 + 替换为水下环境音(气泡声)
Reverb Bus: Room Size 增大,Decay Time 1.8s(水下混响长)
PlayerSFX Bus: Low-Pass Filter 1000 Hz(攻击声变闷)
// 进入/退出液体区域时过渡
public class UnderwaterAudioController : MonoBehaviour
{
[SerializeField] AudioMixer _mixer;
[SerializeField] float _transitionDuration = 0.3f;
public void EnterWater()
{
_mixer.FindSnapshot("Underwater")
.TransitionTo(_transitionDuration);
// 同时降低 BGM 音量,增强代入感
}
public void ExitWater()
{
_mixer.FindSnapshot("Default")
.TransitionTo(_transitionDuration);
}
}
15.2 水下专属 SFX
| 动作 | 水上 SFX | 水下 SFX |
|---|---|---|
| 攻击 | sfx_player_slash |
sfx_player_slash_underwater(低频混响版) |
| 跳跃(浮出水面) | — | sfx_splash_exit |
| 入水 | — | sfx_splash_enter |
| 游泳移动 | — | sfx_swim_loop(循环,随速度调整 pitch) |
16. 距离衰减曲线(Distance Attenuation)
所有世界空间中的 3D 音源(非 2D UI 音效)使用统一的衰减配置。
16.1 衰减模式
游戏是 2D 横版卷轴,使用 AudioSource.spatialBlend = 0(纯2D)为主,仅以下场景例外:
| 场景 | 实现方式 |
|---|---|
| 大型场景中的远景环境音(瀑布/机械声) | AudioSource.spatialBlend = 1.0,自定义 Volume Rolloff 曲线 |
| NPC 的近身对话语音 | spatialBlend = 0.5,增加位置感但不做完整 3D |
| 远处 Boss 的警示音效(在门口听到 Boss 移动声) | spatialBlend = 0.7,最大距离 30 个世界单位 |
16.2 推荐衰减曲线参数(用于 3D 音源)
| 参数 | 值 |
|---|---|
| Min Distance | 2(2单位内全音量) |
| Max Distance | 25(超过25单位静音) |
| Rolloff Mode | Custom Curve(非线性,靠近时迅速增大,见下方) |
| Volume Rolloff | 2~5: ×1.0 → 5~15: ×0.6 → 15~25: ×0.2 → >25: ×0 |
Volume
1.0 |───╮
| ╲
0.6 | ╲──╮
| ╲
0.2 | ╲──╮
0.0 | ╲────
+─────────────────────> Distance
0 5 10 15 20 25
16.3 Reverb Zone(区域混响)
每个主区域在 Unity 的 AudioReverbZone 组件中配置环境混响:
| 区域 | ReverbPreset | 额外调整 |
|---|---|---|
| 森林 | Forest |
DecayTime 0.8s |
| 地穴 | Cave |
DecayTime 2.0s,高频较少 |
| 废墟 | Room |
DecayTime 1.2s |
| 深渊 | Cave(更大) |
DecayTime 3.5s,Bass 增强 |
| 核心 | Hallway |
DecayTime 1.8s,金属感 |