初学者,自用笔记
单例父类模板
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绑定在对象池的预制体上
}