初学者,自用笔记

初始化

//将音频文件存储在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>();

  
      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();
    }

    // 播放一次性音效(基础版):使用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 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();
        }
    }

    // 距离感应淡入淡出协程,简单x轴判断,可重载重构
    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; // 每帧更新
        }
    }

limiter限制器

淡入淡出协程,可在协程启用处,通过时间戳等进行判断是否执行

    // 需限制频率的音效类型枚举:统一管理高频触发音效(如敌人受击、子弹撞击)
    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();

    // 播放音乐(带频率限制版):在基础版上增加冷却判断(适用于高频触发的循环音效)
    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; // 更新时间戳
        }
        // 未冷却:直接过滤(不播放)
    }

    // 播放一次性音效(带频率限制版):在基础版上增加冷却判断(适用于高频点击等)
    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; // 更新时间戳
        }
        // 未冷却:过滤
    }

音量保存载入

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

    // 从存档加载音量配置
    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;
    }

完整代码