Files
zeling_v2/Docs/Design/12_AudioSystem.md
2026-05-08 11:04:00 +08:00

20 KiB
Raw Permalink Blame History

12 · 音频系统

命名空间 BaseGames.Audio
所属文档集 ← 返回索引 · 总览
依赖 BaseGames.Core.Events · Unity Audio · AudioMixer


目录

  1. 设计原则
  2. AudioMixer 架构
  3. AudioManager
  4. BGMController — 自适应音乐
  5. 音乐状态机
  6. AudioZone — 区域音乐触发
  7. AudioEventSO — SFX 集成
  8. GlobalSFXPlayer
  9. 音频资产规范
  10. AudioConfigSO
  11. SettingsManager — 音量持久化
  12. 事件频道
  13. 编辑器友好设计

1. 设计原则

  • 分层混音:所有音频通过 AudioMixer 路由,统一控制音量分组,不直接设置 AudioSource.volume
  • 自适应音乐BGM 根据游戏状态(探索/战斗/Boss动态切换保持氛围沉浸感
  • 零耦合BGMController 只订阅事件频道,不持有 GameManagerEnemyBase 等引用
  • 像素风音效:优先使用复古 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 dB1.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 交叉淡入淡出

使用双 AudioSourceA/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 中(见 §10BGMController 通过 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.assetOnGameStateChanged.asset 在其他频道组已定义BGMController 直接订阅。


13. 编辑器友好设计

  • AudioManager Custom Inspector实时显示当前 BGM 名称、Source A/B 音量、当前 Snapshot
  • AudioConfigSO 提供 [播放预览] 按钮Editor Only在 Inspector 中点击可直接预听对应 BGM/SFXUI Toolkit ButtonCreateInspectorGUI() 中添加)
  • AudioZone Gizmo在 Scene View 显示音频区域范围(半透明绿色圆圈 + regionId 文字标签)
  • BGMController Inspector显示当前 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 / 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攻击声变闷
// 进入/退出液体区域时过渡
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 22单位内全音量
Max Distance 25超过25单位静音
Rolloff Mode Custom Curve非线性靠近时迅速增大见下方
Volume Rolloff 25: ×1.0 → 515: ×0.6 → 1525: ×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.5sBass 增强
核心 Hallway DecayTime 1.8s,金属感