在 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)

当你的游戏变得复杂时,你可能会写 AudioManagerUIManagerInputManager。如果每个脚本都把上面的 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 新手最容易滥用的模式:

  1. 不要把什么东西都做成单例。 比如玩家(Player)和敌人(Enemy)就不应该做成单例,因为未来你可能会想做双人模式或者多敌人。
  2. 隐藏的依赖关系: 过多使用单例会让代码耦合度变高(任何脚本都在随意调用 GameManager.Instance),这会导致后期排查 Bug 和修改代码变得困难。建议只在必须全局共享的核心管理器上使用单例。