Files
UnityAI/modules/frameworks/音效系统重构方案.md

22 KiB
Raw Permalink Blame History

音效系统重构方案

目的

使用 Unity 原生 AudioSource + AudioMixer 重写框架音效系统,移除业务层对 SoundManagerPro、AudioSource、AudioClip、SongCallBack 和插件配置概念的依赖,并建立适合微信小游戏的加载、播放、缓存与释放流程。

本文是共享框架的可实施设计。第一版只覆盖 2D 音频;CrystalBattle 中的旧 HFSoundManager 仅作为迁移参考,不作为新接口规范。

当前实施状态(2026-07-21)

第一版已经在 FFCard_WeChat 落地,并于 2026-07-22 同步到 CrystalBattle 参考工程,两边均通过 Unity Editor 编译。已实现统一播放句柄、Music 双源交叉淡化、SFX/UI 共享池、同资源并发限制、并发加载合并、引用计数与 LRU、PlayerPrefs 设置存储、UI 按钮桥接及 HFFunCatalogue.Audio 入口;两个工程均已移除 SoundManagerPro、旧 HFSoundManager 封装和对应 asmdef 依赖,CrystalBattle 原有业务调用也已迁移到新入口。后续根据业务使用反馈继续补充预加载、优先级抢占和更细的诊断接口。

主要路径

类型 路径 说明
旧实现参考 Assets/Game/GameHFScripte/GameFunction/SoundManagerPro/New/ CrystalBattle 中基于 SoundManagerPro 的旧封装
热更框架源码 Assets/StrayFog/Editor/CopyToX/CopyToHF/Audio/ 音效管理器、播放句柄、缓存和资源适配器的真实源码
热更生成结果 Assets/Game/GameHFScripte/CopyToHF/Audio/ 由 CopyToX 生成,禁止直接修改
非热更桥接源码 Assets/StrayFog/Editor/CopyToX/CopyToGeneral/Audio/ UI 音效接口和桥接器的真实源码
非热更生成结果 Assets/Game/GameGeneralScripte/CopyToGeneral/Audio/ 由 CopyToX 生成,禁止直接修改
项目播放入口 Assets/Game/GameHFScripte/GameFunction/GameRoot/HFFunCatalogue_Audio.cs 向业务统一暴露 HFFunCatalogue.Audio
UI 接入 Assets/Game/GameMono/Scripts/UIMono/Abs/AbsSFUI_Button.cs 默认、覆盖、禁用三种按钮点击音模式
混音资源 Assets/Game/GameMono/Resources/Audio/HFDefaultAudioMixer.mixer 首包内的 Master、Music、SFX、UI 四个混音总线
启动接入 Assets/Game/GameHFScripte/GameFunction/GameRoot/HFGameStart.cs Res 表加载完成后显式初始化音效系统

已确认的设计决定

项目 决定
播放后端 Unity 原生 AudioSource + AudioMixer
框架范围 共享框架,具体项目只提供资源和启动参数
资源标识 公开接口直接接收 Res 表中的 int ID
配置来源 不再读取 Sounds.xlsx;Res 表只负责 ID 到资源路径的解析
旧接口兼容 不保留兼容层,业务调用直接迁移到新接口
播放控制 每次播放立即返回统一的 HFAudioHandle
初始化 Res 表可用后,由 HFGameStart 显式初始化
混音 强制使用标准 AudioMixer,不提供代码音量回退
BGM 切换 两个 Music AudioSource 交替执行交叉淡化
SFX 并发 SFX/UI 总数 24,同一 Res ID 默认最多 4 个实例
缓存 引用计数、并发加载合并、LRU 淘汰;默认 32 个、空闲 60 秒
UI 点击音 AbsSFUI_Button 自动播放,支持默认、覆盖和禁用
设置保存 通过 IHFAudioSettingsStore 注入,提供 PlayerPrefs 默认实现
插件退出 新系统验收后立即删除 SoundManagerPro 及 asmdef 引用

源码与生成目录

CopyToHF 和 CopyToGeneral 是生成目录,不是框架源码目录。新音效系统必须遵循现有 CopyToX 规则:

Assets/StrayFog/Editor/CopyToX/CopyToHF/Audio/
  -> 编写 ESF_ / IESF_ 热更框架源码
  -> CopyToX
  -> Assets/Game/GameHFScripte/CopyToHF/Audio/
  -> 生成 HF_ / IHF_ 运行时代码

Assets/StrayFog/Editor/CopyToX/CopyToGeneral/Audio/
  -> 编写 ESF_ / IESF_ 非热更桥接源码
  -> CopyToX
  -> Assets/Game/GameGeneralScripte/CopyToGeneral/Audio/
  -> 生成 HF_ / IHF_ 运行时代码

命名对应关系:

真实源码 生成后的公开类型
ESF_AudioManager HFAudioManager
ESF_AudioHandle HFAudioHandle
IESF_AudioAssetLoader IHFAudioAssetLoader
IESF_UISoundPlayer IHFUISoundPlayer
ESF_UIAudioBridge HFUIAudioBridge

本文后续 API 示例统一使用业务实际调用到的 HF_ 生成名称。实施时必须修改对应 ESF_ 源码并运行 CopyToX,禁止在生成结果中直接实现或修复功能。

边界

本模块负责

  • 根据 Res ID 加载和释放 AudioClip。
  • Music、SFX、UI 的播放、暂停、恢复、停止和音量控制。
  • BGM 循环与交叉淡化。
  • SFX/UI AudioSource 池、并发限制和实例抢占。
  • 播放句柄状态、结果和实例级控制。
  • 并发加载合并、引用计数、预加载和 LRU 缓存。
  • Master、Music、SFX、UI 设置的读取与保存。
  • 应用进入后台及恢复时的声音状态管理。
  • UI 按钮默认点击音桥接。

本模块不负责

  • 不维护独立的声音 XLSX 配置表。
  • 不向业务公开 AudioClip、AudioSource 或 AudioMixerGroup。
  • 第一版不处理 3D 位置、距离衰减、跟随目标和空间混音。
  • 不负责音频资源制作;只规定推荐的 Unity 导入策略。
  • 不直接销毁资源系统加载的 AudioClip。

总体架构

业务 / UI
  -> HFFunCatalogue.Audio
  -> HFAudioManager
       -> HFAudioHandle:一次加载与播放的状态和控制
       -> 内部 Voice:Music 双源、SFX/UI 共享池
       -> 内部 ClipCache:合并加载、引用计数、LRU
       -> HFResAudioAssetLoader
            -> Resource 表:Res ID -> Path
            -> HFABMgr:加载、等待下载、取消、失败、卸载
       -> AudioMixer:Master / Music / SFX / UI
       -> IHFAudioSettingsStore:音量与静音持久化

关键类与接口

类/接口 用途
HFAudioManager 音效系统唯一入口和生命周期管理器
HFAudioHandle 表示一次加载、排队和播放请求
HFAudioManager 内部 ClipCache 管理 Clip 加载合并、引用计数和 LRU
HFAudioManager 内部 Voice 管理 AudioSource 的分配和归还
HFResAudioAssetLoader 使用 Res 表和 HFABMgr 加载/卸载音频资源
IHFAudioSettingsStore 音量和静音设置存储边界
HFPlayerPrefsAudioSettingsStore 默认设置存储实现
IHFUISoundPlayer GameMono 可依赖的 UI 音效最小接口
HFUIAudioBridge UI 按钮与 GameHF 音效实现之间的依赖倒置桥

HFFunCatalogue 音效播放入口

在项目层通过 HFFunCatalogue 的 partial 文件增加唯一业务入口:

public static partial class HFFunCatalogue
{
    public static HFAudioManager Audio => HFAudioManager.Instance;
}

业务代码统一通过该入口调用:

HFAudioHandle music = HFFunCatalogue.Audio.PlayMusic(musicResourceId);
HFAudioHandle sfx = HFFunCatalogue.Audio.PlaySfx(sfxResourceId);
HFAudioHandle ui = HFFunCatalogue.Audio.PlayUi(uiResourceId);

SFX 播放期间需要临时压低 Music 时,将 Duck 参数固定在播放参数中传入:

private static readonly HFAudioPlayOptions BattleSfxOptions = new HFAudioPlayOptions
{
    // SFX 播放期间将 Music 临时压到当前设置音量的 35%。
    musicDuckVolume = 0.35f,
    musicDuckFadeIn = 0.08f,
    musicDuckFadeOut = 0.25f,
};

HFFunCatalogue.Audio.PlaySfx(sfxResourceId, BattleSfxOptions);
  • musicDuckVolume = 1f 表示不压低 Music,0.35f 表示临时保留 35% 音量。
  • musicDuckFadeIn 是压低所需时间,musicDuckFadeOut 是 SFX 结束后的恢复时间,单位均为秒。
  • 同时存在多个 Duck 音效时使用最低音量系数,所有相关音效结束后才恢复 Music。
  • Duck 只作用于当前播放过程,不会修改或持久化玩家的 Music 音量设置。
  • 播放参数会在管理器内部复制,因此可以安全复用同一个固定参数实例。

使用规则:

  • HFFunCatalogue.Audio 是业务层唯一允许使用的音效系统入口。
  • 业务代码禁止直接访问 HFAudioManager.Instance。
  • HFFunCatalogue 只负责暴露管理器,不重复包装 PlayMusic、PlaySfx、PlayUi 等方法,避免形成两套 API。
  • 入口文件属于项目集成层,不能放入 CopyToHF 或 CopyToGeneral 生成目录。
  • 初始化仍由 HFGameStart 在 XLSX 加载完成后调用 HFFunCatalogue.InitializeAudio(...)。

后续完整 API 目标

管理器公开接口:

public AbsHFTask Initialize(
    HFAudioInitializeOptions options,
    IHFAudioSettingsStore settingsStore,
    Action<HFAudioInitializeResult> onComplete = null);

public HFAudioHandle PlayMusic(
    int resourceId,
    HFMusicOptions options = default);

public HFAudioHandle PlaySfx(
    int resourceId,
    HFSfxOptions options = default);

public HFAudioHandle PlayUi(
    int resourceId,
    HFUiAudioOptions options = default);

public AbsHFTask Preload(
    int resourceId,
    Action<HFAudioLoadResult> onComplete = null);

public void ReleasePreload(int resourceId);
public void ClearUnused();

public void PauseMusic();
public void ResumeMusic();
public void StopMusic(float fadeOutSeconds = -1f);
public void StopAll(enHFAudioBus bus = enHFAudioBus.All);

public void SetVolume(enHFAudioBus bus, float normalizedVolume);
public void SetMuted(enHFAudioBus bus, bool muted);

public void Shutdown();

初始化参数

public sealed class HFAudioInitializeOptions
{
    public int AudioMixerResourceId;
    public int DefaultUiClickResourceId;
    public float DefaultMusicCrossFadeSeconds = 0.5f;
    public int MaxSfxVoices = 24;
    public int MaxInstancesPerResource = 4;
    public int MaxUnusedCachedClips = 32;
    public float UnusedClipLifetimeSeconds = 60f;
}
  • AudioMixerResourceId 必须是 Res 表中指向 HFAudioMixer.mixer 的有效 ID。
  • DefaultUiClickResourceId == 0 表示默认按钮点击音关闭。
  • 初始化必须验证 Mixer Group 和公开参数;缺失时整体失败,不静默降级。

播放参数

HFMusicOptions 默认值:

  • Loop = true
  • Volume = 1f
  • FadeInSeconds = 0.5f
  • CrossFadeSeconds = -1f,负数表示使用初始化默认值
  • RestartIfSame = false

HFSfxOptions 默认值:

  • Loop = false
  • Volume = 1f
  • DelaySeconds = 0f
  • Priority = 128
  • MaxInstancesPerResource = -1,负数表示使用全局默认值

HFUiAudioOptions 默认值:

  • Loop = false
  • Volume = 1f
  • DelaySeconds = 0f
  • Priority = 64,优先级高于普通 SFX

所有输入音量必须限制到 0~1,延迟和淡化时间不得小于零,非法参数返回明确失败结果或使用文档规定的默认值。

播放句柄

public enum enHFAudioHandleState
{
    Loading,
    WaitingDownload,
    Playing,
    Paused,
    Completed,
    Stopped,
    Failed,
    Canceled
}

HFAudioHandle 至少公开:

  • long InstanceId
  • int ResourceId
  • enHFAudioBus Bus
  • enHFAudioHandleState State
  • float Progress
  • HFAudioResult Result
  • Stop(float fadeOutSeconds = 0f)
  • Pause()
  • Resume()
  • SetVolume(float normalizedVolume)
  • OnCompleted(Action<HFAudioResult> callback)

规则:

  • Play 立即返回非空句柄;即使未初始化或 ID 非法,也返回已经失败的句柄。
  • 一个句柄只能进入一次终态,完成回调只能执行一次。
  • 加载期间调用 Stop,结果为 Stopped;调用 Cancel,结果为 Canceled。
  • 被并发策略抢占时进入 Stopped,结果原因标记为 VoiceStolen。
  • 不允许通过句柄取得底层 AudioSource 或 AudioClip。

建议结果码:

Success
NotInitialized
InvalidResourceId
ResourceConfigMissing
AssetTypeMismatch
LoadFailed
Canceled
MixerUnavailable
MixerLayoutInvalid
VoiceStolen
Shutdown

AudioMixer 规范

Mixer 必须包含以下 Group:

Master
  Music
  SFX
  UI

必须公开以下参数:

MasterVolumeDb
MusicVolumeDb
SfxVolumeDb
UiVolumeDb

运行时保存 0~1 的线性音量,写入 Mixer 时转换为分贝:

db = normalizedVolume <= 0.0001f
    ? -80f
    : 20f * Mathf.Log10(normalizedVolume);

静音使用 -80 dB。取消静音后恢复该总线最近一次保存的线性音量。

BGM 行为

  • Music 使用两个专用 AudioSource,不占用 SFX/UI 的 24 个实例。
  • 新 BGM 播放时,空闲 Music Source 淡入,旧 Source 同时淡出。
  • 淡化使用 Time.unscaledDeltaTime,不受游戏暂停或 timeScale 影响。
  • 相同 Res ID 且 RestartIfSame == false 时返回当前 Music 句柄,不重新加载或播放。
  • RestartIfSame == true 时按一次新的切换执行。
  • PauseMusic 只暂停当前 Music;ResumeMusic 只恢复由该接口暂停的 Music。
  • StopMusic(-1) 使用初始化默认淡出时间,StopMusic(0) 立即停止。
  • BGM 加载失败时旧 BGM 保持播放,不先停止旧音乐。

SFX/UI 并发与抢占

  • SFX 和 UI 共用最多 24 个 AudioSource。
  • 同一 Res ID 默认最多同时播放 4 个实例。
  • 达到同 ID 上限时,停止该 ID 中最早启动的实例,再复用其 Source。
  • 达到全局上限时:
    1. 优先归还已经完成但尚未清理的 Source。
    2. 在可抢占实例中选择最低优先级。
    3. 同优先级时选择最早启动的实例。
  • 数值越小优先级越高,UI 默认 64,普通 SFX 默认 128。
  • Music Source 永远不参与 SFX/UI 抢占。
  • 循环 SFX 也必须通过句柄停止,不能依赖 Res ID 查找最后一个实例。

加载、缓存与释放

Res ID 加载

HFResAudioClipLoader 的固定流程:

校验 resourceId > 0
  -> 查询 Resource 表
  -> 取得并规范化 Path
  -> HFABMgr.LoadAssetObject<AudioClip>
  -> 返回 AudioClip、底层任务和实际卸载路径

加载器必须传递成功、失败、取消、进度和等待下载状态,不能只处理成功回调。

并发加载合并

  • 同一 Res ID 同时只允许一个底层加载任务。
  • 后续播放或预加载请求加入等待列表。
  • 任一调用方取消时只移除自己的等待句柄;仍有等待者时不得取消共享底层任务。
  • 最后一个等待者取消后,才允许取消底层任务。
  • 成功、失败或取消后必须从“正在加载”字典移除。

引用计数

每个缓存项至少记录:

ResourceId
ResolvedPath
AudioClip
ActiveReferenceCount
PreloadPinCount
LastUsedRealtime
LoadState
  • 活跃播放开始前增加引用,进入终态时减少引用。
  • Preload 增加固定引用;ReleasePreload 减少固定引用。
  • ActiveReferenceCount > 0 或 PreloadPinCount > 0 时禁止淘汰。

LRU 默认策略

  • 无引用 Clip 空闲超过 60 秒后可被淘汰。
  • 无引用缓存超过 32 个时,从最久未使用的条目开始淘汰。
  • 先清理超过空闲时间的条目,再按数量上限清理。
  • 卸载必须调用 HFABMgr.UnloadAssetObject(resolvedPath) 或等价资源接口。
  • 禁止 Destroy(AudioClip)。
  • 底层卸载失败时保留缓存记录并输出一次可定位日志,等待后续重试。

UI 点击音与程序集边界

GameHF 已引用 SFMono。如果 AbsSFUI_Button 直接调用 HFFunCatalogue.Audio,将形成程序集循环依赖。

固定使用依赖倒置:

GameGeneral
  -> 定义 IHFUISoundPlayer、HFUIAudioBridge

SFMono
  -> AbsSFUI_Button 调用 HFUIAudioBridge

GameHF
  -> HFAudioManager 实现 IHFUISoundPlayer
  -> Initialize 时注册
  -> Shutdown 时注销

按钮增加:

public enum enHFUIButtonAudioMode
{
    Default,
    Override,
    Disabled
}

序列化字段:

[SerializeField]
private enHFUIButtonAudioMode mClickAudioMode = enHFUIButtonAudioMode.Default;

[SerializeField]
private int mClickAudioResourceId;

行为:

  • Default:使用初始化参数中的默认 UI 点击音 ID。
  • Override:使用按钮自身的 mClickAudioResourceId。
  • Disabled:不播放。
  • 仅在按钮通过 interactable 判断和现有 0.2 秒点击防抖后播放。
  • 音效未初始化时桥接调用直接忽略,只允许输出一次警告,不能每次点击刷日志。

设置持久化

public interface IHFAudioSettingsStore
{
    bool TryLoad(out HFAudioVolumeSettings settings);
    void Save(HFAudioVolumeSettings settings);
}

HFAudioVolumeSettings 保存 Master、Music、SFX、UI 四组音量和静音值。

默认实现使用独立版本键,例如 hf.audio.settings.v1。项目可替换为微信本地存储、账号设置或云存储实现,音效核心不得直接调用平台 SDK。

生命周期

初始化顺序

HFGameStart.Init
  -> 设置资源模式和语言
  -> 加载 XLSX/Res 表
  -> OnLoadDbFinish
  -> HFFunCatalogue.Audio.Initialize
  -> 初始化成功后进入业务场景

Res 表未加载前无法使用 Res ID,因此 LoadingWindow 在数据库加载完成前默认没有 UI 点击音。如果启动界面未来需要声音,应单独设计首包固定资源通道,不能绕过当前 Res 加载约束。

应用后台

  • 进入后台时记录由系统暂停的播放实例。
  • 返回前台时只恢复这些实例。
  • 原本由业务手动暂停的实例保持暂停。

Shutdown

Shutdown 必须:

  1. 拒绝新的播放请求。
  2. 取消或结束所有等待句柄。
  3. 停止 Music、SFX 和 UI Source。
  4. 注销 HFUIAudioBridge。
  5. 释放预加载固定引用。
  6. 通过资源管理器卸载可释放的 Clip 和 Mixer。
  7. 清空池、缓存、回调和运行状态。

迁移方案

调用替换

// 旧
HFFunCatalogue.SoundPool.PlayBgm(id);

// 新
HFFunCatalogue.Audio.PlayMusic(id);
// 旧
HFFunCatalogue.SoundPool.PlaySFX(id);

// 新
HFFunCatalogue.Audio.PlaySfx(id);
// 旧
HFFunCatalogue.SoundPool.SetPauseBGM(true);
HFFunCatalogue.SoundPool.SetPauseBGM(false);

// 新
HFFunCatalogue.Audio.PauseMusic();
HFFunCatalogue.Audio.ResumeMusic();

旧接口中返回或回调 AudioClip、AudioSource 的能力不迁移。业务如需停止循环声音,必须保存新接口返回的 HFAudioHandle。

实施顺序

  1. 在 Assets/StrayFog/Editor/CopyToX 对应源码目录创建 ESF 核心类型、状态和结果模型,并通过 CopyToX 生成 HF 代码。
  2. 创建标准 AudioMixer 资源并登记 Res ID。
  3. 实现 Res 加载适配器和缓存。
  4. 实现 Music 双源和 SFX/UI Source 池。
  5. 接入设置存储和 UI 桥。
  6. 在项目层增加 HFFunCatalogue.Audio 播放入口。
  7. 在 HFGameStart 中通过该入口完成显式初始化。
  8. 一次性迁移业务调用,不增加旧 API 兼容层。
  9. 完成 Editor、AssetBundle、CDN/等待下载验证。
  10. 删除旧 HFSoundManager、Sounds.xlsx 音效依赖和 SoundManagerPro。
  11. 删除 GameHF 对 SoundManagerProDefine 的 asmdef 引用。

音频资源导入建议

针对微信小游戏:

类型 建议 Load Type 建议用途
BGM Streaming 长音乐,避免整段解压常驻内存
短 UI/SFX Compressed In Memory 控制常驻内存,适合重复播放
极短且高频 SFX 视性能测试选择 Decompress On Load 只用于数量有限、确有解码性能瓶颈的资源

其他建议:

  • 无空间定位需求的音频优先转 Mono。
  • 不建议所有资源统一使用质量 1.0,应按听感和包体测试设定。
  • BGM 不得使用 Decompress On Load 作为默认策略。
  • 平台 Override 必须通过 Unity 构建结果和运行时内存验证,而不是只看源文件大小。

验证

EditMode

  • 非法、缺失 Res ID 返回正确失败结果。
  • 同 ID 并发请求只触发一次底层加载。
  • 成功、失败、取消时所有句柄都进入且只进入一次终态。
  • 单个等待者取消不影响其他等待者。
  • 活跃引用和预加载固定引用阻止错误淘汰。
  • 32 个/60 秒 LRU 按约定顺序释放。
  • 卸载通过资源管理器执行,没有直接销毁 Clip。
  • 音量设置能够保存、恢复并正确转换为分贝。

PlayMode

  • BGM 双源交叉淡化正确,切换失败时旧 BGM 不停止。
  • 相同 BGM 默认不重播,RestartIfSame 可以强制重启。
  • SFX/UI 总 Source 不超过 24,同 ID 不超过 4。
  • 抢占顺序符合优先级和启动时间规则。
  • 循环 SFX 可以通过各自句柄独立停止。
  • UI 默认、覆盖、禁用三种模式正确。
  • 暂停、后台恢复和场景切换不会产生重复管理器或错误恢复。

资源模式

  • Editor、AssetBundle、本地包、CDN 和等待下载路径均能闭环。
  • 下载失败、资源类型错误和取消不会留下永久等待回调。
  • Shutdown 后没有活动 Source、等待任务、UI 桥或缓存引用。

验收标准

  • 工程内不存在业务侧 SoundManagerPro 类型引用。
  • 所有业务播放调用均通过 HFFunCatalogue.Audio,不存在直接访问 HFAudioManager.Instance 的调用点。
  • 音效框架的可维护实现全部位于 Assets/StrayFog/Editor/CopyToX,生成目录没有手工维护代码。
  • GameHF.asmdef 不再引用 SoundManagerProDefine。
  • 不再读取 Sounds.xlsx 播放音频。
  • 所有播放失败都可以通过句柄得到明确结果。
  • Music 固定使用两个 Source,SFX/UI Source 上限固定为 24。
  • 资源只由 HFABMgr 加载和释放。
  • SoundManagerPro 目录可以在不引发编译错误的情况下删除。

当前状态

  • 待验证。
  • 已完成旧系统分析和新系统设计。
  • 尚未实现代码、Mixer 资源、业务迁移和运行时验证。

文档关联

文档 关联原因
模块索引 模块入口和状态
资源管理 AudioClip 的加载、等待下载和卸载边界
数据配置框架 Resource 表及 Res ID 解析
第三方插件 SoundManagerPro 的退出和 asmdef 清理
Hotfix 启动 HFGameStart 初始化时机
架构说明 GameGeneral、SFMono、GameHF 依赖方向