初学者,自用笔记

单例父类模板

by B站 超级依薇尔

//让单例早于awake被创建并调用
//空对象Manager命名应与脚本相同
public class TheManager<T> : MonoBehaviour where T : MonoBehaviour
{
    // 单例实例的公开访问属性
    public static T Instance
    {
        get
        {
            // 若实例为空,先在场景中查找该类型的组件
            if (_instance == null)
            {
                _instance = Object.FindAnyObjectByType<T>();
            }
          
            // 若场景中未找到实例,尝试创建
            if (_instance == null)
            {
                Debug.Log($"单例模式 {typeof(T).Name} 场景中不存在,尝试创建...");
            }
          
            // 若仍为空,进一步处理
            if (_instance == null)
            {
                // 查找带有 "Manager" 标签的游戏对象(用于挂载新组件)
                GameObject target = GameObject.FindGameObjectWithTag("Manager");
                if (target == null)
                {
                    // 若未找到标签为 "Manager" 的对象,创建一个新的游戏对象
                    target = new GameObject();
                    target.name = $"单例模式管理器 {typeof(T).Name}"; // 命名为具体管理器名称
                }
                // 若成功获取或创建了目标对象,给它添加 T 类型的组件(即单例实例)
                if (target != null)
                {
                    _instance = target.AddComponent<T>();
                }
            }
          
            return _instance;
        }
    
        set
        {
            // 若实例为空,将传入的值赋给实例(初始化)
            if (_instance == null)
            {
                _instance = value;
            }
            else
            {
                // 若已有实例,销毁新传入的重复实例(确保单例唯一性)
                if (value != null)
                {
                    Destroy(value);
                }
            }
        }
    }

    // 静态实例变量(私有,通过 Instance 属性访问)
    private static T _instance;

    // 唤醒时初始化单例
    private void Awake()
    {
        Instance = this as T;
        // 标记该对象在场景切换时不被销毁(确保单例跨场景存在)
        DontDestroyOnLoad(_instance.gameObject);
    }
}
  //用例public class GameManager : Singleton<GameManager>
  //调用为Manager.Instance,子类无需再次初始化

GameManger

// 游戏管理器,继承单例基础类并实现存档接口,负责检查点管理、场景重启等核心游戏逻辑
public class GameManager : TheManager<GameManager>, ISaveManager
{ 
    [SerializeField] private CheckPoint[] checkPoints; // 场景中所有检查点数组

    Player player;

    public Vector2 DiePosition; // 玩家死亡位置

    // 初始化:获取单例并查找所有检查点和玩家
    protected override void Awake()
    {
        base.Awake();
        checkPoints = FindObjectsByType<CheckPoint>(FindObjectsSortMode.None); // 查找场景中所有检查点
        player = PlayerManager.Instance.player;
    }

    private void Start()
    {
    }

    // 重启当前场景(保存数据后重新加载场景)
    public void RestarScene()
    {
        SaveManager.Instance.SaveGame(); // 重启前保存游戏状态
        var scene = SceneManager.GetActiveScene();
        SceneManager.LoadScene(scene.name); // 重新加载当前场景
    }

    // 从存档数据加载检查点状态和玩家位置
    public void LoadDate(GameData _data)
    {
        // 激活存档中已激活的检查点
        foreach (var pair in _data.checkPoints)
        {
            if (pair.Value == true) // 如果检查点在存档中是激活状态
            {
                if (checkPoints == null) Debug.Log("3 bug"); // 调试用:检查点数组为空时提示
                foreach (var v in checkPoints)
                {
                    if (v.CheckPointID == pair.Key) // 匹配检查点ID
                    {
                        v.ActivateCheckPoint(); // 激活检查点
                    }
                }
            }
        }

        // 将玩家位置设置到最近激活的检查点
        foreach (var checkPoint in checkPoints)
        {
            if (checkPoint.CheckPointID == _data.closestCheckPointID) // 匹配最近激活的检查点ID
            {
                player.transform.position = checkPoint.transform.position; // 移动玩家到该检查点
            }
        }
    }

    // 保存检查点状态到存档数据
    public void SaveDate(ref GameData _data)
    {
        // 记录最近激活的检查点ID
        if (FindClosestCheckPoint() != null)
            _data.closestCheckPointID = FindClosestCheckPoint().CheckPointID;
        
        _data.checkPoints.Clear(); // 清空原有检查点数据

        // 保存所有检查点的激活状态
        foreach (var checkPoint in checkPoints)
        {
            _data.checkPoints.Add(checkPoint.CheckPointID, checkPoint.IsActive);
        }
    }

    // 查找距离玩家死亡位置最近的已激活检查点
    private CheckPoint FindClosestCheckPoint()
    {
        float closestDistance = Mathf.Infinity; // 初始化为无限大
        CheckPoint closestPoint = null;

        foreach (var point in checkPoints)
        {
            // 计算死亡位置与检查点的距离
            float distance = Vector3.Distance(DiePosition, point.transform.position);

            // 找到距离最近且已激活的检查点
            if (distance < closestclosestDistance && point.IsActive == true)
            {
                closestDistance = distance;
                closestPoint = point;
            }
        }

        return closestPoint;
    }

    // 暂停/恢复游戏(通过修改时间缩放实现)
    public void Pausegame(bool _pause)
    {
        if (_pause) Time.timeScale = 0f; // 暂停:时间停止
        else Time.timeScale = 1f; // 恢复:时间正常流动
    }
}

PlayerManager

public class PlayerManager : TheManager<PlayerManager>, ISaveManager
{
    public Player player;

    public int SkillPoints; // 玩家当前拥有的技能点

    // 技能点变化时触发的回调(用于UI更新等)
    public System.Action OnSkillPointsChange;


    protected override void Awake()
    {
        base.Awake();
    }

    private void Start()
    {
    }

    private void Update()
    {
    }

    // 检查是否满足消耗技能点的条件(用于解锁技能等操作)
    /// <param name="_num">需要消耗的技能点数量</param>
    /// <param name="_islock">是否处于锁定状态(锁定则无法消耗)</param>
    /// <param name="_can1">额外条件1(如需要解锁的技能树)</param>
    /// <param name="_can2">额外条件2(如需要锁定的技能树)</param>
    /// <returns>满足条件返回true(并扣减技能点),否则返回false</returns>
    public bool HaveEnoughSkillPoints(int _num, bool _islock, bool _can1, bool _can2)
    {
        // 依次检查:技能点不足、状态锁定、条件1不满足、条件2不满足 → 任一不满足则返回false
        if (_num > SkillPoints) return false;
        else if (_islock) return false;
        else if (!_can1) return false;
        else if (!_can2) return false;
        // 所有条件满足:扣减技能点,并触发技能点变化回调
        else
        {
            SkillPoints -= _num;
            OnSkillPointsChange?.Invoke(); // 安全触发回调(避免空引用)
            return true;
        }
    }

    // 从存档数据加载技能点(_data.SkillPoints=-1时不加载,对应新游戏初始状态)
    public void LoadDate(GameData _data)
    {
        if (_data.SkillPoints != -1)
            SkillPoints = _data.SkillPoints;
    }

    // 将当前技能点保存到存档数据中
    public void SaveDate(ref GameData _data)
    {
        _data.SkillPoints = SkillPoints;
    }
}

SkillManager

public class SkillManager : TheManager<SkillManager>
{

    public DashSkill dash;
    public CloneSkill clone;
    public SwordSkill sword;
    public BlackholeSkill blackhole;

    protected override void Awake()
    { 
        base.Awake();
        dash = GetComponent<DashSkill>();
        clone = GetComponent<CloneSkill>();
        sword = GetComponent<SwordSkill>();
        blackhole = GetComponent<BlackholeSkill>();
    }
}

MusicManager模板

将音频文件存储在Resources文件夹中
// 音频管理单例类,继承基础基础管理器(_Manager)并实现存档接口(ISaveManager)
public class MusicManager : _Manager<MusicManager>, ISaveManager
{
    [Header("音频源配置")]
    [SerializeField] private AudioSource backgroundSource; // 背景音音频源(如BGM)
    [SerializeField] private AudioSource sfxSource; // 音效音频源(如敌人受击、子弹声)
    [SerializeField] private AudioSource voiceSource; // 语音音频源(如角色对话)
    [SerializeField] private AudioSource uiSource; // UI音效音频源(如按钮点击)
    [SerializeField] private AudioSource systemSoundSource; // 系统提示音音频源(如成就解锁)

    [Header("音频混合器配置")]
    [SerializeField] private AudioMixer mixer; // 主混音器(统一控制所有音频音量)
    [SerializeField] private AudioMixerGroup MasterGroup; // 主音量混音组
    [SerializeField] private AudioMixerGroup backgroundMixerGroup; // 背景音混音组
    [SerializeField] private AudioMixerGroup sfxMixerGroup; // 音效混音组
    [SerializeField] private AudioMixerGroup voiceMixerGroup; // 语音混音组
    [SerializeField] private AudioMixerGroup uiMixerGroup; // UI音效混音组
    [SerializeField] private AudioMixerGroup systemMixerGroup; // 系统提示音混音组
    [SerializeField] private AudioMixerGroup extraMixerGroup; // 备用混音组(扩展用)

    [Header("音效设置")]
    [SerializeField] private float sfxMinDistance; // 音效最小播放距离(超出则不播放)

    // 音频源字典:通过Key快速查找对应音频源(避免频繁Find)
    private Dictionary<string, AudioSource> audioSources = new Dictionary<string, AudioSource>();
    // 音频剪辑缓存字典:缓存已加载的音频,避免重复Resources.Load(优化性能)
    private Dictionary<string, AudioClip> audioClips = new Dictionary<string, AudioClip>();

    // 需保存的混音组参数名列表:与AudioMixer中暴露的参数名一一对应(用于存档/读档)
    private List<string> saveableGroups = new List<string>()
    {
        "Master",       // 主音量参数名
        "Background",   // 背景音音量参数名
        "SFX",          // 音效音量参数名
        "Voice",        // 语音音量参数名
        "UI",           // UI音效音量参数名
        "System",       // 系统提示音音量参数名
        "Extra"         // 备用音量参数名
    };

    // 需限制频率的音效类型枚举:统一管理高频触发音效(如敌人受击、子弹撞击)
    public enum LimitedSoundType
    {
        EnemyHit,       // 敌人受击音效
        BulletImpact,   // 子弹撞击音效
        UIQuickClick    // 快速点击UI音效(防连续点击刷屏)
    }

    // 音效冷却时间配置表:在Inspector可视化配置每种音效的默认冷却时间
    [Header("Limiter 冷却配置")]
    [SerializeField]
    private Dictionary<LimitedSoundType, float> SoundCDs = new()
    {
        { LimitedSoundType.EnemyHit, 0.05f },    // 敌人受击:0.05秒内仅播放1次
        { LimitedSoundType.BulletImpact, 0.1f }, // 子弹撞击:0.1秒内仅播放1次
        { LimitedSoundType.UIQuickClick, 0.3f }  // 快速点击UI:0.3秒内仅播放1次
    };

    // 音效最后播放时间戳字典:记录每种音效的上次播放时间(用于冷却判断)
    private Dictionary<LimitedSoundType, float> LastTime = new();


    private void Awake()
    {

    }

    private void Start()
    {
        InitializeAudioSources(); // 初始化音频源(绑定混音组、设置基础属性)
        RegisterAudioSources();   // 将音频源注册到字典(便于后续通过Key查找)
    }

    // 初始化音频源:为每个音频源绑定对应混音组,设置默认属性(如循环、是否唤醒播放)
    private void InitializeAudioSources()
    {
        // 初始化背景音:默认循环、不唤醒播放
        if (backgroundSource != null)
        {
            backgroundSource.outputAudioMixerGroup = backgroundMixerGroup;
            backgroundSource.loop = true; // 背景音默认循环
            backgroundSource.playOnAwake = false;
        }

        // 初始化普通音效:绑定音效混音组、设置最小播放距离
        if (sfxSource != null)
        {
            sfxSource.outputAudioMixerGroup = sfxMixerGroup;
            sfxSource.minDistance = sfxMinDistance;
            sfxSource.playOnAwake = false;
        }

        // 初始化语音:绑定语音混音组
        if (voiceSource != null)
        {
            voiceSource.outputAudioMixerGroup = voiceMixerGroup;
            voiceSource.playOnAwake = false;
        }

        // 初始化UI音效:绑定UI混音组
        if (uiSource != null)
        {
            uiSource.outputAudioMixerGroup = uiMixerGroup;
            uiSource.playOnAwake = false;
        }

        // 初始化系统提示音:2D音效(无空间衰减)、默认音量稍高
        if (systemSoundSource != null)
        {
            systemSoundSource.outputAudioMixerGroup = systemMixerGroup;
            systemSoundSource.spatialBlend = 0f; // 2D音效(优先级高)
            systemSoundSource.volume = 1.2f; // 系统提示音默认音量稍高
            systemSoundSource.playOnAwake = false;
        }
    }

    // 将音频源注册到字典:通过字符串Key快速访问(如"backgroundSource"对应背景音)
    private void RegisterAudioSources()
    {
        audioSources.Add("backgroundSource", backgroundSource);
        audioSources.Add("sfxSource", sfxSource);
        audioSources.Add("voiceSource", voiceSource);
        audioSources.Add("uiSource", uiSource);
        audioSources.Add("systemSoundSource", systemSoundSource);
    }


    // 加载音频资源:从Resources文件夹加载指定路径的音频剪辑
    public AudioClip LoadAudio(string path)
    {
        return (AudioClip)Resources.Load(path);
    }

    // 获取音频并缓存:先查缓存,无则加载并缓存(减少IO操作,优化性能)
    public AudioClip GetAudio(string path)
    {
        if (!audioClips.ContainsKey(path))
        {
            audioClips[path] = LoadAudio(path);
        }
        return audioClips[path];
    }

    // 播放音乐(基础版):支持指定音频源、路径、循环、音量和播放位置(带距离判断)
    public void PlayMusic(string _source, string _path, bool _loop = true, float _volume = 1.0f, Transform _position = null)
    {
        // 校验音频源是否存在
        if (!audioSources.TryGetValue(_source, out var source) || source == null)
            return;

        // 获取音频剪辑(自动缓存)
        AudioClip clip = GetAudio(_path);
        if (clip == null)
            return;

        // 设置音频源属性
        source.clip = clip;
        source.volume = _volume;
        source.loop = _loop;

        // 距离判断:超出最小距离则不播放
        if (_position != null && PlayerManager.Instance.player != null)
        {
            if (Vector2.Distance(PlayerManager.Instance.player.transform.position, _position.position) > sfxMinDistance)
                return;
        }

        // 已在播放则不重复播放(如循环BGM)
        if (source.isPlaying) return;

        source.Play();
    }

    // 播放音乐(带频率限制版):在基础版上增加冷却判断(适用于高频触发的循环音效)
    public void PlayMusic(LimitedSoundType _soundType, string _source, string _path, bool _loop = true, float _volume = 1.0f, Transform _position = null)
    {
        // 1. 校验是否配置了该音效的冷却时间
        if (!SoundCDs.TryGetValue(_soundType, out float CD))
        {
            return;
        }

        // 2. 校验音频源和音频资源
        if (!audioSources.TryGetValue(_source, out var source) || source == null)
            return;
        AudioClip clip = GetAudio(_path);
        if (clip == null)
            return;

        // 3. 距离判断(超出范围不播放)
        if (_position != null && PlayerManager.Instance.player != null)
        {
            if (Vector2.Distance(PlayerManager.Instance.player.transform.position, _position.position) > sfxMinDistance)
                return;
        }

        // 4. 冷却时间判断(核心Limiter逻辑)
        float curTime = Time.time;
        // 首次播放:记录时间戳并播放
        if (!LastTime.ContainsKey(_soundType))
        {
            source.clip = clip;
            source.volume = _volume;
            source.loop = _loop;
            source.Play();
            LastTime.Add(_soundType, curTime);
            return;
        }
        // 非首次:超过冷却时间才播放
        if (curTime - LastTime[_soundType] >= CD)
        {
            source.clip = clip;
            source.volume = _volume;
            source.loop = _loop;
            source.Play();
            LastTime[_soundType] = curTime; // 更新时间戳
        }
        // 未冷却:直接过滤(不播放)
    }


    // 播放一次性音效(基础版):使用PlayOneShot播放短音效(如点击、爆炸)
    public void PlayOneShotMusic(string _source, string _path, bool _loop = false, float _volume = 1.0f, Transform _position = null)
    {
        // 校验音频源
        if (!audioSources.TryGetValue(_source, out var source) || source == null)
            return;

        // 获取音频剪辑
        AudioClip clip = GetAudio(_path);
        if (clip == null)
            return;

        // 距离判断(超出范围不播放)
        if (_position != null && PlayerManager.Instance.player != null)
        {
            if (Vector2.Distance(PlayerManager.Instance.player.transform.position, _position.position) > sfxMinDistance)
                return;
        }

        // PlayOneShot不支持循环,忽略_loop参数
        source.PlayOneShot(clip, _volume);
    }

    // 播放一次性音效(带频率限制版):在基础版上增加冷却判断(适用于高频点击等)
    public void PlayOneShotMusic(LimitedSoundType _soundType, string _source, string _path,
                           float _volume = 1.0f, Transform _position = null)
    {
        // 1. 校验冷却配置
        if (!SoundCDs.TryGetValue(_soundType, out float CD))
        {
            return;
        }

        // 2. 校验音频源和资源
        if (!audioSources.TryGetValue(_source, out var source) || source == null)
            return;
        AudioClip clip = GetAudio(_path);
        if (clip == null)
            return;

        // 3. 距离判断
        if (_position != null && PlayerManager.Instance.player != null)
        {
            if (Vector2.Distance(PlayerManager.Instance.player.transform.position, _position.position) > sfxMinDistance)
                return;
        }

        // 4. 冷却时间判断
        float curTime = Time.time;
        // 首次播放:记录时间戳
        if (!LastTime.ContainsKey(_soundType))
        {
            source.PlayOneShot(clip, _volume);
            LastTime.Add(_soundType, curTime);
            return;
        }
        // 超过冷却时间才播放
        if (curTime - LastTime[_soundType] >= CD)
        {
            source.PlayOneShot(clip, _volume);
            LastTime[_soundType] = curTime; // 更新时间戳
        }
        // 未冷却:过滤
    }

    // 停止指定音频源的播放
    public void StopMusic(string _source)
    {
        if (audioSources.TryGetValue(_source, out var source) && source != null)
        {
            source.Stop();
        }
    }

    // 暂停指定音频源
    public void PauseMusic(string _source)
    {
        if (audioSources.TryGetValue(_source, out var source) && source != null)
            source.Pause();
    }

    // 恢复指定音频源的播放
    public void UnpauseMusic(string _source)
    {
        if (audioSources.TryGetValue(_source, out var source) && source != null && source.isPlaying == false)
            source.UnPause();
    }

    // 调整指定音频源的音量
    public void SetMusicVolume(string _source, float _volume)
    {
        if (audioSources.TryGetValue(_source, out var source) && source != null)
        {
            source.volume = _volume;
        }
    }

    // 调整混音组音量(影响该组下所有音频源)
    public void SetGroupVolume(string groupName, float volume)
    {
        // 将0~1的线性音量转换为混音器需要的dB值(AudioMixer使用分贝刻度)
        float dBVolume = Mathf.Log10(Mathf.Clamp(volume, 0.0001f, 1f)) * 20f;
        mixer.SetFloat(groupName, dBVolume);
    }
    
    // 创建新的音频源并注册到字典(用于临时音效或特殊需求)
    public AudioSource CreatSource(string sourceName, AudioMixerGroup group, bool loop = false, float volume = 1.0f, Transform parent = null)
    {
        // 若已存在同名音频源,直接返回
        if (audioSources.ContainsKey(sourceName))
        {
            return audioSources[sourceName];
        }

        // 创建新音频源游戏对象
        GameObject audioObj = new GameObject($"{sourceName}");
        audioObj.transform.SetParent(parent != null ? parent : transform);
        AudioSource source = audioObj.AddComponent<AudioSource>();
        
        // 配置音频源属性
        source.outputAudioMixerGroup = group;
        source.loop = loop;
        source.volume = volume;
        source.playOnAwake = false;
        
        // 注册到字典
        audioSources[sourceName] = source;
        return source;
    }

    // 音频淡入协程:从0音量平滑过渡到目标音量(如场景切换时的BGM淡入)
    public IEnumerator FadeInMusic(string _source, string _path, float targetVolume = 1f, float fadeTime = 1f, bool _loop = true)
    {
        // 校验音频源
        if (!audioSources.TryGetValue(_source, out AudioSource source) || source == null)
        {
            yield break;
        }

        // 获取音频剪辑
        AudioClip clip = GetAudio(_path);
        if (clip == null)
        {
            yield break;
        }

        // 初始化播放状态(从0音量开始)
        source.clip = clip;
        source.loop = _loop;
        source.volume = 0f;
        source.Play();

        // 淡入逻辑:按时间线性插值音量
        float elapsedTime = 0f;
        while (elapsedTime < fadeTime)
        {
            source.volume = Mathf.Lerp(0f, targetVolume, elapsedTime / fadeTime);
            elapsedTime += Time.deltaTime;
            yield return null; // 每帧更新
        }

        // 确保最终音量准确
        source.volume = targetVolume;
    }

    // 音频淡出协程:从当前音量平滑过渡到目标音量(如场景结束时的BGM淡出)
    public IEnumerator FadeOutMusic(string _source, float targetVolume = 0f, float fadeTime = 1f)
    {
        // 校验音频源是否存在且正在播放
        if (!audioSources.TryGetValue(_source, out AudioSource source) || source == null || !source.isPlaying)
        {
            yield break;
        }

        // 记录初始音量,用于插值计算
        float startVolume = source.volume;
        float elapsedTime = 0f;

        // 淡出逻辑:按时间线性插值音量
        while (elapsedTime < fadeTime)
        {
            source.volume = Mathf.Lerp(startVolume, targetVolume, elapsedTime / fadeTime);
            elapsedTime += Time.deltaTime;
            yield return null;
        }

        // 确保最终音量准确,若目标音量为0则停止播放
        source.volume = targetVolume;
        if (targetVolume <= 0f)
        {
            source.Stop();
        }
    }

    // 距离感应淡入淡出协程:根据物体与范围中心的距离自动调整音量(如敌人靠近时音效变大)
    public IEnumerator FadeDistanceMusic(string _source, string _path, Transform _position, float _minDistance, float _maxDistance, bool _loop = false)
    {
        // 校验音频源
        if (!audioSources.TryGetValue(_source, out AudioSource source) || source == null)
        {
            yield break;
        }

        // 获取音频剪辑
        AudioClip clip = GetAudio(_path);
        if (clip == null)
        {
            yield break;
        }

        // 初始化播放状态
        source.clip = clip;
        source.loop = _loop;
        source.volume = 0;
        source.Play();

        // 计算范围中点和插值系数(用于音量计算)
        float _mid = (_minDistance + _maxDistance) / 2;
        float _value = _mid - _minDistance;

        // 实时更新音量:根据位置距离中点的远近来调整
        while (source.isPlaying)
        {
            // 超出范围则停止播放
            if (_position.position.x < _minDistance || _position.position.x > _maxDistance)
            {
                source.Stop();
                yield break;
            }

            // 计算音量:越靠近中点音量越大,越靠近边缘音量越小
            source.volume = Mathf.Lerp(1, 0, Mathf.Abs(_position.position.x - _mid) / _value);

            yield return null; // 每帧更新
        }
    }


    // 从存档加载音量配置
    public void LoadDate(GameData _data)
    {
        if (_data.groupVolumes == null) return;

        // 遍历所有需加载的混音组,应用存档中的音量值
        foreach (var groupName in saveableGroups)
        {
            if (_data.groupVolumes.TryGetValue(groupName, out float savedVolume))
            {
                SetGroupVolume(groupName, savedVolume);
            }
            else
            {
                // 存档中无该组数据时使用默认最大音量
                SetGroupVolume(groupName, 1.0f);
            }
        }
    }

    // 将当前音量配置保存到存档
    public void SaveDate(ref GameData _data)
    {
        _data.groupVolumes.Clear(); // 清空旧数据

        // 遍历所有需保存的混音组,获取当前音量并保存
        foreach (var groupName in saveableGroups)
        {
            if (TryGetGroupVolume(groupName, out float currentVolume))
            {
                _data.groupVolumes[groupName] = currentVolume;
            }
        }
    }

    // 辅助方法:将混音组的dB音量转换为0~1的线性音量(用于存档)
    private bool TryGetGroupVolume(string groupName, out float linearVolume)
    {
        linearVolume = 1.0f; // 默认值
        if (mixer == null) return false;

        // 从混音器获取当前dB值
        if (mixer.GetFloat(groupName, out float dbVolume))
        {
            // 反向转换:dB转线性音量
            linearVolume = Mathf.Pow(10, dbVolume / 20);
            linearVolume = Mathf.Clamp01(linearVolume); // 限制在0~1范围
            return true;
        }
        return false;
    }

    //待学习-音频优先级管理,中小项目可手动管理
    //待学习-source独立对象池,中小项目可将source绑定在对象池的预制体上

}