Unity MyFramework 用法说明(二十二):使用 AudioManager 统一管理背景音乐与 2D/3D 音效
2026/7/31 12:32:41 网站建设 项目流程

游戏中的按钮点击、角色受击、技能释放和背景音乐,都需要加载并播放音频。如果每个业务对象自己创建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 Dictionary<string, AudioInfo> mAudioList = new(); protected Dictionary<int, string> mSoundDefineMap = new();

它们分别负责:

mAudioList 音频路径 → AudioInfo mSoundDefineMap 音效ID → 音频路径

AudioInfo中保存实际资源和加载状态:

public class AudioInfo : ClassObject { public ResourceRef<AudioClip> 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; } }

本地音频通过ResourceRef<AudioClip>持有,可以交给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);

内部会把播放对象移动到指定位置,并设置为完全 3D:

AudioHelper helper = mAudioManager.getAudioHelper(0.0f); helper.setSpatialBlend(1.0f); helper.setPosition(pos);

最终的衰减范围、空间混合和其他 3D 参数,仍然由AudioSource的配置决定。

七、AudioHelper 为什么需要复用

如果每次播放音效都创建一个 GameObject:

创建GameObject 添加AudioSource 播放音效 等待结束 销毁GameObject

短时间内大量战斗音效会频繁创建和销毁节点。

AudioManager使用两个集合管理辅助对象:

protected SafeHashSet<AudioHelper> mHelperList = new(); protected Queue<AudioHelper> 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 SafeHashSetReader<AudioHelper>(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提供统一的播放入口,让背景音乐、界面音效和场景音效都使用同一套规则。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询