# Audio 模块使用文档 ## 1. 模块目标 `Audio` 模块为框架提供统一的音频播放、缓存、回收、3D 空间音频与配置管理能力。 当前实现目标: - 高频播放路径尽量稳定,避免热路径 GC - `AudioClip` / `AudioSource` 生命周期可控,避免泄漏 - 支持 `Music`、`Sound`、`UISound`、`Voice`、`Ambient` - 支持 2D 音频、3D 定点音频、跟随目标音频 - 支持距离衰减、空间混合、遮挡低通 - 配置从组件 Inspector 中抽离为可复用 `ScriptableObject` 模块核心代码位于: - `Runtime/Audio/AudioComponent.cs` - `Runtime/Audio/IAudioService.cs` - `Runtime/Audio/AudioService.cs` - `Runtime/Audio/AudioCategory.cs` - `Runtime/Audio/AudioAgent.cs` - `Runtime/Audio/AudioGroupConfig.cs` - `Runtime/Audio/AudioGroupConfigCollection.cs` ## 2. 架构概览 模块运行时由以下几层组成: ### 2.1 AudioComponent `AudioComponent` 是场景中的挂载入口,负责: - 注册 `AudioService` - 绑定 `AudioMixer` - 绑定 `AudioListener` - 绑定 `AudioGroupConfigCollection` - 初始化运行时音频系统 ### 2.2 AudioService `AudioService` 是模块核心调度层,负责: - 播放请求入口 - `AudioType` 维度的音量与开关管理 - `AudioClip` 缓存、引用计数、TTL、LRU - `AudioSource` 对象池管理 - 播放句柄分配与控制 - 监听器注册 ### 2.3 AudioCategory 每个 `AudioType` 对应一个 `AudioCategory`,负责: - 维护该分类下的固定数量 `AudioAgent` - 用空闲栈管理可用槽位 - 用最老播放堆管理满载抢占 - 更新该分类下活跃音频 ### 2.4 AudioAgent 每个 `AudioAgent` 对应一个运行时播放槽位,负责: - 绑定一个 `AudioSource` - 管理单条播放状态 - 管理当前 `AudioClip` 引用 - 处理跟随、淡出、遮挡检测 ### 2.5 AudioSourceObject `AudioSourceObject` 是 `AudioSource` 的池化包装对象,接入框架 `ObjectPoolService`。 ### 2.6 AudioClipCacheEntry `AudioClipCacheEntry` 是单个地址的缓存条目,负责: - `AssetHandle` - `AudioClip` - 引用计数 - LRU 链表节点 - pending 加载请求链 ## 3. 生命周期 ### 3.1 初始化 初始化链路: 1. 场景中挂载 `AudioComponent` 2. `Awake` 时注册 `AudioService` 3. `Start` 时读取 `AudioGroupConfigCollection` 4. 显式注册 `AudioListener` 5. 创建每个分类的 `AudioCategory` 6. 为每个分类预创建固定数量 `AudioAgent` 7. 每个 `AudioAgent` 从 `ObjectPoolService` 获取一个 `AudioSourceObject` ### 3.2 播放 播放链路: 1. 业务通过 `IAudioService` 发起播放 2. `AudioService` 生成池化播放请求 3. 对应 `AudioCategory` 从空闲栈取槽位,或从最老播放堆抢占 4. `AudioAgent` 绑定配置 5. 直接播放 `AudioClip` 或进入 `AudioClip` 加载/缓存逻辑 6. 播放完成、停止、淡出结束后释放引用 ### 3.3 回收 回收链路: - `AudioAgent` 停止后释放 `AudioClip` 引用 - `AudioClip` 引用计数归零后进入 LRU - 到达 TTL 或容量超限时淘汰 - 服务销毁时统一停止播放、释放缓存、释放未使用 `AudioSource` ## 4. 场景配置 ### 4.1 AudioComponent 在场景中创建一个 GameObject 挂载 `AudioComponent`。 推荐字段配置: - `Audio Mixer` - 指向音频混音器资源 - `Instance Root` - 音频实例根节点 - `Audio Listener` - 显式绑定实际生效的监听器 - `Audio Group Configs` - 指向 `AudioGroupConfigCollection` 资源 ### 4.2 为什么要显式绑定 AudioListener 当前实现不再扫描全场景查找监听器。 这样做的原因: - 避免运行时全场景线性查找 - 避免多监听器场景中的不确定性 - 保证 3D 音频和遮挡始终基于明确目标 如果不手动绑定,`AudioComponent` 会在自身层级下尝试一次 `GetComponentInChildren(true)`。 推荐规则: - 主场景主相机上的监听器手动拖给 `AudioComponent` - 运行时如果切换监听器,应显式重新注册 ## 5. 配置资产 ### 5.1 AudioGroupConfigCollection `AudioGroupConfigCollection` 是一个 `ScriptableObject`,保存全部分类配置。 默认通过 `CreateAssetMenu` 创建: - `AlicizaX/Audio/Audio Group Configs` ### 5.2 默认配置 新建资源后默认包含以下五组: - `Music` - `Sound` - `UISound` - `Voice` - `Ambient` ### 5.3 AudioGroupConfig 字段说明 每个 `AudioGroupConfig` 包含: - `音频类型` - 对应 `AudioType` - `名称` - 配置展示名 - `静音` - 初始化时该分类是否关闭 - `音量` - 初始线性音量 - `通道数` - 该分类最大并发 `AudioAgent` 数量 - `Mixer音量参数` - 对应 `AudioMixer` 暴露参数名 - `空间混合` - 2D/3D 混合比例 - `多普勒` - 多普勒系数 - `扩散` - 声场扩散 - `AudioSource优先级` - Unity AudioSource Priority - `混响区混合` - Reverb Zone Mix - `衰减模式` - `AudioRolloffMode` - `最小距离` - 3D 音频近距离阈值 - `最大距离` - 3D 音频远距离阈值 - `开启遮挡` - 是否进行遮挡检测 - `遮挡检测层` - 遮挡 Raycast LayerMask - `遮挡检测间隔` - 检测频率 - `遮挡低通截止频率` - 遮挡时低通频率 - `遮挡音量系数` - 遮挡时音量乘数 ## 6. Inspector 使用 ### 6.1 AudioComponent Inspector `AudioComponentInspector` 提供: - `音频监听器` 字段 - `音频分组配置` 字段 - 一键创建默认配置资源按钮 ### 6.2 AudioGroupConfigCollection Inspector `AudioGroupConfigCollectionInspector` 提供: - 中文字段名 - 分类分块显示 - `Occlusion` 未开启时隐藏遮挡相关字段 ## 7. 运行时 API 核心接口为 `IAudioService`。 ### 7.1 全局开关 ```csharp IAudioService audio = GameApp.Audio; audio.Volume = 1f; audio.Enable = true; ``` ### 7.2 通用分类音量接口 推荐统一使用以下接口: ```csharp audio.SetCategoryVolume(AudioType.Music, 0.8f); audio.SetCategoryVolume(AudioType.Sound, 1f); float musicVolume = audio.GetCategoryVolume(AudioType.Music); ``` ### 7.3 通用分类开关接口 ```csharp audio.SetCategoryEnable(AudioType.Music, true); audio.SetCategoryEnable(AudioType.Ambient, false); bool ambientEnabled = audio.GetCategoryEnable(AudioType.Ambient); ``` ### 7.4 2D 地址播放 ```csharp ulong handle = audio.Play( AudioType.Sound, "Audio/SFX/Click", loop: false, volume: 1f, async: false, cacheClip: true); ``` 参数说明: - `type` - 分类 - `path` - 资源地址 - `loop` - 是否循环 - `volume` - 播放音量 - `async` - 是否异步加载 - `cacheClip` - 是否在播放后保留到缓存 ### 7.5 直接播放 AudioClip ```csharp ulong handle = audio.Play(AudioType.Music, clip, loop: true, volume: 1f); ``` ### 7.6 3D 定点播放 ```csharp Vector3 position = hitPoint; ulong handle = audio.Play3D( AudioType.Sound, "Audio/SFX/Explosion", position, loop: false, volume: 1f, async: true, cacheClip: true); ``` ### 7.7 跟随目标播放 ```csharp ulong handle = audio.PlayFollow( AudioType.Voice, "Audio/Voice/Npc001", npcTransform, Vector3.zero, loop: false, volume: 1f, async: true, cacheClip: true); ``` 注意: - 跟随播放不会把池化 `AudioSource` 挂到业务节点下 - 实际上是保持在音频根节点下,并同步世界位置/旋转 ### 7.8 停止、暂停、恢复 ```csharp audio.Stop(handle, fadeout: true); audio.Pause(handle); audio.Resume(handle); ``` ### 7.9 按分类停止 ```csharp audio.Stop(AudioType.Music, fadeout: true); audio.StopAll(fadeout: false); ``` ### 7.10 预加载与卸载 ```csharp audio.Preload(preloadList, pin: true); audio.Unload(unloadList); audio.ClearCache(); ``` 说明: - `Preload(..., pin: true)` - 预加载并常驻 - `Unload(...)` - 取消 pin,并在无引用时释放 - `ClearCache()` - 只清 unused 条目,不会强拆播放中和加载中的 clip ## 8. 句柄语义 播放返回值为 `ulong handle`。 句柄特点: - 不是 `AudioAgent` 对象引用 - 对外只暴露控制句柄,不暴露内部实现 - 使用代际句柄避免旧句柄误命中新播放实例 推荐规则: - 业务层如需后续停止某条音频,保存 `handle` - 不需要控制的瞬时音效可忽略返回值 ## 9. 缓存策略 ### 9.1 基本机制 每个 `AudioClip` 地址对应一个 `AudioClipCacheEntry`。 条目包含: - `AssetHandle` - `AudioClip` - `RefCount` - `Pinned` - `CacheAfterUse` - `Loading` - `PendingHead / PendingTail` - `LRU` 节点 ### 9.2 引用计数 播放开始时: - `RetainClip` 播放结束时: - `ReleaseClip` ### 9.3 LRU 与 TTL 当 `RefCount == 0` 时: - 若 `CacheAfterUse == true`,进入 LRU - 若 `CacheAfterUse == false`,立即清理 缓存会在以下情况被淘汰: - 超过容量 - 超过 TTL ### 9.4 Pinned 资源 通过 `Preload(..., pin: true)` 的资源为 pinned。 特点: - 不会被普通缓存淘汰 - 必须显式 `Unload(...)` 解除 pin ## 10. AudioSource 池化 ### 10.1 池化来源 `AudioSourceObject` 使用框架内置 `ObjectPoolService`。 每个分类初始化时会创建固定数量 `AudioAgent`。 每个 `AudioAgent` 都会对应一个可复用 `AudioSourceObject`。 ### 10.2 回收时机 模块销毁时: - 停止所有播放 - 分类销毁 - `AudioSourceObject` 归还池 - 未使用池对象统一释放 ## 11. 3D 空间音频 ### 11.1 空间混合 由以下字段决定: - `AudioGroupConfig.SpatialBlend` - 播放请求是否是 3D / Follow / WorldPosition ### 11.2 距离衰减 由以下字段决定: - `RolloffMode` - `MinDistance` - `MaxDistance` ### 11.3 遮挡 如果开启 `Occlusion`: - 按配置时间间隔执行 `Physics.Raycast` - 命中遮挡层后: - 启用 `AudioLowPassFilter` - 设置低通频率 - 按 `OcclusionVolumeMultiplier` 压低音量 ### 11.4 监听器来源 监听器只来自显式注册: - `AudioComponent` 绑定的 `AudioListener` - 或未来业务显式切换注册 ## 12. 抢占策略 当某个 `AudioCategory` 已满: - 先尝试空闲栈取空槽 - 若没有空槽,则从“最老播放堆”中取最早开始播放的槽位进行复用 特点: - 无线性扫描 - 高并发下行为确定 - CPU 成本稳定 ## 13. 与资源模块的关系 音频模块不直接把 `AudioClip` 交给资源模块对象池管理,而是: - 使用 `IResourceService.LoadAssetSyncHandle()` - 使用 `IResourceService.LoadAssetAsyncHandle()` - 自己持有 `AssetHandle` - 在缓存淘汰或销毁时 `Dispose()` 这样做的原因: - 音频需要精细控制引用计数 - 音频有自己的 `pin/LRU/TTL/pending` 语义 ## 14. 常见使用模式 ### 14.1 BGM ```csharp var audio = GameApp.Audio; audio.SetCategoryVolume(AudioType.Music, 0.8f); ulong bgmHandle = audio.Play( AudioType.Music, "Audio/BGM/MainTheme", loop: true, volume: 1f, async: true, cacheClip: true); ``` ### 14.2 UI 点击音效 ```csharp GameApp.Audio.Play( AudioType.UISound, "Audio/UI/Click", loop: false, volume: 1f, async: false, cacheClip: true); ``` ### 14.3 角色语音 ```csharp GameApp.Audio.PlayFollow( AudioType.Voice, "Audio/Voice/Hero/Greeting", heroTransform, Vector3.up * 1.5f, loop: false, volume: 1f, async: true, cacheClip: false); ``` ### 14.4 环境循环声 ```csharp GameApp.Audio.Play3D( AudioType.Ambient, "Audio/Ambient/WaterfallLoop", waterfallPosition, loop: true, volume: 1f, async: true, cacheClip: true); ``` ## 15. 运行时切换监听器 如果项目存在相机切换或监听器切换逻辑,应显式调用: ```csharp IAudioService audio = GameApp.Audio; audio.UnregisterListener(oldListener); audio.RegisterListener(newListener); ``` 建议规则: - 保证同一时刻只有一个主监听器负责注册 - 切换时先注销旧监听器,再注册新监听器 ## 16. 扩展组件 ### 16.1 AudioListenerBinder `AudioListenerBinder` 用于把场景内的 `AudioListener` 显式注册到 `IAudioService`。 适用场景: - 主相机不在 `AudioComponent` 同一节点下 - 运行时相机或监听器会切换 - 不希望 `AudioComponent` 扫描子节点查找监听器 使用方式: 1. 在带有 `AudioListener` 的对象上添加 `AudioListenerBinder`。 2. 确保场景中已经有 `AudioComponent` 初始化音频服务。 3. 对象启用时自动注册,禁用时自动注销。 ### 16.2 AudioEmitter `AudioEmitter` 是场景 3D 声源组件,面向篝火、电台、瀑布、机器噪音等固定或跟随物体的循环环境声。 核心字段: - `Audio Type`:播放分类,环境声通常使用 `Ambient`。 - `Address`:音频资源地址。 - `Play On Enable`:对象启用时播放。 - `Loop`:是否循环。 - `Follow Self`:是否跟随当前物体。 - `Min Distance`:近距离清晰范围。 - `Max Distance`:超过该距离后听不到或接近听不到。 - `Use Trigger Range`:是否进入半径才播放。 - `Trigger Range`:自定义进入播放区域。 - `Trigger Hysteresis`:离开判定缓冲,避免边界频繁启停。 - `Draw Gizmos`:绘制触发范围和衰减范围。 篝火使用方式: 1. 在篝火对象上添加 `AudioEmitter`。 2. `Audio Type` 设为 `Ambient`。 3. `Address` 填篝火循环音效地址。 4. 开启 `Play On Enable`、`Loop`、`Follow Self`。 5. 关闭 `Use Trigger Range`。 6. 设置 `Min Distance` 为清晰听到的距离,例如 `2`。 7. 设置 `Max Distance` 为听不到的距离,例如 `18`。 这样玩家靠近篝火时声音更清晰,远离到最大距离后由 Unity 3D 衰减处理到不可闻。 电台使用方式: 1. 在电台对象上添加 `AudioEmitter`。 2. `Audio Type` 设为 `Ambient` 或 `Voice`。 3. `Address` 填电台循环音效地址。 4. 开启 `Loop`、`Follow Self`、`Use Trigger Range`。 5. 设置 `Trigger Range` 为进入区域,例如 `8`。 6. 设置 `Min Distance` 和 `Max Distance` 控制区域内的清晰范围与衰减。 7. 开启 `Draw Gizmos`,在 Scene 视图查看触发范围和衰减范围。 注意事项: - `AudioEmitter` 不做场景扫描,不读取私有配置,不创建临时对象。 - 进入区域只触发一次;非循环音效播放完后不会在范围内每帧重播,离开再进入才会重新触发。 - `Play3D` 静态声源和 `PlayFollow` 跟随声源都支持单次播放覆盖 `MinDistance`、`MaxDistance`、`RolloffMode`、`SpatialBlend`。 ## 17. 性能建议 ### 17.1 推荐 - 高频短音效使用同步加载 + `cacheClip: true` - 大型语音或低频资源可异步加载 - 常驻 BGM / 高频 UI 音效建议提前 `Preload` - 明确设置分类通道数,避免过小导致过度抢占 - 3D 语音/环境音再开启遮挡,不要给所有分类都开 ### 17.2 不推荐 - 每次播放都 `cacheClip: false` 且地址重复 - 不显式绑定 `AudioListener` - 给 `Music` 设置过高空间混合 - 给大量瞬时音效开启高频遮挡检测 ## 18. 常见问题 ### 18.1 为什么播放返回 0 可能原因: - `AudioService` 尚未初始化 - `AudioType` 越界 - `AudioComponent` 未正确配置 - `AudioListener` 未注册时 3D 音频相关行为不完整 ### 18.2 为什么 `ClearCache()` 后仍有部分资源未释放 因为: - 播放中的 clip 仍有引用 - 正在加载的 clip 仍有 pending request - pinned 资源未调用 `Unload` ### 18.3 为什么跟随音频不会挂到目标节点下 这是刻意设计: - 防止业务节点销毁时把池对象一起销毁 - 防止池生命周期被业务层意外接管 ## 19. 扩展建议 如果后续继续扩展模块,优先遵守以下方向: - 核心接口只保留 `AudioType` 通用访问 - 不再继续膨胀 `IAudioService` - 新分类优先通过: - `AudioType` - `AudioGroupConfigCollection` - `AudioMixer` 暴露参数 - 通用 `Get/SetCategory` API 不推荐继续在核心接口里新增: - `NpcVoiceVolume` - `BattleMusicEnable` - `CutsceneAmbientVolume` 这类业务语义应放在业务 facade 或上层系统里。 ## 20. 自测清单 修改音频模块后建议至少回归以下场景: - 2D 同步播放 - 2D 异步播放 - 3D 定点播放 - Follow 播放 - 目标销毁时的 Follow 回收 - 分类满载时抢占 - `Preload(pin: true)` 与 `Unload` - `ClearCache()` 对 unused 资源的清理 - 遮挡开启/关闭切换 - 监听器切换 ## 21. 总结 当前 `Audio` 模块的核心使用原则可以归纳为: - 入口统一从 `IAudioService` 走 - 分类控制统一用 `AudioType` - 监听器显式注册,不做全场景扫描 - 高频资源使用缓存与预加载 - 3D 音频通过配置资产控制,不在业务层硬编码 - 池对象生命周期永远由音频系统自己掌握 如果严格遵守以上规则,模块可以稳定支撑常规项目中的 BGM、SFX、UI、Voice、Ambient 五类音频需求。