U
11 / 22 · 9 分钟
常见问题

单例:整个游戏都能访问的一个 GameManager

简短回答

单例是什么,GameManager 应该怎么写?

单例就是一个存活的对象,配一个指向它的静态字段,于是任何脚本都能调用 GameManager.Instance 而无需自己持有引用。在 Awake 里给该字段赋值,在 Start 里读取它,销毁重新加载场景时带进来的第二个实例,并且只有当这个 manager 确实必须比场景活得更久时才调用 DontDestroyOnLoad。

分数可能需要从刚死掉的敌人、暂停菜单、存档文件这些地方往上涨,可它们谁都不拥有管分数的那个 manager。常见的答案是 singleton:一个对象、一个指向它的 static 字段,任何脚本都能按名字直接拿到它。

这个模式的全貌

GameManager.cs — a singleton that survives a scene change
using UnityEngine;

public class GameManager : MonoBehaviour
{
    // The one field the rest of the game reaches it through.
    public static GameManager Instance { get; private set; }

    public int Score { get; private set; }

    void Awake()
    {
        // Coming back to a scene that already contains one? This is the spare copy.
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;

        // Only for a manager that must outlive the scene. Leave it out otherwise.
        DontDestroyOnLoad(gameObject);
    }

    void OnDestroy()
    {
        // Do not leave the static field pointing at a destroyed object.
        if (Instance == this) Instance = null;
    }

    public void AddScore(int points) => Score += points;
}

在游戏里的任何其他地方,现在都只剩一行:GameManager.Instance.AddScore(10)。不用往 Inspector 里拖 public 字段,也不用在场景里翻找那个对象。

让它正常工作的四条规则

  • 在 Awake 里给 Instance 赋值,在 Start 里读取它。Unity 会先跑完场景里所有的 Awake,才开始第一个 Start,所以在 Start 里读 manager 的脚本总能找到。两个脚本在 Awake 里互相读对方,是一场赢不了的竞态。

  • 销毁的是重复的那份,不是原本。加载菜单再回来会生成第二个 manager;该走的是后到的那一份,否则所有已经发出去的引用都会指向一个已经死掉的对象。

  • DontDestroyOnLoad 只对根对象有效。挂在子对象上,Unity 会记下警告然后无视它 —— 而 manager 照样跟着场景一起死。

  • 别出于习惯就挂上 DontDestroyOnLoad。如果这个 manager 只属于一个 场景,就让它随那个场景一起销毁:一个活得比它统计的关卡还久的 manager,正是分数乱结转、敌人被数两次的根源。

编辑器里的一个坑

如果你为了更快进入 Play Mode 而关掉 Domain Reload,static 字段会原样保留上次停止时的值 —— Instance 还指着上一次运行时的对象,第一帧就会抛错。显式地重置它,这个快捷方式才能一直安全。

GameManager.cs — two lines that make Play Mode without a domain reload safe
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void ResetStatics() => Instance = null;

什么时候别用它

  • 任何可能同时存在两份的东西 —— 分屏里的玩家、敌人、武器 —— 都不是 singleton,硬把它做成单例,等于提前给自己预约一次重写。

  • 如果只有一个脚本需要这个 manager,在 Inspector 里拖一个 public 字段引用更简单,而且看一眼 Inspector 就知道谁连着谁。

  • manager 之间靠 Instance 互相伸手,到头来既测不了也复用不了。一个项目里有两三个算正常;有十几个,说明游戏已经没什么结构可言了。

做出这个的那一课

界面、面板与 GameManager

这篇指南单独成篇,是一份可以直接照着做的配方。在课程里,同样的东西会作为贯穿全部五个章的那个项目的一部分来搭建 —— 基础,第 10 课。