彻底搞懂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中,如下图所示。

开始场景的搭建步骤如下:创建一个空物体命名为GameManager,把刚才的GameManager脚本挂上去;创建一个Canvas,在Canvas里面创建一个Text命名为GoldText,放在合适的位置;再创建一个Button命名为EnterGameButton,把按钮上的文字改成Start;最后创建一个空物体命名为StartSceneUI,挂载StartSceneUI脚本,把GoldText拖拽到脚本的goldText字段上,在Inspector中把gameSceneName设置为GameScene,然后选中EnterGameButton,在Button组件的OnClick事件中点击加号,把StartSceneUI物体拖进去,选择StartSceneUI.OnEnterGameButton方法,如下图所示。

游戏场景的搭建步骤如下。创建一个Canvas,在Canvas里面创建一个Text命名为CurrentGoldText,放在右上角;再创建一个Button命名为ExitButton,文字改成Back,取消勾选Active让这个按钮初始隐藏;创建一个空物体命名为GameSceneUI,挂载GameSceneUI脚本,把CurrentGoldText拖拽到currentGoldText字段,把ExitButton拖拽到exitButton字段,把startSceneName设置为StartScene;然后选中ExitButton,在Button组件的OnClick事件中点击加号,把GameSceneUI物体拖进去,选择GameSceneUI.OnExitButton方法;最后创建五个Cube作为可点击的Box,调整它们的位置让它们分散在场景中,如下图所示。

运行测试流程:从开始场景启动游戏,你会看到总金币显示为0。点击进入游戏按钮,进入游戏场景。依次点击五个Box,每点击一个右上角的本局金币数就会加一。点击完第五个Box后,退出按钮会自动显示出来。点击退出按钮,你会回到开始场景,此时总金币变成了5。再次点击进入游戏,右上角又从0开始,再次点完五个Box后退出,总金币就会变成10。
浙公网安备 33010602011771号