彻底搞懂Unity单例

  在游戏开发过程中,单例是一种非常常见的设计模式,它的目标是确保一个类在整个游戏运行期间只有唯一的一个实例,并且提供一个全局访问点来让其它脚本获取这个实例,你可以把它理解成现实世界中的太阳或月亮,无论在什么地方看,都是同一个天体。
  问题来了,为什么游戏开发中需要单例?
  在实际的游戏项目中,很多管理器天生就应该只有一个。比如管理游戏全局的GameManager,负责所有音频的AudioManager,控制界面显示的UIManager,还有处理数据保存和加载的DataManager。如果这些类被创建出多个实例,就会出现状态不一致的问题,比如一个地方的GameManager记录的金币数是100,另一个地方的GameManager记录的金币数是50。
补充一个常见的理解误区,单例不等于防止场景切换时被销毁,单例不等于DontDestroyOnLoad,它们两个是独立的概念,只是在开发中经常搭配使用。
  为了让你更好地理解单例在实际项目中的用法,我做一个完整的管理器案例。这个案例包含两个场景,开始场景和游戏场景。开始场景中有一个Text组件用来显示总金币数,一个按钮用来进入游戏场景。游戏场景中有五个可以点击的Box,每点击一个Box就会销毁Box并且让金币数量加一,右上角还有一个Text组件显示当前局获得的金币数。当五个Box全部被点击之后,会弹出一个退出按钮,点击这个按钮就会回到开始场景,并且把这一局获得的金币累加到总金币上。
  我先给出管理器的代码(GameManager.cs)。

using UnityEngine;

public class GameManager : MonoBehaviour
{
    // 单例实例
    public static GameManager Instance { get; private set; }

    // 数据
    private int totalGold = 0;           // 总金币(跨场景永久累加)
    private int currentSessionGold = 0;  // 当前局获得金币(每局重置)

    private void Awake()
    {
        // 单例逻辑 + 跨场景持久化
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
            Debug.Log("GameManager 已创建,将跨场景存在");
        }
        else
        {
            Destroy(gameObject);
        }
    }

    /// <summary>
    /// 增加金币(游戏中点击 Box 时调用)
    /// </summary>
    public void AddGold(int amount)
    {
        currentSessionGold += amount;
        Debug.Log($"获得 {amount} 金币,当前局累计:{currentSessionGold}");
    }

    /// <summary>
    /// 结束当前局,把本局金币加到总金币,并重置本局计数
    /// 点击退出按钮时调用
    /// </summary>
    public void EndSession()
    {
        totalGold += currentSessionGold;
        Debug.Log($"本局获得 {currentSessionGold} 金币,总金币变为:{totalGold}");
        currentSessionGold = 0;
    }

    /// <summary>
    /// 获取总金币(开始场景显示用)
    /// </summary>
    public int GetTotalGold()
    {
        return totalGold;
    }

    /// <summary>
    /// 获取当前局金币(游戏场景右上角显示用)
    /// </summary>
    public int GetCurrentSessionGold()
    {
        return currentSessionGold;
    }
}
end

  这个脚本的核心逻辑在Awake方法中。当实例不存在时,就保存自己并且调用DontDestroyOnLoad让这个物体跨场景存在,如果实例已经存在,就销毁新生成的物体,确保永远只有一个实例存在。
  接下来是开始场景的UI脚本(StartSceneUI.cs)。

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using TMPro;  // 使用 TextMeshPro(推荐)或普通 Text

public class StartSceneUI : MonoBehaviour
{
    [SerializeField] private Text goldText;  // 拖拽赋值
    [SerializeField] private string gameSceneName = "GameScene";  // 游戏场景名

    private void Start()
    {
        UpdateGoldDisplay();
    }

    private void OnEnable()
    {
        // 每次激活场景时刷新显示(从游戏场景返回时)
        UpdateGoldDisplay();
    }

    private void UpdateGoldDisplay()
    {
        if (GameManager.Instance != null)
        {
            goldText.text = $"总金币:{GameManager.Instance.GetTotalGold()}";
        }
        else
        {
            Debug.LogError("GameManager 实例不存在!");
            goldText.text = "总金币:0";
        }
    }

    // 给按钮调用的方法
    public void OnEnterGameButton()
    {
        SceneManager.LoadScene(gameSceneName);
    }
}
end

  这个脚本做的事情很简单,每次进入开始场景或者从游戏场景返回时,都会从GameManager中读取总金币数并显示在Text组件上。OnEnterGameButton方法绑定给按钮,点击后加载游戏场景。
  然后是游戏场景的主控制脚本(GameSceneUI.cs)。

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using TMPro;

public class GameSceneUI : MonoBehaviour
{
    [SerializeField] private Text currentGoldText;  // 右上角显示本局金币
    [SerializeField] private GameObject exitButton;            // 退出按钮(初始隐藏)
    [SerializeField] private string startSceneName = "StartScene";

    private int boxesClickedCount = 0;   // 已点击的 Box 数量
    private int totalBoxes = 5;          // 总共 5 个 Box

    private void Start()
    {
        // 确保退出按钮初始隐藏
        if (exitButton != null)
            exitButton.SetActive(false);

        // 初始化显示
        UpdateGoldDisplay();
    }

    private void Update()
    {
        // 实时刷新显示(或者只在金币变化时刷新,为了简单这里每帧更新)
        UpdateGoldDisplay();
    }

    private void UpdateGoldDisplay()
    {
        if (GameManager.Instance != null)
        {
            currentGoldText.text = $"本局金币:{GameManager.Instance.GetCurrentSessionGold()}";
        }
    }

    /// <summary>
    /// 由 ClickableBox 调用,通知一个 Box 被点击了
    /// </summary>
    public void OnBoxClicked()
    {
        boxesClickedCount++;
        Debug.Log($"已点击 {boxesClickedCount} / {totalBoxes} 个 Box");

        // 检查是否点完了所有 Box
        if (boxesClickedCount >= totalBoxes)
        {
            ShowExitButton();
        }
    }

    private void ShowExitButton()
    {
        if (exitButton != null)
        {
            exitButton.SetActive(true);
            Debug.Log("所有 Box 已点完,显示退出按钮");
        }
    }

    // 给退出按钮调用的方法
    public void OnExitButton()
    {
        // 结束当前局:把本局金币加到总金币
        if (GameManager.Instance != null)
        {
            GameManager.Instance.EndSession();
        }

        // 返回开始场景
        SceneManager.LoadScene(startSceneName);
    }
}
end

  这个脚本负责管理游戏场景中的UI显示和退出逻辑,每有一个Box被点击,OnBoxClicked方法就会被调用,计数器加一,当计数器达到5时,退出按钮就会显示出来,点击退出按钮时,会调用GameManager的EndSession方法,把当前局的金币累加到总金币中,然后切换回开始场景。
  最后是可点击Box的脚本(ClickableBox.cs)。

using UnityEngine;

public class ClickableBox : MonoBehaviour
{
    private bool isClicked = false;  // 防止重复点击

    private void OnMouseDown()
    {
        if (isClicked) return;

        isClicked = true;

        // 通知 GameManager 增加金币
        if (GameManager.Instance != null)
        {
            GameManager.Instance.AddGold(1);
        }

        // 通知 GameSceneUI 一个 Box 被点击了
        GameSceneUI gameSceneUI = FindObjectOfType<GameSceneUI>();
        if (gameSceneUI != null)
        {
            gameSceneUI.OnBoxClicked();
        }

        // 销毁这个 Box
        Destroy(gameObject);

        Debug.Log($"{gameObject.name} 被点击并销毁");
    }
}
end

  这个脚本挂载到每个Box上,OnMouseDown是Unity内置的方法,当用户点击带有Collider组件的物体时会被自动调用。为了防止重复点击同一个Box,使用isClicked标志位,第一次点击后就会置为true,后续的点击不会再执行。每次点击会调用GameManager的AddGold方法增加金币,同时通知GameSceneUI更新点击计数,最后销毁这个Box物体。
  加下来开始搭建场景,在开始搭建之前,你需要先创建两个场景,分别命名为StartScene和GameScene,然后把这两个场景添加到Build Settings中,如下图所示。
图片1
  开始场景的搭建步骤如下:创建一个空物体命名为GameManager,把刚才的GameManager脚本挂上去;创建一个Canvas,在Canvas里面创建一个Text命名为GoldText,放在合适的位置;再创建一个Button命名为EnterGameButton,把按钮上的文字改成Start;最后创建一个空物体命名为StartSceneUI,挂载StartSceneUI脚本,把GoldText拖拽到脚本的goldText字段上,在Inspector中把gameSceneName设置为GameScene,然后选中EnterGameButton,在Button组件的OnClick事件中点击加号,把StartSceneUI物体拖进去,选择StartSceneUI.OnEnterGameButton方法,如下图所示。
图片2
  游戏场景的搭建步骤如下。创建一个Canvas,在Canvas里面创建一个Text命名为CurrentGoldText,放在右上角;再创建一个Button命名为ExitButton,文字改成Back,取消勾选Active让这个按钮初始隐藏;创建一个空物体命名为GameSceneUI,挂载GameSceneUI脚本,把CurrentGoldText拖拽到currentGoldText字段,把ExitButton拖拽到exitButton字段,把startSceneName设置为StartScene;然后选中ExitButton,在Button组件的OnClick事件中点击加号,把GameSceneUI物体拖进去,选择GameSceneUI.OnExitButton方法;最后创建五个Cube作为可点击的Box,调整它们的位置让它们分散在场景中,如下图所示。
图片3
  运行测试流程:从开始场景启动游戏,你会看到总金币显示为0。点击进入游戏按钮,进入游戏场景。依次点击五个Box,每点击一个右上角的本局金币数就会加一。点击完第五个Box后,退出按钮会自动显示出来。点击退出按钮,你会回到开始场景,此时总金币变成了5。再次点击进入游戏,右上角又从0开始,再次点完五个Box后退出,总金币就会变成10。

posted on 2026-04-16 00:29  lcx_Bill  阅读(61)  评论(0)    收藏  举报

导航