Unity MyFramework 用法说明(二十二):使用 AudioManager 统一管理背景音乐与 2D/3D 音效

📅 2026/7/31 12:32:46
Unity MyFramework 用法说明(二十二):使用 AudioManager 统一管理背景音乐与 2D/3D 音效
游戏中的按钮点击、角色受击、技能释放和背景音乐都需要加载并播放音频。如果每个业务对象自己创建AudioSource、加载AudioClip和管理销毁容易出现重复加载、音量规则不统一以及大量临时节点。MyFramework 使用AudioManager统一管理音频资源并通过可复用的AudioHelper播放临时音效。项目地址https://github.com/ZHOURUIH/MyFramework一、AudioManager 负责什么AudioManager主要管理音频路径和音效ID的映射 AudioClip 的加载与卸载 背景音乐播放对象 2D和3D音效播放 音乐与音效的全局音量 临时 AudioHelper 的复用 同时播放的音效数量业务层通常不需要自己创建AudioSource只需要调用AT.MUSIC(...); AT.SOUND_2D(...); AT.SOUND_3D(...);二、注册音效音频文件路径相对于Assets/GameResources并且需要带文件后缀。例如public static class SOUND_DEFINE { public const int MAIN_BGM 1; public const int BUTTON_CLICK 2; public const int MONSTER_HIT 3; }注册对应的文件mAudioManager.registeSoundDefine( SOUND_DEFINE.MAIN_BGM, Audio/Music/Main.mp3); mAudioManager.registeSoundDefine( SOUND_DEFINE.BUTTON_CLICK, Audio/UI/ButtonClick.wav); mAudioManager.registeSoundDefine( SOUND_DEFINE.MONSTER_HIT, Audio/Battle/MonsterHit.wav);注册后业务代码只需要使用音效 ID不需要关心具体文件路径。实际项目中通常会把音效配置放进 Excel再统一注册using static FrameBaseHotFix; using static GBR; public class AudioRegister : FrameSystem { public override void init() { base.init(); if (mExcelSound null) { return; } foreach (EDSound data in mExcelSound.queryAll()) { mAudioManager.registeSoundDefine( data.mID, data.mFileName); } } }这样修改音频文件时只需要调整表格配置。三、内部如何保存音频信息AudioManager内部保存了两个主要字典protected Dictionarystring, AudioInfo mAudioList new(); protected Dictionaryint, string mSoundDefineMap new();它们分别负责mAudioList 音频路径 → AudioInfo mSoundDefineMap 音效ID → 音频路径AudioInfo中保存实际资源和加载状态public class AudioInfo : ClassObject { public ResourceRefAudioClip mClip; public AudioClip mRawClip; public string mAudioName; public bool mIsLocal; public LOAD_STATE mState; public AudioClip getClip() { if (mClip ! null) { return mClip.getResource(); } return mRawClip; } }本地音频通过ResourceRefAudioClip持有可以交给ResourceManager正确卸载。通过网络地址加载的音频则直接保存AudioClip销毁时使用 Unity 接口释放。四、播放背景音乐播放默认循环的背景音乐AT.MUSIC(SOUND_DEFINE.MAIN_BGM);指定是否循环AT.MUSIC( SOUND_DEFINE.MAIN_BGM, true);指定本次播放音量AT.MUSIC( SOUND_DEFINE.MAIN_BGM, true, 0.8f);也可以直接通过路径播放AT.MUSIC( Audio/Music/Main.mp3, true, 0.8f);停止当前背景音乐AT.MUSIC();AudioManager内部只维护一个背景音乐播放对象protected AudioHelper mMusicHelper;初始化资源系统后它会创建一个长期存在的AudioHelper并将其设置为 2D 音频mMusicHelper getOneUnusedHelper(-1.0f); mMusicHelper.setSpatialBlend(0.0f);因此同一时间只维护一条全局背景音乐播放通道。五、播放 2D 音效UI 点击、系统提示和奖励弹窗等音效与场景位置无关适合使用 2D 音效AT.SOUND_2D( SOUND_DEFINE.BUTTON_CLICK);设置单次播放音量AT.SOUND_2D( SOUND_DEFINE.BUTTON_CLICK, 0.7f);也可以通过路径播放AT.SOUND_2D( Audio/UI/ButtonClick.wav, 0.7f);例如按钮点击protected void onConfirmClick() { AT.SOUND_2D( SOUND_DEFINE.BUTTON_CLICK); confirmOperation(); }播放时框架会获取一个临时AudioHelper并设置helper.setSpatialBlend(0.0f);所以音效不会受到摄像机和场景距离影响。六、播放 3D 音效攻击、爆炸、怪物受击等声音通常来自场景中的指定位置AT.SOUND_3D( monster.getPosition(), SOUND_DEFINE.MONSTER_HIT);指定音量AT.SOUND_3D( monster.getPosition(), SOUND_DEFINE.MONSTER_HIT, 0.8f);内部会把播放对象移动到指定位置并设置为完全 3DAudioHelper helper mAudioManager.getAudioHelper(0.0f); helper.setSpatialBlend(1.0f); helper.setPosition(pos);最终的衰减范围、空间混合和其他 3D 参数仍然由AudioSource的配置决定。七、AudioHelper 为什么需要复用如果每次播放音效都创建一个 GameObject创建GameObject 添加AudioSource 播放音效 等待结束 销毁GameObject短时间内大量战斗音效会频繁创建和销毁节点。AudioManager使用两个集合管理辅助对象protected SafeHashSetAudioHelper mHelperList new(); protected QueueAudioHelper mUnusedList new();获取播放对象时优先从未使用队列中取出if (mUnusedList.Count 0) { helper mUnusedList.Dequeue(); } else { CLASS(out helper); helper.setName(AudioHelper); helper.init(); }音效播放结束后不销毁对象而是重新放回队列获取 AudioHelper ↓ 设置 AudioSource 并播放 ↓ 音效时间结束 ↓ 关闭 AudioHelper ↓ 放回未使用队列下一次播放音效时继续复用。八、如何判断音效已经结束非循环音效播放后框架会记录剩余时间helper.mRemainTime mAudioManager.getAudioLength(soundName);AudioManager.update()中持续更新using var a new SafeHashSetReaderAudioHelper(mHelperList); foreach (AudioHelper item in a.mReadList) { if (item ! mMusicHelper tickTimerOnce( ref item.mRemainTime, elapsedTime)) { unusedHelper(item); } }倒计时结束后辅助对象自动回到缓存队列。这里使用SafeHashSet是因为遍历播放对象时可能同时有对象被回收。九、限制同时存在的音效数量战斗中可能在一瞬间播放大量音效。可以设置辅助对象数量上限mAudioManager.setMaxAudioCount(32);当播放对象数量达到上限后框架会找到剩余时间最短的音效将其提前回收再播放新的音效。设置为mAudioManager.setMaxAudioCount(0);表示不限制数量。这个上限适合根据项目规模和目标设备进行调整不能设置得过小否则重要音效可能被过早替换。十、设置音乐和音效音量设置全局音效音量mAudioManager.setSoundVolume(0.8f);设置全局背景音乐音量mAudioManager.setMusicVolume(0.5f);这两个值会在后续播放时乘到本次播放音量上最终音量 本次播放音量 × 全局分类音量修改当前正在播放的背景音乐音量可以使用AT.MUSIC_VOLUME(0.5f);平滑淡出AT.MUSIC_VOLUME( 1.0f, 0.0f, 0.5f);平滑淡入AT.MUSIC_VOLUME( 0.0f, 1.0f, 0.5f);它会接入前面介绍的关键帧系统不需要自己在update()中修改AudioSource.volume。十一、预加载和卸载音频注册音频并不会立即加载全部资源。首次播放时AudioManager会自动加载对应的AudioClip。需要提前加载时mAudioManager.loadAudio( SOUND_DEFINE.BUTTON_CLICK);异步加载mAudioManager.loadAudioAsync( SOUND_DEFINE.BUTTON_CLICK, (info) { if (info ! null) { log(音效加载完成); } });预加载全部已注册音频mAudioManager.loadAllAsync();不再使用时卸载mAudioManager.unload( SOUND_DEFINE.MAIN_BGM);或者通过路径卸载mAudioManager.unload( Audio/Music/Main.mp3);卸载只是释放音频资源和重置加载状态注册的 ID 与路径映射仍然保留之后可以再次加载。十二、总结MyFramework 的音频使用流程可以概括为1. 注册音效ID与资源路径 2. 使用 AT.MUSIC 播放背景音乐 3. 使用 AT.SOUND_2D 播放界面音效 4. 使用 AT.SOUND_3D 播放场景音效 5. AudioManager 统一加载和卸载 AudioClip 6. AudioHelper 自动缓存并复用播放节点 7. 通过音量和数量上限控制整体表现业务层不需要反复创建AudioSource也不需要为每个短音效编写销毁计时。AudioManager负责资源和播放对象的生命周期AT提供统一的播放入口让背景音乐、界面音效和场景音效都使用同一套规则。