重构音频模块 1. 高频、大量音频反复调用时,单帧 CPU 开销与 GC 最优 2. AudioClip / AudioSource 的加载、缓存淘汰、卸载形成完整闭环,避免线性遍历 3. AudioSource 对象池 + 播放请求 struct 全部池化覆盖所有分配点 4. 支持3D环境音并具备距离衰减、遮挡等空间属性 5. 新增音频类型(BGM/SFX/Voice/Ambient) 6. 可调式监控Debug信息 及时跟踪音频缓存 处理 句柄状态
17 KiB
Audio 模块使用文档
1. 模块目标
Audio 模块为框架提供统一的音频播放、缓存、回收、3D 空间音频与配置管理能力。
当前实现目标:
- 高频播放路径尽量稳定,避免热路径 GC
AudioClip/AudioSource生命周期可控,避免泄漏- 支持
Music、Sound、UISound、Voice、Ambient - 支持 2D 音频、3D 定点音频、跟随目标音频
- 支持距离衰减、空间混合、遮挡低通
- 配置从组件 Inspector 中抽离为可复用
ScriptableObject
模块核心代码位于:
Runtime/Audio/AudioComponent.csRuntime/Audio/IAudioService.csRuntime/Audio/AudioService.csRuntime/Audio/AudioCategory.csRuntime/Audio/AudioAgent.csRuntime/Audio/AudioGroupConfig.csRuntime/Audio/AudioGroupConfigCollection.cs
2. 架构概览
模块运行时由以下几层组成:
2.1 AudioComponent
AudioComponent 是场景中的挂载入口,负责:
- 注册
AudioService - 绑定
AudioMixer - 绑定
AudioListener - 绑定
AudioGroupConfigCollection - 初始化运行时音频系统
2.2 AudioService
AudioService 是模块核心调度层,负责:
- 播放请求入口
AudioType维度的音量与开关管理AudioClip缓存、引用计数、TTL、LRUAudioSource对象池管理- 播放句柄分配与控制
- 监听器注册
2.3 AudioCategory
每个 AudioType 对应一个 AudioCategory,负责:
- 维护该分类下的固定数量
AudioAgent - 用空闲栈管理可用槽位
- 用最老播放堆管理满载抢占
- 更新该分类下活跃音频
2.4 AudioAgent
每个 AudioAgent 对应一个运行时播放槽位,负责:
- 绑定一个
AudioSource - 管理单条播放状态
- 管理当前
AudioClip引用 - 处理跟随、淡出、遮挡检测
2.5 AudioSourceObject
AudioSourceObject 是 AudioSource 的池化包装对象,接入框架 ObjectPoolService。
2.6 AudioClipCacheEntry
AudioClipCacheEntry 是单个地址的缓存条目,负责:
AssetHandleAudioClip- 引用计数
- LRU 链表节点
- pending 加载请求链
3. 生命周期
3.1 初始化
初始化链路:
- 场景中挂载
AudioComponent Awake时注册AudioServiceStart时读取AudioGroupConfigCollection- 显式注册
AudioListener - 创建每个分类的
AudioCategory - 为每个分类预创建固定数量
AudioAgent - 每个
AudioAgent从ObjectPoolService获取一个AudioSourceObject
3.2 播放
播放链路:
- 业务通过
IAudioService发起播放 AudioService生成池化播放请求- 对应
AudioCategory从空闲栈取槽位,或从最老播放堆抢占 AudioAgent绑定配置- 直接播放
AudioClip或进入AudioClip加载/缓存逻辑 - 播放完成、停止、淡出结束后释放引用
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<AudioListener>(true)。
推荐规则:
- 主场景主相机上的监听器手动拖给
AudioComponent - 运行时如果切换监听器,应显式重新注册
5. 配置资产
5.1 AudioGroupConfigCollection
AudioGroupConfigCollection 是一个 ScriptableObject,保存全部分类配置。
默认通过 CreateAssetMenu 创建:
AlicizaX/Audio/Audio Group Configs
5.2 默认配置
新建资源后默认包含以下五组:
MusicSoundUISoundVoiceAmbient
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 全局开关
IAudioService audio = GameApp.Audio;
audio.Volume = 1f;
audio.Enable = true;
7.2 通用分类音量接口
推荐统一使用以下接口:
audio.SetCategoryVolume(AudioType.Music, 0.8f);
audio.SetCategoryVolume(AudioType.Sound, 1f);
float musicVolume = audio.GetCategoryVolume(AudioType.Music);
7.3 通用分类开关接口
audio.SetCategoryEnable(AudioType.Music, true);
audio.SetCategoryEnable(AudioType.Ambient, false);
bool ambientEnabled = audio.GetCategoryEnable(AudioType.Ambient);
7.4 2D 地址播放
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
ulong handle = audio.Play(AudioType.Music, clip, loop: true, volume: 1f);
7.6 3D 定点播放
Vector3 position = hitPoint;
ulong handle = audio.Play3D(
AudioType.Sound,
"Audio/SFX/Explosion",
position,
loop: false,
volume: 1f,
async: true,
cacheClip: true);
7.7 跟随目标播放
ulong handle = audio.PlayFollow(
AudioType.Voice,
"Audio/Voice/Npc001",
npcTransform,
Vector3.zero,
loop: false,
volume: 1f,
async: true,
cacheClip: true);
注意:
- 跟随播放不会把池化
AudioSource挂到业务节点下 - 实际上是保持在音频根节点下,并同步世界位置/旋转
7.8 停止、暂停、恢复
audio.Stop(handle, fadeout: true);
audio.Pause(handle);
audio.Resume(handle);
7.9 按分类停止
audio.Stop(AudioType.Music, fadeout: true);
audio.StopAll(fadeout: false);
7.10 预加载与卸载
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。
条目包含:
AssetHandleAudioClipRefCountPinnedCacheAfterUseLoadingPendingHead / PendingTailLRU节点
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 距离衰减
由以下字段决定:
RolloffModeMinDistanceMaxDistance
11.3 遮挡
如果开启 Occlusion:
- 按配置时间间隔执行
Physics.Raycast - 命中遮挡层后:
- 启用
AudioLowPassFilter - 设置低通频率
- 按
OcclusionVolumeMultiplier压低音量
- 启用
11.4 监听器来源
监听器只来自显式注册:
AudioComponent绑定的AudioListener- 或未来业务显式切换注册
12. 抢占策略
当某个 AudioCategory 已满:
- 先尝试空闲栈取空槽
- 若没有空槽,则从“最老播放堆”中取最早开始播放的槽位进行复用
特点:
- 无线性扫描
- 高并发下行为确定
- CPU 成本稳定
13. 与资源模块的关系
音频模块不直接把 AudioClip 交给资源模块对象池管理,而是:
- 使用
IResourceService.LoadAssetSyncHandle<T>() - 使用
IResourceService.LoadAssetAsyncHandle<T>() - 自己持有
AssetHandle - 在缓存淘汰或销毁时
Dispose()
这样做的原因:
- 音频需要精细控制引用计数
- 音频有自己的
pin/LRU/TTL/pending语义
14. 常见使用模式
14.1 BGM
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 点击音效
GameApp.Audio.Play(
AudioType.UISound,
"Audio/UI/Click",
loop: false,
volume: 1f,
async: false,
cacheClip: true);
14.3 角色语音
GameApp.Audio.PlayFollow(
AudioType.Voice,
"Audio/Voice/Hero/Greeting",
heroTransform,
Vector3.up * 1.5f,
loop: false,
volume: 1f,
async: true,
cacheClip: false);
14.4 环境循环声
GameApp.Audio.Play3D(
AudioType.Ambient,
"Audio/Ambient/WaterfallLoop",
waterfallPosition,
loop: true,
volume: 1f,
async: true,
cacheClip: true);
15. 运行时切换监听器
如果项目存在相机切换或监听器切换逻辑,应显式调用:
IAudioService audio = GameApp.Audio;
audio.UnregisterListener(oldListener);
audio.RegisterListener(newListener);
建议规则:
- 保证同一时刻只有一个主监听器负责注册
- 切换时先注销旧监听器,再注册新监听器
16. 扩展组件
16.1 AudioListenerBinder
AudioListenerBinder 用于把场景内的 AudioListener 显式注册到 IAudioService。
适用场景:
- 主相机不在
AudioComponent同一节点下 - 运行时相机或监听器会切换
- 不希望
AudioComponent扫描子节点查找监听器
使用方式:
- 在带有
AudioListener的对象上添加AudioListenerBinder。 - 确保场景中已经有
AudioComponent初始化音频服务。 - 对象启用时自动注册,禁用时自动注销。
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:绘制触发范围和衰减范围。
篝火使用方式:
- 在篝火对象上添加
AudioEmitter。 Audio Type设为Ambient。Address填篝火循环音效地址。- 开启
Play On Enable、Loop、Follow Self。 - 关闭
Use Trigger Range。 - 设置
Min Distance为清晰听到的距离,例如2。 - 设置
Max Distance为听不到的距离,例如18。
这样玩家靠近篝火时声音更清晰,远离到最大距离后由 Unity 3D 衰减处理到不可闻。
电台使用方式:
- 在电台对象上添加
AudioEmitter。 Audio Type设为Ambient或Voice。Address填电台循环音效地址。- 开启
Loop、Follow Self、Use Trigger Range。 - 设置
Trigger Range为进入区域,例如8。 - 设置
Min Distance和Max Distance控制区域内的清晰范围与衰减。 - 开启
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 - 新分类优先通过:
AudioTypeAudioGroupConfigCollectionAudioMixer暴露参数- 通用
Get/SetCategoryAPI
不推荐继续在核心接口里新增:
NpcVoiceVolumeBattleMusicEnableCutsceneAmbientVolume
这类业务语义应放在业务 facade 或上层系统里。
20. 自测清单
修改音频模块后建议至少回归以下场景:
- 2D 同步播放
- 2D 异步播放
- 3D 定点播放
- Follow 播放
- 目标销毁时的 Follow 回收
- 分类满载时抢占
Preload(pin: true)与UnloadClearCache()对 unused 资源的清理- 遮挡开启/关闭切换
- 监听器切换
21. 总结
当前 Audio 模块的核心使用原则可以归纳为:
- 入口统一从
IAudioService走 - 分类控制统一用
AudioType - 监听器显式注册,不做全场景扫描
- 高频资源使用缓存与预加载
- 3D 音频通过配置资产控制,不在业务层硬编码
- 池对象生命周期永远由音频系统自己掌握
如果严格遵守以上规则,模块可以稳定支撑常规项目中的 BGM、SFX、UI、Voice、Ambient 五类音频需求。