Unity Audio 系统

游戏加入声音后,会大大的增强沉浸感,提高游戏体验,unity提供了强大的声音组件来帮助播放音效,即AudioSystem

使用Audio的最简单的例子

  • 将音效文件导入unity工程
  • 在场景中创建一个GameObject
  • 在Project中找到导入的音效文件,直接拖动到上面创建的GameObject上
  • 再创建一个Game Object,添加AudioListener组件来“听”场景中的声音
  • 运行例子,我们将听到音效被播放了

在上面的过程中,我们涉及到了3个概念:

AudioListener

可以认为时场景中的“耳朵”,用来‘听’场景中的声音。该组件必须在场景中唯一,如果有多个,会报大量警告

AudioClip

声音资源,我们导入到工程中的声音资源,加载到Unity中后,将以AudioClip的形式存在,我们也可以在代码中访问它,将AudioClip指定给AudioSource,来创建音源。
在Project选中一个声音文件,将在Inspector面板中看到导入配置:

参数含义
Force To Mono是否将多声道音源混合到MonoTrack单省道
Normalize启用Force To Mono,在混合处理中进行Normalize
Load In Background开启,则执行多线程加载,避免主线程阻塞
Ambisonic三维声音文件,多用于XR项目
Preload Audio Dta是否在加载场景时预加载声音。仅对挂在Unity behaviour上的,场景加载完后需要立即播放的音效起作用。如果不选择该选项,则音效在AudioSource.Play(),AudioSource.PlayOneShot()时,或者通过AudioSource.LoadAudioData()加载(相应通过AudioSource.UnloadAudioData())时加载
Load Type加载方式
- Decompress On Load加载完成后立即解压,播放时会更加流畅,多用于文件比较小的音效,如果对大声音文件选择该选项,会产生较大内存开销
- Compressed In Memory加载完后在内存中以压缩格式保存,在播放时在解压,这种模式会造成一定程度的性能开销,因此仅用于较大尺寸的问价来降低内存开销
- Streaming播放时解码,该模式利用一块较小的内存加载声音文件的一部分,并在播放时解码。解压则是在单独的Streaming线程进行。无论是否有声音播放,该模式都将占用200K内存
Compression Format压缩格式
- PCM声音质量做好,尺寸最大,最好用于比较短的音效,CPU占用最低
- ADPC该格式用于有背景音且需要大量播放的音效,如脚步,枪开火,撞击类声音。该格式比PCM小3.5倍,CPU占用比Vorbis/MP3要小,通常多数音效使用该格式压缩
- Vorbis/MP3压缩比比较高,文件尺寸小,但是声音质量低,多用于较长的音效,以及音乐文件。该格式可以通过调节Quality来在压缩比和质量之间平衡,还可以通过采样率SampleRate来进一步压缩
Quality声音质量,仅对Vorbis/MP3。以多大的压缩比压缩
Sample Rate Setting采样率设置,仅对Vorbis/MP3
- Preserve Sample Rate保留原始采样率
- Optimize Sample Rate根据对音源高频部分的分析,自动进行优化采样率
- Override Sample Rate手动设置采样率

AudioSource

场景中的音源,用来播放AutioClip声音,同时提供参数来告诉unity如何播放AudioClip,主要参数:
AudioClip

参数含义
AudioClip播放的声音
Output声音输出到哪个混音器,如果没有指定,表示直接交给声卡播放
Mute静音该音源
PlayOnAwakeGameObject OnAwake时,自动播放声音
Loop是否循环播放
Volume设置该音源的音量,只影响自己
Pitch声音播放速度/频率
3D声音参数
Doppler Level声音多普勒效应等级
Spread声音传播方向0=360,否则是定向传播
MinDistance最小传播距离,在该距离内,声音不会衰减
MaxDistance最大播放距离,超过该距离,将听不到该音源。如果MinDistance,MaxDistance都为0,将不会听到该音源

以上并没有列出所有参数,只是列出了常用的几个,更加详细的,看官方文档

Audio Mixer

Unity还提供了更加复杂的混音系统,来帮助我们实现更好的声音效果,即AudioMixer。
在这里插入图片描述
Audio Source播放时,可以指定AudioMixer混音器,来进行混音后交给声卡播放,当然也可以不指定,则直接交给声卡播放,但是该情况下,如果播放大量音效声卡会有爆炸杂音。
同时AudioMixer中的Audio MixerGroup允许我们以分组的形式播放和控制声音,如背景音乐组,UI音效组,战斗音效组。典型的,我们可以统一管理组内的音量控制。
还以通过为AudioMixerGroup添加Sender和Receiver效果器,将多个AudioMixerGroup串联起来,构成混音“路径”,来对声音进行一系列的处理,做音效的同学对该概念一定特别熟悉,一般游戏内(相对于音乐游戏),将音效播放分组就够了。
AudioMixer实际上是个资源,我们可以在Project视窗中右键菜单-Create-AudioMixer来创建。
具体的AudioMixer配置,使用,见官方文档。

音效管理器

游戏开发过程中,我们不可能将音效都挂在Game Object上进行播放,更多的情况是根据游戏逻辑的需要,动态地播放音效,所以,我们需要一个音效系统来管理音效的播放,并将Unity的音效复杂性隐藏起来,提供易用接口。

AudioManager : MonoBehavior

AudioManager派生自MonoBehariour,这样我们可以挂在启动场景的对象上进行配置

    /// <summary>
    /// 声音管理器,负责提供音效播放接口,管理声音播放
    /// </summary>
    public class AudioManager : MonoBehaviour
    {
        /// <summary>
        /// 单体实例
        /// </summary>
        public static AudioManager Inst;
        /// <summary>
        /// 使用的混音器
        /// </summary>
        public AudioMixer mAudioMixer;
        /// <summary>
        /// 外部提供的资源加载接口
        /// </summary>
        private Func<string, AudioClip> mAudioClipLoader;
        /// <summary>
        /// 音效组管理器
        /// </summary>
        private AudioMixerGroupManager mGroupMgr = new AudioMixerGroupManager();
        /// <summary>
        /// 音效播放对象池,用于在指定位置播放音效
        /// </summary>
        private GameObjectPool mAudioSourceObjectPool = new GameObjectPool();
        /// <summary>
        /// 下个音效ID
        /// </summary>
        private long mNextAudioID = 1;
        /// <summary>
        /// 音效字典,存放所有正在播放的音效
        /// </summary>
        private Dictionary<long, AudioPlayEntry> mAudioEntryDic = new Dictionary<long, AudioPlayEntry>();
        /// <summary>
        /// 淡入淡出的音效
        /// </summary>
        private List<AudioPlayEntry> mAudioEntryInFading = new List<AudioPlayEntry>();
        /// <summary>
        /// 非循环,等待结束的音效
        /// </summary>
        private List<AudioPlayEntry> mAudioEntryWaitFinish = new List<AudioPlayEntry>();
        /// <summary>
        /// 启动
        /// </summary>
        private void OnStart()
        {
            DontDestroyOnLoad(this);
            Inst = null;
        }
        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="resLoader">外部提供资源加载接口</param>
        public void Init(Func<string, AudioClip> resLoader)
        {
            mAudioClipLoader = resLoader;
            mAudioSourceObjectPool.Init(gameObject);
        }
        /// <summary>
        /// 更新逻辑
        /// </summary>
        private void Update()
        {
            // 更新淡入淡出逻辑
            for(int i = 0; i < mAudioEntryInFading.Count;)
            {
                AudioPlayEntry audioEntry = mAudioEntryInFading[i];
                audioEntry.mFadeTimer += Time.deltaTime;
                float factor = audioEntry.mFadeTimer * audioEntry.mOneFadeTimeDiv;
                if (audioEntry.mFadeState == EAudioFade.In)
                {
                    if (factor >= 1)
                    {
                        mAudioEntryInFading.RemoveAt(i);
                        continue;
                    }
                }
                else
                {
                    factor = 1 - factor;
                    if(factor <= 0)
                    {
                        mAudioEntryInFading.RemoveAt(i);
                        mAudioEntryDic.Remove(audioEntry.mID);
                        DestroyAudioEntry(ref audioEntry);
                        continue;
                    }
                }
                audioEntry.mAudioSource.volume = factor;
                i++;
            }
            // 更新非循环音效,检查结束
            for(int i = 0; i < mAudioEntryWaitFinish.Count;)
            {
                AudioPlayEntry audioEntry = mAudioEntryWaitFinish[i];
                if(audioEntry.mAudioSource.isPlaying == false)
                {
                    mAudioEntryWaitFinish.RemoveAt(i);
                    mAudioEntryDic.Remove(audioEntry.mID);
                    DestroyAudioEntry(ref audioEntry);
                    continue;
                }
                ++i;
            }
        }
        /// <summary>
        /// 播放音效
        /// </summary>
        /// <param name="player">播放音效的Game Object,我们会将AudioSource创建到该对象上,并在播放完成后移除Audio Source</param>
        /// <param name="soundName">音效名字</param>
        /// <param name="mixerGroup">在那个混音组播放</param>
        /// <param name="loop">是否循环</param>
        /// <param name="minDist">3D音效最小距离,-1表示非3D音效</param>
        /// <param name="maxDist">3D音效最大距离</param>
        /// <param name="fadeTime">淡入淡出时间</param>
        /// <returns>音效播放ID,用来控制音效</returns>
        public long PlaySound(GameObject player, string soundName, string mixerGroup, bool loop, float minDist, float maxDist, float fadeTime)
        {
            AudioPlayEntry audioEntry = new AudioPlayEntry()
            {
                mID = mNextAudioID++,
                mSoundName = soundName,
                mMixerGroup = mixerGroup,
                mLoop = loop,
                mMinDist = minDist,
                mMaxDist = maxDist,
                mFadeTime = fadeTime,
                mGameObject = player,
                mIsPoolGameObject = false
            };
            if (PlaySound(ref audioEntry))
                return audioEntry.mID;
            return -1;
        }
        /// <summary>
        /// 播放音效,与上个接口不同在于不提供Game Object,在指定位置播放,所以我们要实现一套GameObject管理器,来动态地创建,以进行Audio Source的播放
        /// </summary>
        /// <param name="player">播放音效的Game Object,我们会将AudioSource创建到该对象上,并在播放完成后移除Audio Source</param>
        /// <param name="soundName">音效名字</param>
        /// <param name="mixerGroup">在那个混音组播放</param>
        /// <param name="loop">是否循环</param>
        /// <param name="minDist">3D音效最小距离,-1表示非3D音效</param>
        /// <param name="maxDist">3D音效最大距离</param>
        /// /// <param name="fadeTime">淡入淡出时间</param>
        /// <returns>音效播放ID,用来控制音效</returns>
        public long PlaySound(Vector3 position, string soundName, string mixerGroup, bool loop, float minDist, float maxDist, float fadeTime)
        {
            AudioPlayEntry audioEntry = new AudioPlayEntry()
            {
                mID = mNextAudioID++,
                mSoundName = soundName,
                mMixerGroup = mixerGroup,
                mLoop = loop,
                mMinDist = minDist,
                mMaxDist = maxDist,
                mFadeTime = fadeTime,
                mGameObject = mAudioSourceObjectPool.Get(),
                mIsPoolGameObject = true
            };
            audioEntry.mGameObject.transform.position = position;
            if (PlaySound(ref audioEntry))
                return audioEntry.mID;
            return -1;
        }
        /// <summary>
        /// 停止播放音效
        /// </summary>
        /// <param name="id">要停止的音效的ID,为创建时的返回ID</param>
        public void StopSound(long id, float fadeTime)
        {
            AudioPlayEntry audioEntry = new AudioPlayEntry();
            if (mAudioEntryDic.TryGetValue(id, out audioEntry) == false)
                return;
            if (audioEntry.mFadeState == EAudioFade.Out)
                return;
            if(audioEntry.mFadeState == EAudioFade.In)
            {
                audioEntry.mFadeTimer = fadeTime * (1 - audioEntry.mFadeTimer * audioEntry.mOneFadeTimeDiv);
            }
            else
            {
                audioEntry.mFadeTimer = 0;
            }
            audioEntry.mOneFadeTimeDiv = 1.0f / fadeTime;
            audioEntry.mFadeState = EAudioFade.Out;
        }
        /// <summary>
        /// 设置指定混音组的音量
        /// </summary>
        /// <param name="mixerGroup"></param>
        /// <param name="volume"></param>
        public void SetVolume(string mixerGroup, float volume)
        {
            mGroupMgr.SetVolume(mixerGroup, volume);
        }
        /// <summary>
        /// 获取指定混音组的音量
        /// </summary>
        /// <param name="mixerGroup"></param>
        /// <returns></returns>
        public float GetVolume(string mixerGroup)
        {
            return mGroupMgr.GetVolume(mixerGroup);
        }
        /// <summary>
        /// 播放声音的接口
        /// </summary>
        /// <param name="audioEntry"></param>
        /// <returns></returns>
        private bool PlaySound(ref AudioPlayEntry audioEntry)
        {
            audioEntry.mAudioSource = audioEntry.mGameObject.AddComponent<AudioSource>();
            // 加载资源
            AudioClip ac = mAudioClipLoader(audioEntry.mSoundName);
            if (ac == null)
                return false;
            // 指定混音组
            audioEntry.mAudioSource.outputAudioMixerGroup = mGroupMgr.GetMixerGroup(audioEntry.mMixerGroup);
            if (audioEntry.mAudioSource.outputAudioMixerGroup == null)
                return false;
            // 设置播放参数
            audioEntry.mAudioSource.clip = ac;
            audioEntry.mAudioSource.loop = audioEntry.mLoop;
            audioEntry.mAudioSource.volume = audioEntry.mFadeTime > 0 ? 0 : 1;
            if (audioEntry.Is3D)
            {
                audioEntry.mAudioSource.spatialBlend = 1;
                audioEntry.mAudioSource.minDistance = audioEntry.mMinDist;
                audioEntry.mAudioSource.maxDistance = audioEntry.mMaxDist;
            }
            else
            {
                audioEntry.mAudioSource.spatialBlend = 0;
            }
            mAudioEntryDic.Add(audioEntry.mID, audioEntry);
            // 混音参数初始化
            if(audioEntry.mFadeTime > 0)
            {
                audioEntry.mFadeState = EAudioFade.In;
                audioEntry.mFadeTimer = 0;
                audioEntry.mOneFadeTimeDiv = 1.0f / audioEntry.mFadeTime;
                mAudioEntryInFading.Add(audioEntry);
            }
            // 非循环,加入检测结束的列表
            if(audioEntry.mLoop == false)
            {
                mAudioEntryWaitFinish.Add(audioEntry);
            }
            return true;
        }
        /// <summary>
        /// 销毁音效
        /// </summary>
        /// <param name="audioEntry"></param>
        private void DestroyAudioEntry(ref AudioPlayEntry audioEntry)
        {
            DestroyImmediate(audioEntry.mAudioSource);
            if (audioEntry.mIsPoolGameObject)
                mAudioSourceObjectPool.Free(audioEntry.mGameObject);
        }
    }

为了便于管理,我们要记录正在播放的音效

    /// <summary>
    /// 记录正在播放的音效
    /// </summary>
    public struct AudioPlayEntry
    {
        /// <summary>
        /// ID
        /// </summary>
        public long mID;
        /// <summary>
        /// 音效名字
        /// </summary>
        public string mSoundName;
        /// <summary>
        /// 在哪个混音组播放
        /// </summary>
        public string mMixerGroup;
        /// <summary>
        /// 是否循环
        /// </summary>
        public bool mLoop;
        /// <summary>
        /// 3D音效衰减最小距离
        /// </summary>
        public float mMinDist;
        /// <summary>
        /// 3D音效最大衰减距离
        /// </summary>
        public float mMaxDist;
        /// <summary>
        /// 淡入淡出时间
        /// </summary>
        public float mFadeTime;
        /// <summary>
        /// 声音播放时挂在哪个对象上
        /// </summary>
        public GameObject mGameObject;
        /// <summary>
        /// 声音播放对象是否时内部对象池分配的
        /// </summary>
        public bool mIsPoolGameObject;
        /// <summary>
        /// 音效播放组件
        /// </summary>
        public AudioSource mAudioSource;
        /// <summary>
        /// 淡入淡出配置
        /// </summary>
        public EAudioFade mFadeState;
        /// <summary>
        /// 1/mFadeTime
        /// </summary>
        public float mOneFadeTimeDiv;
        /// <summary>
        /// 淡入淡出计时器
        /// </summary>
        public float mFadeTimer;
        /// <summary>
        /// 是否时3D音效
        /// </summary>
        public bool Is3D { get { return mMinDist > 0 && mMaxDist > 0; } }
    }

对unity的AudioMixerGroup进行封装,易于使用及方便管理:

    /// <summary>
    /// 混音组
    /// </summary>
    public class AudioMixerGroupEntry
    {
        /// <summary>
        /// 混音器
        /// </summary>
        public AudioMixer mAudioMixer;
        /// <summary>
        /// U3D混音组
        /// </summary>
        public AudioMixerGroup mAudioMixerGroup;
        /// <summary>
        /// 音量参数名字
        /// </summary>
        public string mVolumeName;
        /// <summary>
        /// 当前音量
        /// </summary>
        public float mVolume = -1;
        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="mixer">混音器</param>
        /// <param name="name">混音组名字</param>
        /// <returns></returns>
        public bool Init(AudioMixer mixer, string name)
        {
            mAudioMixer = mixer;
            AudioMixerGroup[] groups = mAudioMixer.FindMatchingGroups(name);
            if (groups == null || groups.Length == 0)
            {
                // Erro log
                return false;
            }
            mAudioMixerGroup = groups[0];
            mVolumeName = "Vol_" + name;
            return true;
        }
        /// <summary>
        /// 设置音量
        /// </summary>
        /// <param name="v"></param>
        public void SetVolume(float v)
        {
            if (mAudioMixerGroup == null || mVolume == v)
                return;
            mVolume = Mathf.Clamp01(v);
            float dbVolume = mVolume > 0 ? AudioMixerGroupManager.DBMin + AudioMixerGroupManager.DBRange * v : -80;
            mAudioMixer.SetFloat(mVolumeName, dbVolume);
        }
        /// <summary>
        /// 获取音量
        /// </summary>
        /// <returns></returns>
        public float GetVolume()
        {
            if (mAudioMixerGroup == null)
                return -1;
            if (mVolume < 0)
            {
                float dbVolume = 0;
                mAudioMixer.GetFloat(mVolumeName, out dbVolume);
                mVolume = (AudioMixerGroupManager.DBMax - dbVolume) / AudioMixerGroupManager.DBRange;
            }
            return mVolume;
        }
    }

管理我们的混音组:

 /// <summary>
    /// 混音组管理器
    /// </summary>
    public class AudioMixerGroupManager
    {
        /// <summary>
        /// 最小分贝,小于该值则按照-80分贝算
        /// </summary>
        public static float DBMin = -30;
        /// <summary>
        /// 最大分贝
        /// </summary>
        public static float DBMax = 20;
        /// <summary>
        /// 分贝范围
        /// </summary>
        public static float DBRange = DBMax - DBMin;
        /// <summary>
        /// 所有的混音组的字典
        /// </summary>
        public Dictionary<string, AudioMixerGroupEntry> mMixerGroups = new Dictionary<string, AudioMixerGroupEntry>();
        /// <summary>
        /// 查找一个混音组
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public AudioMixerGroup GetMixerGroup(string name)
        {
            return GetMixerGroupEntry(name).mAudioMixerGroup;
        }
        /// <summary>
        /// 查找混音组
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public AudioMixerGroupEntry GetMixerGroupEntry(string name)
        {
            AudioMixerGroupEntry en = null;
            if (mMixerGroups.TryGetValue(name, out en) == false)
            {
                en = new AudioMixerGroupEntry();
                en.Init(AudioManager.Inst.mAudioMixer, name);
                mMixerGroups.Add(name, en);
            }
            return en;
        }
        /// <summary>
        /// 设置音量
        /// </summary>
        /// <param name="name"></param>
        /// <param name="volume"></param>
        public void SetVolume(string name, float volume)
        {
            GetMixerGroupEntry(name).SetVolume(volume);
        }
        /// <summary>
        /// 获取音量
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public float GetVolume(string name)
        {
            return GetMixerGroupEntry(name).GetVolume();
        }
    }

音效播放必须要挂在GameObject上,但有时接口的使用者可能只是想在一个指定的位置播放,并没有对象,所以我们内部要提供对象池,来解决这种情况:

    /// <summary>
    /// GameObject 池
    /// </summary>
    public class GameObjectPool
    {
        GameObject mRoot;
        GameObject mEnabled;
        GameObject mDisabled;
        int mTotal;
        Stack<GameObject> mGOStack = new Stack<GameObject>();
        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="root"></param>
        public void Init(GameObject root)
        {
            mRoot = new GameObject("GOPool");
            mRoot.transform.parent = root.transform;
            mEnabled = new GameObject("Enabled");
            mEnabled.transform.parent = mRoot.transform;
            mDisabled = new GameObject("Disabled");
            mDisabled.transform.parent = mRoot.transform;
        }
        /// <summary>
        /// 获取对象
        /// </summary>
        /// <returns></returns>
        public GameObject Get()
        {
            if(mGOStack.Count == 0)
            {
                mTotal++;
                GameObject go = new GameObject("GOPool:" + mTotal.ToString());
                return go;
            }
            return mGOStack.Pop();
        }
        /// <summary>
        /// 归还对象
        /// </summary>
        /// <param name="go"></param>
        public void Free(GameObject go)
        {
            mGOStack.Push(go);
        }
    }

该音效系统没有处理异步加载音效逻辑,各位看官,可以自己尝试解决

  • 6
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值