需求描述
在游戏开发中,音频资源是不可或缺的,通常情况下音频资源随机分布,各个音频的操作和管理都是各自负责,同时对于音频的很多操作逻辑都是大同小异的,这就造成了许多冗余代码的堆叠,除此之外在获取各类音频资源的时候也会造成不必要的开销。所以解决资源分散的问题最直接的方式就是集中管理和分配,通过统一的渠道和特有标识即可获取或操作对应的音频资源。所以本篇文章将围绕这个方案进行尝试。
功能描述
在Unity中我们导入的音频资源都会转换为AudioClip,音频的设置和管理则由AudioSource负责,AudioListener负责监听音频。我们可以在此基础上去封装,从而打造一个音频管理器。
音频管理器负责管理音频信息以及操作音频,比如音频信息的增加和删除,音频的播放和暂停等;
音频信息可以使用一个单独的实体类来记录,用于记录AudioSource组件中的信息,之所以单独用一个实体类来记录音频信息而不直接采用AudioSource,主要是可以通过业务需求去动态调整所要记录的音频信息,我们的实际开发中并非需要AudioSource中所有的信息,有时候仅仅需要其中的一部分,同时音频信息可能涉及存储,直接采用AudioSource可能无法与已经开发好的存储系统相互兼容,而实体类可以为其添加接口或继承来兼容存储系统;
AudioSource组件的管理可以通过一个组件池进行管理,我们知道AudioSource也是等同于一个音频实体类,但是其同时也是一种组件,组件同样作为一种资源,通常情况下相比简单的实体类而言会带来更大的开销,例如一个场景中有十个游戏对象有播放音频的需求,那么按照传统情况就需要每个游戏对象挂载一个AudioSource组件,但是实际运行中十个游戏对象并不一定需要同时播放音频,它们或许都存在各自触发音频播放的条件,我们只需要在游戏对象需要播放音频时为其分配一个AudioSource组件即可,组件池则负责维护AudioSource组件的生产、获取和归还,进而减少资源开销。
本质上,AudioSource组件承担的工作就是记录音频信息和操作音频,我们现在将记录工作分担给音频信息实体类,而操作音频的工作则分担给音频管理器,例如音频管理器的播放依旧是调用AudioSource的播放方法,在播放之前由音频管理器去获取AudioSource组件并且为之配置音频信息,其它的音频操作逻辑同理。
代码展示(C#)
AudioManager.cs
using System.Linq; using System; using System.Collections.Generic; using UnityEngine; namespace Tools.AudioManagerAPI { /// <summary> /// 音频管理器 /// </summary> [DisallowMultipleComponent] public class AudioManager : MonoBehaviour { [Header("必要属性")] [Tooltip("音频总音量,默认为1(值属于[0,1]),该值将影响所有音频的音量,例如该值为0则所有音频音量变为原有音量的0%,若为1则所有音频音量保持不变,即该值将基于所有音频的当前音量进行影响,而不是直接统一所有音频的音量为该值")] [Range(0, 1), SerializeField] private float TotalVolume = 1; /// <summary> /// 音频总音量,默认为1(值属于[0,1]) /// <para>声明:该值将影响所有音频的音量,例如该值为0则所有音频音量变为原有音量的0%,若为1则所有音频音量保持不变,即该值将基于所有音频的当前音量进行影响,而不是直接统一所有音频的音量为该值</para> /// </summary> public float mTotalVolume { get { return TotalVolume; } set { if (value >= 0 && value <= 1 && TotalVolume != value) { TotalVolume = value; mTotalVolumeChangedEvents?.Invoke(value); } } } /// <summary> /// 是否启用音频信息覆盖,默认为true /// </summary> public bool mIsOverWrite { get => isOverWrite; set => isOverWrite = value; } /// <summary> /// 音频管理器中所存储的音频数量 /// </summary> public int mCount { get => audioInfos.Count; } /// <summary> /// 总音量更改事件回调 /// </summary> public event Action<float> mTotalVolumeChangedEvents; /// <summary> /// 音频信息名称合集 /// </summary> public string[] mAudioInfoNames { get => audioInfos.Keys.ToArray(); } private Dictionary<string, AudioInfo> audioInfos;//音频信息集合 private bool isInit;//是否完成了初始化 private bool isOverWrite;//是否启用音频信息覆盖 private static AudioSourcePool audioSourcePool = AudioSourcePool.GetInstance();//AudioSource组件池 /// <summary> /// 播放指定名称的音频 /// <para>p_audioName:音频名称</para> /// </summary> public void Play(string p_audioName) { if (isInit) { if (audioInfos.ContainsKey(p_audioName)) { AudioInfo ai = audioInfos[p_audioName]; AudioSource v_audioSource = audioSourcePool.Get(ai); ai.mAudioSource = v_audioSource; ai.Play(); } } } /// <summary> /// 播放指定名称的音频并开启立体声过渡 /// <para>p_audioName:音频名称</para> /// <para>声明:该方法要求已启用立体声过渡且已设置好立体声过渡的相关属性</para> /// </summary> public void PlayWithStereoTransition(string p_audioName) { if (isInit) { if (audioInfos.ContainsKey(p_audioName)) { AudioInfo ai = audioInfos[p_audioName]; AudioSource v_audioSource = audioSourcePool.Get(ai); ai.mAudioSource = v_audioSource; StartCoroutine(ai.mStereoPanTransitionCoroutine); ai.Play(); } } } /// <summary> /// 播放指定名称的音频并开启立体声过渡 /// <para>p_audioName:音频名称</para> /// <para>p_stereoTransitionValues:立体声过渡值集合</para> /// <para>p_stereoTimeSpan:立体声过渡每帧时间间隔</para> /// </summary> public void PlayWithStereoTransition(string p_audioName, float[] p_stereoTransitionValues, float p_stereoTimeSpan) { if (isInit) { if (audioInfos.ContainsKey(p_audioName)) { AudioInfo ai = audioInfos[p_audioName]; AudioSource v_audioSource = audioSourcePool.Get(ai); ai.mAudioSource = v_audioSource; ai.mStereoTransition = true; ai.mStereoTransitionValues = p_stereoTransitionValues; ai.mStereoTransitionTimeSpan = p_stereoTimeSpan; StartCoroutine(ai.mStereoPanTransitionCoroutine); ai.Play(); } } } /// <summary> /// 暂停播放指定名称的音频 /// <para>p_audioName:音频名称</para> /// </summary> public void Pause(string p_audioName) { if (isInit) { if (audioInfos.ContainsKey(p_audioName)) { AudioInfo ai = audioInfos[p_audioName]; StopCoroutine(ai.mStereoPanTransitionCoroutine); ai.Pause(); audioSourcePool.Return(ai.mAudioSource); } } } /// <summary> /// 添加音频信息 /// <para>p_audioInfo:音频信息</para> /// <para>声明1:若启用了音频信息覆盖,当存在相同名称的音频时,新的音频信息将覆盖旧的音频信息</para> /// <para>声明2:默认启用了音频信息覆盖,可通过mIsOverWrite属性设置禁用</para> /// </summary> public void AddAudioInfo(AudioInfo p_audioInfo) { if (isInit) DoAddAudioInfo(p_audioInfo); } /// <summary> /// 删除音频信息 /// <para>p_audioName:音频名称</para> /// <para>返回值:若删除成功则返回true,否则返回false</para> /// </summary> public bool DeleteAudioInfo(string p_audioName) { if (isInit) return DoDeleteAudioInfo(p_audioName); return false; } private void Awake() { isInit = false; if (InitParameters()) isInit = true; } //添加音频信息的执行逻辑 private void DoAddAudioInfo(AudioInfo p_audioInfo) { if (p_audioInfo != null && !String.IsNullOrEmpty(p_audioInfo.mAudioName)) { string v_audioName = p_audioInfo.mAudioName; p_audioInfo.BindAudioManager(this); if (isOverWrite) audioInfos[v_audioName] = p_audioInfo; else if (!audioInfos.ContainsKey(v_audioName)) audioInfos.Add(v_audioName, p_audioInfo); } } //删除音频信息的执行逻辑 private bool DoDeleteAudioInfo(string p_audioName) { if (!String.IsNullOrEmpty(p_audioName) && audioInfos.ContainsKey(p_audioName)) { audioInfos[p_audioName].BindAudioManager(null); return audioInfos.Remove(p_audioName); } return false; } //对音频管理器相关参数进行初始化 private bool InitParameters() { audioInfos = new Dictionary<string, AudioInfo>(); isOverWrite = true; audioSourcePool.BindAudioManager(this); #if UNITY_EDITOR foreach (AudioInfo ai in AudioInfos) DoAddAudioInfo(ai); #endif return true; } #if UNITY_EDITOR /// <summary> /// 在当前Inspector面板中的AudioInfos中的元素数量 /// </summary> public int mAudioInfoCount { get => AudioInfos?.Length > 0 ? AudioInfos.Length : 0; } [SerializeField] private AudioInfo[] AudioInfos;//存储Inspector面板中 /// <summary> /// 在当前Inspector面板中的AudioInfos中添加一个元素 /// </summary> public void Add() { AudioInfo v_audioInfo = new AudioInfo(); if (AudioInfos?.Length > 0) { AudioInfo[] v_audioInfos = new AudioInfo[AudioInfos.Length + 1]; AudioInfos.CopyTo(v_audioInfos, 0); v_audioInfos[v_audioInfos.Length - 1] = v_audioInfo; AudioInfos = new AudioInfo[v_audioInfos.Length]; v_audioInfos.CopyTo(AudioInfos, 0); } else AudioInfos = new AudioInfo[] { v_audioInfo }; v_audioInfo.ValidateCheck(); } /// <summary> /// 在当前Inspector面板中的AudioInfos中删除一个元素 /// </summary> public void Delete(int p_index) { if (AudioInfos?.Length == 1) AudioInfos = Array.Empty<AudioInfo>(); else if (AudioInfos?.Length > 1) { AudioInfo[] v_audioInfos = new AudioInfo[AudioInfos.Length - 1]; int v_index = 0; for (int i = 0; i < AudioInfos.Length; i++) { if (i != p_index) v_audioInfos[v_index++] = AudioInfos[i]; } AudioInfos = new AudioInfo[v_audioInfos.Length]; v_audioInfos.CopyTo(AudioInfos, 0); } } private void OnValidate() { foreach (AudioInfo audioInfo in AudioInfos) { audioInfo?.ValidateCheck(); } } #endif } }
讯享网
AudioInfo.cs
讯享网using UnityEngine; using System.Collections; using System.Linq; using System; namespace Tools.AudioManagerAPI { /// <summary> /// 音频信息 /// </summary> [System.Serializable] public class AudioInfo { [Header("必要组件")] [Tooltip("AudioClip组件"), SerializeField] private AudioClip TheAudioClip; [Header("必要属性")] [Tooltip("音频名称"), SerializeField] private string AudioName; [Tooltip("音频音量,默认为1(值属于[0,1])"), Range(0, 1), SerializeField] private float Volume = 1; [Tooltip("音频播放速度,默认为1(值属于[-3,3])"), Range(-3, 3), SerializeField] private float Pitch = 1; [Tooltip("立体声位置,默认为0(值属于[-1,1]),若为-1则完全为左声道,若为1则完全为右声道"), Range(-1, 1), SerializeField] private float StereoPan = 0; [Tooltip("音频优先级,默认为128(值属于[0,256])"), Range(0, 256), SerializeField] private int Priority = 128; [Tooltip("是否在场景启动时进行播放,默认为true"), SerializeField] private bool PlayOnAwake = true; [Tooltip("是否循环播放,默认为false"), SerializeField] private bool Loop; [Tooltip("是否忽略总音量影响,默认为false"), SerializeField] private bool IgnoreTotalVolume; [Header("立体声过渡属性")] [Tooltip("是否启用立体声过渡,默认为false"), SerializeField] private bool StereoTransition; [Tooltip("立体声过渡的每帧时间间隔,默认为0.5(值属于[0.1,5])"), Range(.1f, 5), SerializeField] private float StereoTransitionTimeSpan = 0.5f; [Tooltip("立体声过渡值集合"), SerializeField] private float[] StereoTransitionValues; /// <summary> /// 音频名称 /// </summary> public string mAudioName { get => AudioName; set => AudioName = value; } /// <summary> /// 音频音量,默认为1(值属于[0,1]) /// </summary> public float mVolume { get { return Volume; } set { if (value >= 0 && value <= 1) Volume = value; } } /// <summary> /// 音频播放速度,默认为1(值属于[-3,3]) /// </summary> public float mPitch { get { return Pitch; } set { if (value >= -3 && value <= 3) Pitch = value; } } /// <summary> /// 立体声位置,默认为0(值属于[-1,1]),若为-1则完全为左声道,若为1则完全为右声道 /// </summary> public float mStereoPan { get { return StereoPan; } set { if (value >= -1 && value <= 1) StereoPan = value; } } /// <summary> /// 音频优先级,默认为128(值属于[0,256]) /// </summary> public int mPriority { get { return Priority; } set { if (value >= 0 && value <= 256) Priority = value; } } /// <summary> /// 是否在场景启动时进行播放,默认为true /// </summary> public bool mPlayOnAwake { get => PlayOnAwake; set => PlayOnAwake = value; } /// <summary> /// 是否循环播放,默认为false /// </summary> public bool mLoop { get => Loop; set => Loop = value; } /// <summary> /// 是否启用立体声过渡,默认为false /// </summary> public bool mStereoTransition { get => StereoTransition; set => StereoTransition = value; } /// <summary> /// 立体声过渡的每帧时间间隔,默认为0.5(值属于[0.1,5]) /// </summary> public float mStereoTransitionTimeSpan { get { return StereoTransitionTimeSpan; } set { if (value >= 0.1f && value <= 5) StereoTransitionTimeSpan = value; } } /// <summary> /// 立体声过渡值集合 /// </summary> public float[] mStereoTransitionValues { get => StereoTransitionValues; set => StereoTransitionValues = value; } /// <summary> /// AudioSource组件 /// </summary> public AudioSource mAudioSource { get => audioSource; set => audioSource = value; } /// <summary> /// 立体声过渡协程 /// </summary> public IEnumerator mStereoPanTransitionCoroutine { get => stereoPanTransitionCoroutine; } /// <summary> /// 是否忽略总音量影响,默认为false /// </summary> public bool mIgnoreTotalVolume { get => IgnoreTotalVolume; set => IgnoreTotalVolume = value; } private AudioSource audioSource;//AudioSource组件 private AudioManager audioManager;//音频管理器 private bool isInit;//是否完成初始化 private IEnumerator stereoPanTransitionCoroutine;//立体声过渡协程 private float actualVolume;//实际音量 private Action<float> totalVolumeChangedEvent;//总音量更改事件对象 /// <summary> /// 将指定的AudioSource组件信息记录在新的AudioInfo实例中并返回它 /// <para>p_audioSource:指定的AudioSource组件</para> /// <para>返回值:新的AudioInfo实例</para> /// </summary> public static AudioInfo Record(AudioSource p_audioSource) { AudioInfo v_audioInfo = new AudioInfo(); if (p_audioSource != null) { v_audioInfo.TheAudioClip = p_audioSource.clip; v_audioInfo.Volume = p_audioSource.volume; v_audioInfo.Pitch = p_audioSource.pitch; v_audioInfo.StereoPan = p_audioSource.panStereo; v_audioInfo.Priority = p_audioSource.priority; v_audioInfo.PlayOnAwake = p_audioSource.playOnAwake; v_audioInfo.Loop = p_audioSource.loop; v_audioInfo.audioSource = p_audioSource; } return v_audioInfo; } public AudioInfo() { isInit = false; InitToDefault(); } /// <summary> /// 播放音频 /// </summary> public void Play() { if (audioSource != null && !audioSource.isPlaying) { if (!IgnoreTotalVolume) { if (actualVolume < 0 || actualVolume > 1) actualVolume = Volume; audioSource.volume = actualVolume; } else actualVolume = -1; audioSource.Play(); } } /// <summary> /// 暂停音频播放 /// </summary> public void Pause() { if (audioSource != null && audioSource.isPlaying) { audioSource.Pause(); audioSource.volume = Volume; } } /// <summary> /// 绑定音频管理器 /// <para>p_audioManager:音频管理器</para> /// <para>声明1:若有需要可通过该方法将当前的AudioInfo与指定的音频管理器进行绑定</para> /// <para>声明2:绑定后将自动向指定的音频管理器添加当前的AudioInfo</para> /// </summary> public void BindAudioManager(AudioManager p_audioManager) { audioManager = p_audioManager; if (audioManager != null) { audioManager.mTotalVolumeChangedEvents -= totalVolumeChangedEvent; audioManager.mTotalVolumeChangedEvents += totalVolumeChangedEvent; } } /// <summary> /// 初始化为默认值 /// </summary> public void InitToDefault() { if (!isInit) { TheAudioClip = null; AudioName = "Audio"; Volume = 1; Pitch = 1; StereoPan = 0; StereoTransitionValues = null; StereoTransitionTimeSpan = 0.5f; Priority = 128; StereoTransition = false; PlayOnAwake = true; Loop = false; audioSource = null; stereoPanTransitionCoroutine = StereoPanTransition(); totalVolumeChangedEvent = (val) => TotalVolumeChangedEvent(val); actualVolume = -1; } } /// <summary> /// 将当前AudioInfo实例中的信息配置给指定的AudioSource组件 /// <para>p_audioSource:指定的AudioSource组件</para> /// </summary> public void ShareTo(AudioSource p_audioSource) { if (p_audioSource != null) { p_audioSource.clip = TheAudioClip; p_audioSource.volume = Volume; p_audioSource.pitch = Pitch; p_audioSource.panStereo = StereoPan; p_audioSource.priority = Priority; p_audioSource.playOnAwake = PlayOnAwake; p_audioSource.loop = Loop; } } /// <summary> /// 将指定的AudioSource组件信息存储在当前AudioInfo实例中 /// <para>p_audioSource:指定的AudioSource组件</para> /// </summary> public void SelfRecord(AudioSource p_audioSource) { if (p_audioSource != null) { TheAudioClip = p_audioSource.clip; Volume = p_audioSource.volume; Pitch = p_audioSource.pitch; StereoPan = p_audioSource.panStereo; Priority = p_audioSource.priority; PlayOnAwake = p_audioSource.playOnAwake; Loop = p_audioSource.loop; audioSource = p_audioSource; } } // 总音量更改事件 // p_totalVolume:总音量 // 若不忽略总音量影响,通过调用该事件将基于总音量和当前音量换算实际音量数值 // 当TotleVolume为0时,实际音量为0; // 当TotleVolume为1或不属于[0,1)时,实际音量为Volume; // 当TotleVolume属于(0,1)时,实际音量为Volume * TotalVolume private void TotalVolumeChangedEvent(float p_totalVolume) { if (!IgnoreTotalVolume) { if (p_totalVolume == 0) actualVolume = 0; else if (p_totalVolume > 0 && p_totalVolume < 1) actualVolume = Volume * p_totalVolume; else actualVolume = Volume; //运行时修改AudioSource音量 if (audioSource != null) audioSource.volume = actualVolume; } else actualVolume = -1; } //立体声过渡协程 private IEnumerator StereoPanTransition() { int currentIndex = 0; while (true) { if (audioSource == null || !StereoTransition || StereoTransitionValues == null || StereoTransitionValues.Length == 0) yield break; audioSource.panStereo = StereoTransitionValues[currentIndex]; yield return new WaitForSeconds(StereoTransitionTimeSpan); currentIndex = (currentIndex + 1) % StereoTransitionValues.Length; if (currentIndex == 0) StereoTransitionValues = StereoTransitionValues.Reverse().ToArray<float>(); } } #if UNITY_EDITOR [NonSerialized] private bool isAudioClipLog; private bool isAudioNameLog; /// <summary> /// Inspector面板的数据更改检测 /// </summary> public void ValidateCheck() { AudioClipCheck(); AudioNameCheck(); } //AudioClip检测 private void AudioClipCheck() { if (TheAudioClip == null) { if (!isAudioClipLog) { Debug.LogWarning("Component: <b><color=orange>TheAudioClip</color></b> is null."); isAudioClipLog = true; } } else isAudioClipLog = false; } //AudioName检测 private void AudioNameCheck() { if (String.IsNullOrEmpty(AudioName)) { if (!isAudioNameLog) { Debug.LogWarning("Property: <b><color=orange>AudioName</color></b> is empty."); isAudioNameLog = true; } } else isAudioNameLog = false; } #endif } }
AudioSourcePool.cs
using System.Collections.Generic; using UnityEngine; namespace Tools.AudioManagerAPI { /// <summary> /// AudioSource组件池 /// </summary> public class AudioSourcePool { /// <summary> /// 空闲的AudioSource数量 /// </summary> public int mFreeCount { get => audioSources.Count; } private Stack<AudioSource> audioSources;//AudioSource组件集合 private AudioManager audioManager;//AudioManager组件 private AudioInfo defaultAudioInfo;//默认的AudioInfo /// <summary> /// 获取实例(单例模式) /// </summary> public static AudioSourcePool GetInstance() { return Handler.instance; } /// <summary> /// 绑定音频管理器 /// <para>p_audioManager:音频管理器</para> /// </summary> public void BindAudioManager(AudioManager p_audioManager) { if (p_audioManager != null) audioManager = p_audioManager; } /// <summary> /// 获取AudioSource组件 /// <para>返回值:AudioSource组件</para> /// </summary> public AudioSource Get() { return DoGet(); } /// <summary> /// 获取AudioSource组件并按照指定的AudioInfo为之配置属性 /// <para>p_audioInfo:指定的AudioInfo</para> /// <para>返回值:AudioSource组件</para> /// </summary> public AudioSource Get(AudioInfo p_audioInfo) { AudioSource v_audioSource = DoGet(); p_audioInfo?.ShareTo(v_audioSource); return v_audioSource; } /// <summary> /// 归还指定的AudioSource组件 /// <para>p_audioSource:指定的AudioSource组件</para> /// </summary> public void Return(AudioSource p_audioSource) { if (p_audioSource != null) { CleanAudioSource(p_audioSource); audioSources.Push(p_audioSource); } } class Handler { public static AudioSourcePool instance = new AudioSourcePool(); } private AudioSourcePool() { audioSources = new Stack<AudioSource>(); defaultAudioInfo = new AudioInfo(); } //获取AudioSource组件的执行逻辑 private AudioSource DoGet() { if (audioManager == null) return null; AudioSource v_audioSource = null; while (v_audioSource == null) { if (audioSources.Count == 0) GenerateAudioSource(); v_audioSource = audioSources.Pop(); } return v_audioSource; } //生成AudioSource组件 private void GenerateAudioSource() { if (audioManager?.gameObject != null) { AudioSource v_audioSource = audioManager.gameObject.AddComponent<AudioSource>(); audioSources.Push(v_audioSource); } } //清洗AudioSource组件 private void CleanAudioSource(AudioSource p_audioSource) { defaultAudioInfo.ShareTo(p_audioSource); } } }
NumberRange.cs
讯享网using System.Collections.Generic; using System.Linq; namespace Tools.AudioManagerAPI { /// <summary> /// 数值范围数组工具类 /// </summary> public static class NumberRange { /// <summary> /// 获取指定范围内指定步长的Float数值数组 /// <para>p_start:起始值</para> /// <para>p_end:终点值</para> /// <para>p_step:步长值</para> /// <para>[ContainsEnd]:是否包括终点值,默认为false</para> /// <para>返回值:Float[]</para> /// </summary> public static float[] FloatRange(float p_start, float p_end, float p_step, bool ContainsEnd = false) { if (!ContainsEnd) return DoFloatRange(p_start, p_end, p_step).ToArray(); else { List<float> result = DoFloatRange(p_start, p_end, p_step).ToList(); result.Add(p_end); return result.ToArray(); } } //获取指定范围内指定步长的Float数值数组的执行逻辑 static IEnumerable<float> DoFloatRange(float p_start, float p_end, float p_step) { for (float i = p_start; i <= p_end; i += p_step) { yield return i; } } } }
界面展示

演示效果
自定义Unity组件AudioManager

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/40438.html