自定义Unity组件——AudioManager(音频管理器)

news2024/11/30 2:27:03

需求描述

        在游戏开发中,音频资源是不可或缺的,通常情况下音频资源随机分布,各个音频的操作和管理都是各自负责,同时对于音频的很多操作逻辑都是大同小异的,这就造成了许多冗余代码的堆叠,除此之外在获取各类音频资源的时候也会造成不必要的开销。所以解决资源分散的问题最直接的方式就是集中管理和分配,通过统一的渠道和特有标识即可获取或操作对应的音频资源。所以本篇文章将围绕这个方案进行尝试。

功能描述

        在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

资源下载

GitHub_AudioManager    百度网盘

如果这篇文章对你有帮助,请给作者点个赞吧! 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1051983.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESP32S3的MPU-6050组件移植教程

前言 &#xff08;1&#xff09;实习公司要搞ESP32BOX的驱动移植&#xff0c;所有资料自己找还是比较折磨人的现在我分享几个官方的组件移植资料&#xff1a; <1>Find the most exciting ESP-IDF components&#xff08;ESP32的官方组件都可以在里面查&#xff0c;按照他…

【李沐深度学习笔记】损失函数

课程地址和说明 损失函数p2 本系列文章是我学习李沐老师深度学习系列课程的学习笔记&#xff0c;可能会对李沐老师上课没讲到的进行补充。 损失函数 损失函数是用来衡量预测值 y ^ \hat{y} y^​或 y ′ y y′与真实值 y y y的差别&#xff0c;下面给出常见的损失函数类型&am…

9.30号作业

1.消息队列实现进程间的通信 服务端 #include <myhead.h>#define ERR_MSG(msg) do{\fprintf(stderr,"__%d__:",__LINE__);\perror(msg);\ }while(0)typedef struct{ long msgtype; //消息类型char data[1024]; //消息正文 }Msg;#define SIZE sizeof(Msg)-s…

网站被上传webshell

1。原因 2.工具使用 3.步骤

1038 统计同成绩学生

输入样例&#xff1a; 10 60 75 90 55 75 99 82 90 75 50 3 75 90 88 输出样例&#xff1a; 3 2 0 solution #include <stdio.h> int main(){int n, d, k, hash[101] {0}, a[100000];scanf("%d", &n);for(int i 0; i < n; i){scanf("%d&quo…

SpringCloud网关服务

为什么需要网关 官网&#xff1a; https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.1.3.RELEASE/single/spring-cloud-gateway.html 使用 导入依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>s…

第7讲:v-bind属性绑定,v-model双向绑定,v-on事件监听使用

目录 1.v-bind双向绑定 2.v-model双向绑定 3.v-on事件绑定 一.v-bind双向绑定 1.v-bind 指令可以在其名称后面带一个参数&#xff0c;中间放一个冒号隔开&#xff0c;这个参数通常是HTML元素的特性&#xff08;attribute&#xff09;&#xff0c;Vue官方提供了一个简写方式…

阿里云ACP知识点(三)

1、弹性伸缩不仅提供了在业务需求高峰或低谷时自动调节ECS实例数量的能力&#xff0c;而且提供了ECS实例上自动部署应用的能力。弹性伸缩的伸缩配置支持多种特性&#xff0c;例如______,帮助您高效、灵活地自定义ECS实例配置&#xff0c;满足业务需求。 标签、密钥对、 实例RAM…

侯捷 C++ STL标准库和泛型编程 —— 3 容器(关联式容器)

3.3 关联式容器 3.3.0 RB-Tree 红黑树&#xff08;Red-Black Tree&#xff09;是一种自平衡的二叉搜索树 BST&#xff08;AVL 是另一种&#xff09; rb-tree 提供遍历操作和 iterators&#xff0c;按中序遍历遍历&#xff0c;便可以得到排序状态 不能用 iterator 去改变元素的…

CCC标准——PHY

1.介绍 UWB物理层使用基于频带受限脉冲的脉冲无线电信号波形。UWB物理层主要用于测距&#xff0c;但也可以用于数据通信。在CCC标准中&#xff0c;物理层的具体定义依然基于IEEE 802.15.4z标准中的HRP UWB PHY&#xff0c;支持更高的脉冲重复频率。 对于增强测距设备&#xf…

华为云云耀云服务器L实例评测|云耀云服务器L实例搭建个人镜像站

华为云云耀云服务器L实例评测&#xff5c;云耀云服务器L实例搭建个人镜像站 一、云耀云服务器L实例介绍1.1 云耀云服务器L实例简介1.2 云耀云服务器L实例特点 二、Apache介绍2.1 Apache简介2.2 Apache特点 三、本次实践介绍3.1 本次实践简介3.2 本次环境规划 四、远程登录华为云…

Springboot对MVC、tomcat扩展配置

Springboot在web层的开发基本都是采用Springmvc框架技术&#xff0c;但是Springmvc中的某些配置在boot是没有的&#xff0c;我们就应该根据自己的需求进行对mvc扩展配置 Springboot1.x版本如何配置 通过注解Configuration一个类&#xff0c;继承webmvcconfigureradapter&#…

AutoAnimate - 无需任何配置,一行代码自动为元素添加优雅的过渡动画,可以搭配 Vue / React 和 Sevelt 使用

这个动画库只要一行代码就可以自动在我们的组件中添过渡动画&#xff0c;为什么这么省事高效呢&#xff1f; AutoAnimate 是一个无需任何配置&#xff0c;自动为我们开发的 Web 项目添加平滑过渡动画的 JavaScript 工具库。AutoAnimate 和之前推荐的一些 js 动画库相比&#x…

贪婪的互联网电视让用户忍无可忍,广电总局出手了

广电总局要求电视需要在今年底前实现开机就看电视&#xff0c;开机广告、关机广告将不再被允许&#xff0c;这对于饱受互联网电视无孔不入的广告困扰的消费者来说无疑是一大利好&#xff0c;他们早已无法忍受越来越多的广告。 一、贪婪的互联网电视 互联网电视企业曾以羊毛出在…

Java下的序列化和反序列化(写出和读入)

代码如下&#xff1a; public class MyWork {public static void main(String[] args) throws IOException, ClassNotFoundException {//序列化File f new File("testFile/testObject.txt");ObjectOutputStream oos new ObjectOutputStream(new FileOutputStream(…

【聊天系统的优化】RPC方式的优化

RPC方式的优化 聊天系统的中RPC的选择Jsonprotobufmsgpack 聊天系统的中RPC的选择 在RPC方式中&#xff0c;常用的三种方式&#xff1a;Json&#xff0c;protobuf&#xff0c;Msgback 设定一个简单的加和服务&#xff0c;客户端发送一个list给服务端&#xff0c;需要将list的…

<Xcode> Xcode IOS无开发者账号打包和分发

关于flutter我们前边聊到的初入门、数据解析、适配、安卓打包、ios端的开发和黑苹果环境部署&#xff0c;但是对于苹果的打包和分发&#xff0c;我只是给大家了一个链接&#xff0c;作为一个顶级好男人&#xff0c;我认为这样是对大家的不负责任&#xff0c;那么这篇就主要是针…

Centos 7分区失败,进入 dracut 页面,恢复操作

1. 问题场景&#xff1a; 分区失败&#xff0c;重启了虚拟机&#xff0c;导致系统进入 dracut 页面。开机显示 直接回车&#xff0c;等待重启失败的页面 自动进入了 dracut 模式(救援)。 2. 临时解决进入系统 查了一下&#xff1a;如果出现 “dracut” 提示、进入 dracut…

【分布式计算】三、虚拟化 Virtualization

1.什么是虚拟化 1.1.非虚拟化 我们首先来认识什么是非虚拟化   1.一台机器、一个操作系统、几个应用程序   2.应用程序可能会相互影响。   3.机器利用率较低&#xff0c;正常情况下低于25%。 关于X86平台&#xff1a; 1.服务器基础设施利用率低&#xff08;10-18%&#…

【网络协议】IP

当连接多个异构的局域网形成强烈需求时&#xff0c;用户不满足于仅在一个局域网内进行通信&#xff0c;他们希望通过更高一层协议最终实现异构网络之间的连接。既然需要通过更高一层的协议将多个局域网进行互联&#xff0c;那么这个协议就必须为不同的局域网环境定义统一的寻址…