chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -0,0 +1,622 @@
# 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 dB1.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<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. 编辑器友好设计
- `AudioManager` Custom Inspector实时显示当前 BGM 名称、Source A/B 音量、当前 Snapshot
- AudioConfigSO 提供 `[播放预览]` 按钮Editor Only在 Inspector 中点击可直接预听对应 BGM/SFXUI 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 | 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,金属感 |