# 12 · 音频系统 > **命名空间** `BaseGames.Audio` > **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md) > **依赖** `BaseGames.Core.Events` · Unity Audio · AudioMixer --- ## 目录 1. [设计原则](#1-设计原则) 2. [AudioMixer 架构](#2-audiomixer-架构) 3. [AudioManager](#3-audiomanager) 4. [BGMController — 自适应音乐](#4-bgmcontroller--自适应音乐) 5. [音乐状态机](#5-音乐状态机) 6. [AudioZone — 区域音乐触发](#6-audiozone--区域音乐触发) 7. [AudioEventSO — SFX 集成](#7-audioeventsso--sfx-集成) 8. [GlobalSFXPlayer](#8-globalsfxplayer) 9. [音频资产规范](#9-音频资产规范) 10. [AudioConfigSO](#10-audioconfigso) 11. [SettingsManager — 音量持久化](#11-settingsmanager--音量持久化) 12. [事件频道](#12-事件频道) 13. [编辑器友好设计](#13-编辑器友好设计) --- ## 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 滑条映射): ```csharp 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 全局播放: ```csharp 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 角色,下次切换时复用 ``` ```csharp 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 曲目: ```csharp 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: ```csharp 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](./07_FeedbackSystem.md) 中定义,本系统扩展其与 AudioMixer 的集成: ```csharp [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`: ```csharp // 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 引用集中配置,避免硬编码引用: ```csharp [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`: ```csharp [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(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. 编辑器友好设计 - `AudioManager` Custom Inspector:实时显示当前 BGM 名称、Source A/B 音量、当前 Snapshot - AudioConfigSO 提供 `[播放预览]` 按钮(Editor Only),在 Inspector 中点击可直接预听对应 BGM/SFX(UI Toolkit `Button`,`CreateInspectorGUI()` 中添加) - `AudioZone` Gizmo:在 Scene View 显示音频区域范围(半透明绿色圆圈 + regionId 文字标签) - `BGMController` Inspector:显示当前 `MusicState` 枚举状态 + 当前区域 ID --- ## 14. 脚步声材质分层(Footstep Material System) 脚步声不使用单一 SFX,而是根据脚下地面材质动态切换,增加环境真实感。 ### 14.1 地面材质枚举 ```csharp public enum FootstepMaterial { Stone, // 石板地(默认) Dirt, // 泥土/草地 Wood, // 木板 Metal, // 金属格栅 Water, // 浅水区(溅水声) Sand, // 沙地(细碎声) Grass, // 草丛(沙沙声) Cave, // 洞穴(回响加强) } ``` ### 14.2 FootstepAudioConfigSO ```csharp [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` 组件: ```csharp public class FootstepMaterialMarker : MonoBehaviour { public FootstepMaterial material; } ``` 玩家落地/行走时,通过当前碰撞的地面 GameObject 获取 `FootstepMaterialMarker.material`,驱动脚步声选择。若地面无标记,默认使用 `Stone`。 ### 14.4 播放时机 - **落地**:`PlayerController.OnLanded()` 播放较响的落地音效(同一 MaterialEntry,但音量×1.5) - **行走**:Animancer 动画事件(`FootstepL` / `FootstepR` event 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(攻击声变闷) ``` ```csharp // 进入/退出液体区域时过渡 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,金属感 |