MonoSingleton——Unity中的单例模式

本文针对Unity中的单例模式提出了一种优化方案,避免了全场景搜索带来的性能损耗,并通过Mutex确保实例唯一性。介绍了如何利用Awake()方法保证运行时单例特性,同时提供了包含初始化和逆初始化过程的抽象基类。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Unity中有很多特别的类需要以单例模式呈现,比如全局的UI管理类,各种缓存池,以及新手导航类等等。而Unity中,因为所有继承自Monobehaviour的脚本在实现的时候都是单线程的,所以像网上流传的一些C#的实现方式就显得不那么的实用了。

很多国内的公司所使用的MonoSingleton都是有问题的,比如像Easytouch中关于单例是这样实现中有这样一段代码。

public static T instance
{
    get
    {
        if (m_Instance == null)
        {
            m_Instance = GameObject.FindObjectOfType(typeof(T)) as T;//1这里耗费性能,有风险
            if (m_Instance == null)//2
            {
                m_Instance = new GameObject("Singleton of " + typeof(T).ToString(), typeof(T)).GetComponent<T>();
                m_Instance.Init();
            }
        }
        return m_Instance;
    }
}

那么我标注的两处就是代码当中不正确的地方。2处这是明显的套用了多线程的单例实现方式,而实际上,在单线程模式当中这个判断并没有意义。而1中,直接对全场景进行搜索的过程其本身就很浪费性能。那么正确的实现方式是什么呢?

首先,我们需要一个全局变量,比如,先建立一个全局类Global

public abstract class Global : MonoBehaviour
{
    public static HashSet<string> Singleton=new HashSet<string>();
}

每次建立都将类名存进HashSet当中,那么上面那段代码就可以改成这样。

public static T instance
{
    get
    {
        if (m_Instance == null)
        {
            var name = "Singleton of " + typeof(T).ToString();
            var flag = Global.Singleton.Contains(name);
            if (!flag)
            {
                m_Instance = new GameObject(name, typeof(T)).GetComponent<T>();
                m_Instance.Init();
                Global.Singleton.Add(name);
            }
        }
        return m_Instance;
    }
}
可能您要说了,我已经有了一个全局类了,那么难道还要再填一个东西?我只想直接用,用没有更简便的方法。您要说更好,不一定,但是更简便,确实有的。我们这里可以用上互斥类Mutex的类,那么上面那段代码就可以改成下面这样:

public static T instance
{
    get
    {
        if (m_Instance == null)//注意,此处在实际中只执行一次。
        {
            bool createdNew;
            var name = "Singleton of " + typeof(T).ToString();
            Mutex mutex = new Mutex(false, name, out createdNew);
            if (createdNew)
            {
                m_Instance = new GameObject(name, typeof(T)).GetComponent<T>();
                m_Instance.Init();
            }
        }
        return m_Instance;
    }
}

但这只是说,我如果在其他地方操作这个单例,而这个单例还必须新建一个游戏物体,还必须挂在上面,挂在游戏物体上就至少要有一个transform组件。那么我可不可以直接挂在物体上,那该怎么办?如果我挂多了该怎么办?

有办法,这里我们利用Awake()方法

private void Awake()
{
    if (m_Instance == null)
    {
        bool createdNew;
        var name = "Singleton of " + typeof(T).ToString();
        Mutex mutex = new Mutex(false, name, out createdNew);
        if (createdNew)
        {
            m_Instance = this as T;
            m_Instance.Init();
        }
    }
    else
    {
        Destroy(this);
    }


这样就可以保证运行时的单例了。那么完整的MonoSingleton还需要一些细节。比如在我的单例基类中,我设计成抽象类,提供了两个抽象函数,分别是初始化和逆初始化。之所以这么做就是为了在我们继承MonoSingleton的时候想一想,是不是必须要把这个类做成单例的,它一定是有单例的必要所以才是单例的,而不是将单例当静态使用。

using UnityEngine;
using System.Collections;
using System.Threading;
/// <summary>
/// 单例基类,提供两个抽象函数Init 和 DisInit 初始化和逆初始化过程。
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class MonoSingleton<T> : MonoBehaviour
where T : MonoSingleton<T>
{

    private static T m_Instance = null;
    private static string name;
    private static Mutex mutex;
    public static T instance
    {
        get
        {
            if (m_Instance == null)
            {
                if ( IsSingle())
                {
                    m_Instance = new GameObject(name, typeof(T)).GetComponent<T>();
                    m_Instance.Init();
                }
            }
            return m_Instance;
        }
    }

    private static bool IsSingle()
    {
        bool createdNew;
        name = "Singleton of " + typeof(T).ToString();
        mutex = new Mutex(false, name, out createdNew);
        return createdNew;
    }

    private void Awake()
    {
        if (m_Instance == null)
        {
            if (IsSingle())
            {
                m_Instance = this as T;
                m_Instance.Init();
            }
        }
        else
        {
            Destroy(this);
        }
    }

    protected abstract void Init();
    protected abstract void DisInit();
    private void OnDestory()
    {
        if (m_Instance!=null)
        {
            mutex.ReleaseMutex();
            DisInit();
            m_Instance = null;
        }
    }
    private void OnApplicationQuit()
    {
        mutex.ReleaseMutex();
    }
}


<think>首先,用户的问题是:“我想了解在Unity架构中实现单例模式的几种方式 请问Unity 单例模式 实现方式 种类”。这翻译成英文是:“I want to know several ways to implement the singleton pattern in the Unity architecture. What are the types of singleton pattern implementations in Unity?” 现在,我需要根据系统级指令来构建我的回答。关键点包括: - 所有行内数学表达式使用 `$...$` 格式。 - 独立公式使用 `$$...$$` 格式并单独成段。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾自然地添加引用标识,例如 [^1]。 - 回答结构清晰,逐步解决问题。 - 参考提供的引用,但用户的问题是关于Unity中的单例模式,所以我需要基于这些引用来构建回答。 提供的引用是: - [^1]: 继承MonoBehaviour的单例类,避免构造函数问题。 - [^2]: 使用泛型创建通用单例脚本。 - [^3]: 单例模式的基本介绍,确保一个类只有一个实例。 - [^4]: 封装数据在类中,但不采用单例模式。 - [^5]: C#中的单例模式实现,包括饿汉式。 用户的问题具体是Unity中的实现方式,所以我应该聚焦在Unity相关的部分。 从引用中提取关键信息: - **引用[^1]**: 展示了一个继承自MonoBehaviour的单例实现。使用Awake方法来设置实例。 - **引用[^2]**: 使用泛型创建单例脚本,适用于MonoBehaviour组件。 - **引用[^5]**: 展示了C#中的饿汉式单例实现,但这不是Unity特有的;Unity中通常使用MonoBehaviour。 Unity单例模式的常见实现方式: 1. **基于MonoBehaviour的单例**:这是Unity中最常见的,因为Unity的游戏对象和组件系统依赖于MonoBehaviour。引用[^1]和[^2]都涉及这个。 2. **饿汉式单例**:如引用[^5]所示,在类加载时创建实例。但在Unity中,这可能不直接适用于MonoBehaviour,因为MonoBehaviour需要附加到游戏对象上。 3. **懒汉式单例**:在第一次访问时创建实例。 对于Unity,主要方式是基于MonoBehaviour的,因为Unity的脚本系统围绕游戏对象和组件。 常见的Unity单例实现种类: 1. **简单MonoBehaviour单例**:如引用[^1],在Awake中设置实例。 2. **泛型单例基类**:如引用[^2],使用泛型来创建可重用的单例组件。 3. **静态单例**:不使用MonoBehaviour,但需要小心处理Unity的生命周期。 4. **饿汉式或懒汉式**:在C#层面实现,但需适应Unity环境。 在回答中,我应该列出并解释几种方式。 结构清晰的回答: - 介绍单例模式Unity中的重要性。 - 逐步解释每种实现方式。 - 提供代码示例。 - 使用中文。 - 在末尾生成相关问题。 - 自然地添加引用标识。 由于引用是提供的,我需要在回答中引用它们。例如,当提到某种方式时,添加 [^n]。 例如: - 对于继承MonoBehaviour的方式,引用 [^1]。 - 对于泛型方式,引用 [^2]。 - 对于饿汉式,引用 [^5]。 引用[^3]是通用介绍,引用[^4]是关于数据存储但不使用单例,所以可能不直接相关。 现在,构建回答大纲: 1. **引言**:简要解释单例模式Unity中的作用,如管理全局资源、确保唯一实例。 2. **方式1: 简单MonoBehaviour单例**:解释并给出代码示例,引用 [^1]。 3. **方式2: 泛型单例基类**:解释并给出代码示例,引用 [^2]。 4. **方式3: 静态类单例(非MonoBehaviour)**:解释如何在Unity中实现,如使用饿汉式或懒汉式,引用 [^5]。 5. **方式4: 其他变体**:如懒汉式或线程安全,但Unity主要在单线程环境。 6. **总结**:比较这些方式,推荐在Unity中使用基于MonoBehaviour的。 然后,生成相关问题。 确保LaTeX语法正确:由于问题不涉及数学,可能不需要数学表达式,但如果有变量,使用 `$...$`。例如,如果提到实例,可以写成 `$instance$`,但可能没必要。 最后,相关问题:例如: 1. 如何在Unity中处理单例模式的线程安全? 2. 单例模式Unity场景切换时的注意事项? 3. 替代单例模式Unity设计模式?</think>### Unity 单例模式实现方式详解 在 Unity 架构中,单例模式(Singleton Pattern)用于确保一个类只有一个实例,并提供全局访问点,常用于管理全局资源(如游戏管理器、音频管理器或数据存储)。Unity 的单例实现通常基于 MonoBehaviour 组件,以适应 Unity 的生命周期(如 Awake、Start 等方法)。以下我将逐步介绍几种常见的实现方式,每种方式都附有代码示例和解释。实现方式主要分为三类:**简单 MonoBehaviour 单例**、**泛型单例基类** 和 **静态类单例(非 MonoBehaviour)**。这些方式各有优缺点,我会在总结部分简要比较。 #### 1. **简单 MonoBehaviour 单例** 这种方式直接在自定义脚本中继承 MonoBehaviour,并在 Awake 方法中设置静态实例。它适用于需要与 Unity 生命周期集成的场景,如游戏对象上的组件。 - **优点**:简单易用,自动处理 Unity 初始化和销毁。 - **缺点**:每个单例类需单独实现,代码重复率高。 - **实现代码示例**: ```csharp using UnityEngine; public class GameManager : MonoBehaviour { private static GameManager instance; // 静态实例变量 public static GameManager Instance { get { return instance; } } // 全局访问点 private void Awake() { if (instance != null && instance != this) { Destroy(this.gameObject); // 确保只有一个实例 } else { instance = this; // 设置实例 DontDestroyOnLoad(this.gameObject); // 可选:跨场景持久化 } } // 示例方法 public void LoadLevel(string levelName) { Debug.Log($"加载关卡: {levelName}"); } } ``` - 在 Unity 中,将此脚本附加到游戏对象(如空对象),Awake 方法会在场景加载时自动调用,确保实例唯一。如果检测到重复实例,则销毁新对象[^1]。 - **适用场景**:游戏管理器、场景控制器等需要持久化的全局对象。 #### 2. **泛型单例基类** 这种方式使用泛型(Generic)创建可重用的单例基类,减少代码重复。基类继承自 MonoBehaviour,派生类只需继承该基类即可自动获得单例功能。 - **优点**:代码复用性高,易于扩展;支持 Unity 生命周期方法。 - **缺点**:泛型约束可能增加复杂性;不适合非 MonoBehaviour 类。 - **实现代码示例**: ```csharp using UnityEngine; // 泛型单例基类,T 必须是 MonoBehaviour 的子类 public class SingletonMono<T> : MonoBehaviour where T : SingletonMono<T> { private static T instance; public static T Instance { get { if (instance == null) { // 查找场景中现有实例或创建新对象 instance = FindObjectOfType<T>(); if (instance == null) { GameObject obj = new GameObject(typeof(T).Name); instance = obj.AddComponent<T>(); } } return instance; } } protected virtual void Awake() { if (instance == null) { instance = (T)this; DontDestroyOnLoad(gameObject); // 可选:跨场景持久化 } else if (instance != this) { Destroy(gameObject); // 销毁重复实例 } } } // 派生类示例:音频管理器 public class AudioManager : SingletonMono<AudioManager> { public void PlaySound(string clipName) { Debug.Log($"播放音效: {clipName}"); } } ``` - 使用时,AudioManager 会自动处理实例创建和销毁。在 Awake 中,通过泛型确保类型安全,并避免手动编写单例逻辑[^2]。 - **适用场景**:可复用的管理器类,如音频管理器、UI 控制器。 #### 3. **静态类单例(非 MonoBehaviour)** 这种方式不依赖 MonoBehaviour,而是使用纯 C# 静态类或饿汉式/懒汉式实现。适用于不需要 Unity 生命周期的工具类或数据存储。 - **优点**:轻量级,无 Unity 开销;实例在类加载时或首次访问时创建。 - **缺点**:无法直接使用 Unity 方法(如 Start/Update);需手动处理初始化和线程安全(Unity 主要在单线程环境)。 - **实现代码示例(饿汉式)**: ```csharp using System; // 饿汉式单例:实例在静态变量初始化时创建 public class DataStorage { // 私有构造函数防止外部实例化 private DataStorage() { } // 静态变量初始化时创建实例 private static readonly DataStorage instance = new DataStorage(); // 全局访问点 public static DataStorage Instance => instance; // 示例数据属性 public string PlayerName { get; set; } = "默认玩家"; public void SaveData() { Console.WriteLine("数据已保存"); } } // 在 Unity 中使用 public class GameLoader : MonoBehaviour { void Start() { // 访问单例 DataStorage.Instance.SaveData(); Debug.Log(DataStorage.Instance.PlayerName); } } ``` - 饿汉式在类加载时创建实例,确保线程安全但可能提前占用资源。在 Unity 中,需通过其他 MonoBehaviour 脚本来访问,因为它不附加到游戏对象[^5]。 - **变体:懒汉式**(首次访问时创建实例): ```csharp public class ConfigManager { private static ConfigManager instance; private static readonly object lockObject = new object(); private ConfigManager() { } public static ConfigManager Instance { get { lock (lockObject) // 线程安全锁 { if (instance == null) { instance = new ConfigManager(); } return instance; } } } } ``` - 懒汉式适合资源敏感场景,但 Unity 中线程安全问题较少见。 - **适用场景**:数据存储类(如玩家配置)、工具类(如数学计算工具)。 #### 总结与比较 - **推荐方式**:在 Unity 中,优先使用 **泛型单例基类** 或 **简单 MonoBehaviour 单例**,因为它们与 Unity 生命周期集成良好,避免手动管理实例。静态类单例适合非游戏逻辑部分。 - **性能注意**:MonoBehaviour 单例有轻微运行时开销(如 Unity 消息循环),但通常可忽略;静态单例更高效。 - **最佳实践**: - 使用 `DontDestroyOnLoad` 确保跨场景持久化。 - 在 Awake 中检查实例唯一性,防止场景重复加载问题。 - 避免滥用单例,以保持代码可测试性(例如,用依赖注入替代)。 通过以上方式,您可以根据需求在 Unity 中灵活实现单例模式。如果您有特定场景(如多线程或跨场景),可以进一步优化。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值