初学者,自用笔记
初始化
//将音频文件存储在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;
}