0x02.单例模式
在 Unity 开发中,单例模式(Singleton) 绝对是你最常遇到的模式之一。它非常适合用来管理全局唯一的状态或系统,例如:GameManager(游戏流程管理)、AudioManager(音效管理)、UIManager(界面管理)等。
在 Unity 中实现单例通常有两种方式:一种是继承自 MonoBehaviour 的单例(可以挂载到游戏物体上),另一种是纯 C# 的单例(不挂载到物体上)。下面我为你详细演示这两种最常用的代码写法。
一、 最常用的:继承自 MonoBehaviour 的单例
这是 Unity 开发者最常用的一种写法。它的好处是你可以把它挂载到场景中的一个 GameObject 上,并且可以在 Inspector 面板中配置参数。
using UnityEngine;
public class GameManager : MonoBehaviour
{
// 1. 定义一个公共的静态属性,允许其他任何类访问它,但只能在内部赋值
public static GameManager Instance { get; private set; }
// 测试用的全局数据
public int playerScore = 0;
private void Awake()
{
// 2. 检查场景中是否已经存在了一个 GameManager 实例
if (Instance != null && Instance != this)
{
// 如果存在且不是当前这个,说明场景里多了一个,必须销毁掉,保证“唯一性”
Destroy(this.gameObject);
return;
}
// 3. 如果没有实例,就把自己赋值给 Instance
Instance = this;
// 4. (可选) 让这个单例在切换场景时(LoadScene)不会被销毁
// 这对于全局管理器来说非常有用
DontDestroyOnLoad(this.gameObject);
}
// 提供给外部调用的测试方法
public void AddScore(int points)
{
playerScore += points;
Debug.Log("当前分数: " + playerScore);
}
}
如何使用它?
在游戏中的任何其他脚本里,你都不需要使用 GameObject.Find() 或 GetComponent() 去找它,直接通过类名调用即可:
public class Player : MonoBehaviour
{
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Coin"))
{
// 直接通过 Instance 访问并调用方法
GameManager.Instance.AddScore(10);
Destroy(other.gameObject);
}
}
}
二、 进阶实用版:泛型单例基类 (Generic Singleton)
当你的游戏变得复杂时,你可能会写 AudioManager、UIManager、InputManager。如果每个脚本都把上面的 Awake 逻辑写一遍,代码会很冗余。
作为 Unity 开发者,我们通常会写一个泛型单例基类,以后任何管理器只要继承它,就自动变成了单例!
using UnityEngine;
// T 代表任何继承自 MonoBehaviour 的类
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
public static T Instance
{
get
{
// 如果实例不存在,尝试在场景中找一下
if (_instance == null)
{
_instance = FindObjectOfType<T>();
// 如果场景中连挂载了该脚本的物体都没有,就自动创建一个并挂载
if (_instance == null)
{
GameObject singletonObject = new GameObject(typeof(T).Name + " (Singleton)");
_instance = singletonObject.AddComponent<T>();
}
}
return _instance;
}
}
protected virtual void Awake()
{
// 确保实例的唯一性
if (_instance != null && _instance != this)
{
Destroy(this.gameObject);
}
else
{
_instance = this as T;
DontDestroyOnLoad(this.gameObject);
}
}
}
如何使用泛型单例?
极其简单!比如你要新建一个音效管理器,只需要让它继承 Singleton<AudioManager> 即可:
using UnityEngine;
// 继承泛型基类,它立刻就拥有了单例的所有特性
public class AudioManager : Singleton<AudioManager>
{
public void PlaySound(string soundName)
{
Debug.Log("播放音效: " + soundName);
}
}
// 在其他地方调用:
// AudioManager.Instance.PlaySound("Explosion");
三、 纯 C# 单例(不继承 MonoBehaviour)
并不是所有数据都需要挂载到 GameObject 上(比如玩家的存档数据、游戏配置数据)。对于这种纯数据类,我们可以使用纯 C# 单例。
public class PlayerDataManager
{
// 1. 静态私有实例
private static PlayerDataManager _instance;
// 2. 将构造函数设为 private,防止外部使用 new 关键字创建新对象
private PlayerDataManager()
{
// 这里可以进行初始化,比如从硬盘读取存档
Health = 100;
}
// 3. 静态公有属性,用于获取实例
public static PlayerDataManager Instance
{
get
{
// 延迟实例化:只有在第一次调用时才创建对象
if (_instance == null)
{
_instance = new PlayerDataManager();
}
return _instance;
}
}
public int Health { get; set; }
}
💡 避坑指南(关于单例的缺点)
单例模式虽然好用,但它是 Unity 新手最容易滥用的模式:
- 不要把什么东西都做成单例。 比如玩家(Player)和敌人(Enemy)就不应该做成单例,因为未来你可能会想做双人模式或者多敌人。
- 隐藏的依赖关系: 过多使用单例会让代码耦合度变高(任何脚本都在随意调用
GameManager.Instance),这会导致后期排查 Bug 和修改代码变得困难。建议只在必须全局共享的核心管理器上使用单例。