【unity进阶知识1】最详细的单例模式的设计和应用,继承和不继承MonoBehaviour的单例模式,及泛型单例基类的编写

devtools/2024/10/18 11:41:56/

文章目录

  • 前言
  • 一、不使用单例
  • 二、普通单例模式
    • 1、单例模式介绍
      • 实现步骤:
      • 单例模式分为饿汉式和懒汉式两种。
    • 2、不继承MonoBehaviour的单例模式
      • 2.1、基本实现
      • 2.2、防止外部实例化对象
      • 2.3、最终代码
    • 3、继承MonoBehaviour的单例模式
      • 3.1、基本实现
      • 3.2、自动创建和挂载单例脚本
      • 3.3、切换场景不销毁单例对象
      • 3.4、最终代码
  • 三、泛型单例基类
    • 1、不继承MonoBehaviour的单例模式基类
      • 1.1、基本实现
      • 1.2、防止外部实例化对象
      • 1.3、 多线程访问单例时会遇到问题
      • 1.3、最终代码
    • 2、继承MonoBehaviour的单例模式基类
      • 2.1、基本实现
      • 2.2、切换场景不销毁单例对象
      • 2.3、在OnDestroy方法中访问单例对象
      • 2.4、最终代码
  • 完结

前言

游戏开发中,单例模式应该是我们最常见也是用的最多的设计模式了,但是你真的了解它吗?

本文通过实例分析,我们将阐述如何设计和应用单例模式,以提高代码的可维护性和复用性。无论是初学者还是经验丰富的开发者,这篇文章都将为你提供实用的技巧和深入的理解,帮助你在 Unity 项目中更有效地管理资源和对象。

一、不使用单例

为什么要是有单例?我们先来看看不使用单例的情况下如何访问不同类方法

新增TestModel ,新增Log测试方法

public class TestModel 
{public int money = 100;public int level = 2;public void Log(){Debug.Log($"打印金额:{money}");Debug.Log($"打印等级:{level}");}
}

调用Log方法

TestModel testModel = new TestModel();
testModel.Log();

运行效果,打印日志信息
在这里插入图片描述
可以发现,每次调用Log方法,我们都需要先实例化TestModel。如果我们还希望TestModel 数据应该保证整个游戏只有一份的,但是现在我们可以随意实例化多份TestModel 数据,这样我们就分不清哪个才是我们要的真正的数据
在这里插入图片描述
单例模式就可以很好的解决这个问题,而且访问的时候也可以非常方便的访问

二、普通单例模式

在这里插入图片描述

1、单例模式介绍

如果要让一个类只有唯一的一个对象,则可以使用单例模式来写。使用的时候用“类名.Instance.成员名”的形式来访问这个对象的成员。

实现步骤:

  • 1、把构造函数私有化,防止外部创建对象。
  • 2、提供一个属性给外部访问,这个属性就相当于是这个类唯一的对象。

单例模式分为饿汉式和懒汉式两种。

  • 1、饿汉式单例模式
    在程序一开始的时候就创建了单例对象。但这样一来,这些对象就会在程序一开始时就存在于内存之中,占据着一定的内存。
  • 2、懒汉式单例模式
    在用到单例对象的时候才会创建单例对象。

2、不继承MonoBehaviour的单例模式

2.1、基本实现

按前面的介绍编写代码

public class TestModel 
{private static TestModel instance;public static TestModel Instance { get { //保证对象的唯一性if (instance == null){instance = new TestModel();}return instance; } }public int money = 100;public int level = 2;public void Log(){Debug.Log($"打印金额:{money}");Debug.Log($"打印等级:{level}");}
}

调用Log方法

TestModel.Instance.Log();

运行效果,打印日志信息,和之前的一样
在这里插入图片描述
现在无论你如何访问,都是同一个实例

2.2、防止外部实例化对象

当然,你会发现目前还是可以通过之前非单例模式进行访问TestModel数据

TestModel testModel = new TestModel();

如果你想防止外部实例化对象,其实也很简单,只要定义私有的构造方法即可

private TestModel(){}

2.3、最终代码

public class TestModel 
{private static TestModel instance;public static TestModel Instance { get { //保证对象的唯一性if (instance == null){instance = new TestModel();}return instance; } }//定义私有的构造方法,防止外部实例化对象private TestModel(){}public int money = 100;public int level = 2;public void Log(){Debug.Log($"打印金额:{money}");Debug.Log($"打印等级:{level}");}
}

3、继承MonoBehaviour的单例模式

3.1、基本实现

继承MonoBehaviour的单例模式和前面类似,唯一的区别就是我们需要使用FindObjectOfType<T>() 来获取组件的引用,它是 Unity 中的一个方法,用于在场景中查找并返回类型为 T 的第一个实例。

public class TestUI : MonoBehaviour 
{//定义私有的构造方法,防止外部实例化对象private TestUI(){}private static TestUI instance;public static TestUI Instance { get { //保证对象的唯一性if (instance == null){instance = FindObjectOfType<TestUI>();}return instance; } }public void Log(){Debug.Log("打印日志:访问成功");}
}

调用

TestUI.Instance.Log();

直接执行肯定报空引用异常错误
在这里插入图片描述
因为我们继承了monobehavior的脚本,所以要要挂载到游戏场景身上才有用,记得挂载脚本
在这里插入图片描述

运行效果
在这里插入图片描述

3.2、自动创建和挂载单例脚本

如果每次访问单例我们都需要手动挂载脚本,那也太麻烦了,所以一般我们都通过代码自动创建和挂载对应脚本

GameObject go = new GameObject("TestUI");//创建游戏对象
instance = go.AddComponent<TestUI>();//挂载脚本到游戏对象

3.3、切换场景不销毁单例对象

即使我们在当前场景创建了单例对象,但是切换到一个新场景,必然会销毁之前的所有对象,这样就找不到之前创建的单例对象了

我们可以使用DontDestroyOnLoad(instance);,用于确保指定的游戏对象在加载新场景时不会被销毁。通常,当场景切换时,Unity 会销毁当前场景中的所有对象,但使用这个方法后,调用的对象(例如单例模式中的实例)将保持存在。
在这里插入图片描述

3.4、最终代码

public class TestUI : MonoBehaviour 
{//定义私有的构造方法,防止外部实例化对象private TestUI(){}private static TestUI instance;public static TestUI Instance { get { //保证对象的唯一性if (instance == null){instance = FindObjectOfType<TestUI>();if(instance == null){GameObject go = new GameObject("TestUI");//创建游戏对象instance = go.AddComponent<TestUI>();//挂载脚本到游戏对象}DontDestroyOnLoad(instance);}return instance; } }public void Log(){Debug.Log("打印日志:访问成功");}
}

三、泛型单例基类

一个游戏可能有很多个单例,如果每个单例都需要书写这么多代码,既麻烦又容易出错,我们可以选择定义泛型单例基类

1、不继承MonoBehaviour的单例模式基类

1.1、基本实现

我们没办法new泛型T,所以使用反射

/// <summary>
/// 不继承MonoBehaviour的泛型单例基类
/// </summary>
public class Singleton<T> where T : Singleton<T>
{private static T instance;public static T Instance{get{// 保证对象的唯一性if (instance == null){instance = Activator.CreateInstance(typeof(T), true) as T; // 使用反射创建实例}return instance;}}// 私有构造函数,防止外部实例化protected Singleton() { }
}

使用,想要成为单例的类直接这个继承Singleton泛型单例基类即可,就不需要重复写那么多代码了

public class TestModel : Singleton<TestModel>
{// //定义私有的构造方法,防止外部实例化对象// private TestModel(){}// private static TestModel instance;// public static TestModel Instance { //     get { //         //保证对象的唯一性//         if (instance == null){//             instance = new TestModel();//         }//         return instance; //     } // }public int money = 100;public int level = 2;public void Log(){Debug.Log($"打印金额:{money}");Debug.Log($"打印等级:{level}");}
}

调用,调用和之前一样

TestModel.Instance.Log();

结果,正常打印
在这里插入图片描述

1.2、防止外部实例化对象

不过现在我们又可以通过实例化的方式直接进行访问TestModel数据

TestModel testModel = new TestModel();

我们可以和前面一样,定义私有的构造方法,防止外部实例化对象

private TestModel(){}

不过为了方便,我们通常都不这么做,因为这样每个类我们又要新增这段构造方法,完全没有必要。多人协作时,我们只需要内部沟通好,单例不要通过实例化访问即可。

1.3、 多线程访问单例时会遇到问题

我们可以使用lock线程锁和volatile关键字进行处理。
lock线程锁当多线程访问时,同一时刻仅允许一个线程访问。
volatile关键字修饰的字段,当多个线程都对它进行修改时,可以确保这个字段在任何时刻呈现的都是最新的值。

private static object locker = new object();
private volatile static T instance;

1.3、最终代码

using System;/// <summary>
/// 不继承MonoBehaviour的泛型单例基类
/// </summary>
public class Singleton<T> where T : Singleton<T>
{//线程锁。当多线程访问时,同一时刻仅允许一个线程访问private static object locker = new object();//volatile关键字修饰的字段,当多个线程都对它进行修改时,可以确保这个字段在任何时刻呈现的都是最新的值private volatile static T instance;public static T Instance{get{if (instance == null){lock (locker){// 保证对象的唯一性if (instance == null){instance = Activator.CreateInstance(typeof(T), true) as T; // 使用反射创建实例}}}return instance;}}// 私有构造函数,防止外部实例化protected Singleton() { }
}

2、继承MonoBehaviour的单例模式基类

2.1、基本实现

using UnityEngine;/// <summary>
/// 继承MonoBehaviour的泛型单例基类
/// </summary>
public class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{private static T instance;public static T Instance{get{if (instance == null){instance = FindObjectOfType<T>();if (instance == null){GameObject go = new GameObject(typeof(T).Name); // 创建游戏对象instance = go.AddComponent<T>(); // 挂载脚本}}return instance;}}// 构造方法私有化,防止外部 new 对象protected SingletonMono() { }
}

使用

public class TestUI : SingletonMono<TestUI> 
{// //定义私有的构造方法,防止外部实例化对象// private TestUI(){}// private static TestUI instance;// public static TestUI Instance { //     get { //         //保证对象的唯一性//         if (instance == null){//             instance = FindObjectOfType<TestUI>();//             if(instance == null){//                 GameObject go = new GameObject("TestUI");//创建游戏对象//                 instance = go.AddComponent<TestUI>();//挂载脚本到游戏对象//             }//             DontDestroyOnLoad(instance);//         }//         return instance; //     } // }public void Log(){Debug.Log("打印日志:访问成功");}
}

调用

TestUI.Instance.Log();

效果,正常访问
在这里插入图片描述

2.2、切换场景不销毁单例对象

和前面一样,同样加上DontDestroyOnLoad(instance);即可

2.3、在OnDestroy方法中访问单例对象

如果直接在在OnDestroy方法中访问单例对象

private void OnDestroy() {TestUI.Instance.Log();
}

每次运行结束时会报错:
Some objects were not cleaned up when closing the scene.(Did you spawn new GameObjects from OnDestroy?)

在这里插入图片描述
修改SingletonMono基类,我们可以新增变量IsExisted 记录单例对象是否存在,在成功实例化时IsExisted= trueOnDestroyIsExisted=false

public static bool IsExisted { get; private set; } = false;

在OnDestroy调用时,先判断IsExisted是否为true

private void OnDestroy() {if(TestUI.IsExisted) TestUI.Instance.Log();
}

效果,开始运行执行一次,结束运行调用OnDestroy又执行一次,且无报错
在这里插入图片描述

2.4、最终代码

using UnityEngine;/// <summary>
/// 继承MonoBehaviour的泛型单例基类
/// </summary>
public class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{//记录单例对象是否存在。用于防止在OnDestroy方法中访问单例对象报错public static bool IsExisted { get; private set; } = false;private static T instance;public static T Instance{get{if (instance == null){instance = FindObjectOfType<T>();if (instance == null){GameObject go = new GameObject(typeof(T).Name); // 创建游戏对象instance = go.AddComponent<T>(); // 挂载脚本}}DontDestroyOnLoad(instance);IsExisted = true;return instance;}}// 构造方法私有化,防止外部 new 对象protected SingletonMono() { }private void OnDestroy() {IsExisted = false;}
}

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇,https://xiangyu.blog.csdn.net

一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述


http://www.ppmy.cn/devtools/118135.html

相关文章

废品回收小程序:回收更加便捷!

在日常生活中&#xff0c;废品回收已经成为了一种常见事&#xff0c;随着电商的快速发展&#xff0c;居民难免会产生大量的废纸盒等可回收物&#xff0c;以及在日常生活中产生的其他回收物&#xff0c; 目前&#xff0c;废品回收市场也发生了改革&#xff0c;传统的“叫卖”方…

【嵌入式开发】有关16head(16接口点击器)相关的资料

16接口点击头产品运用ESP8266 ESP8266是一款功能强大的低成本WiFi芯片&#xff0c;它支持多种网络协议&#xff0c;能够实现各种网络通信功能。 点击学习详细内容 之前讲解的点击器是用串口连接后&#xff0c;使用触控头来控制的方法 后续会在CSDN上讲解该板子用http请求控制…

研究生如何利用 ChatGPT 帮助开展日常科研工作?

ChatGPT科研 一、 如何精读论文“三步提问法”1.为什么要做这个研究&#xff1f;这个研究是否值得我们做&#xff1f;2.他们怎么做这个研究3.他们发现了什么&#xff1f; 二、如何利用ChatGPT快速精读论文&#xff1f;首先&#xff0c;“三步走之第一步”--为什么要做这个研究&…

深度解读 2024 Gartner DevOps 魔力象限

上周 Gartner 刚发布了 2024 年度的 DevOps 魔力象限。我们也第一时间来深度解读一下这份行业里最权威的报告。 和2023年对比 23 年入围 14 家厂商&#xff0c;24 年入围 11 家。4 家厂商从报告中消失&#xff0c;分别是 Bitrise, Codefresh, Google Cloud Platform (GCP), VM…

【MySQL】表的操作

目录 一、增加表 二、查看表 2.1 查看当前数据库中的表 2.2 查看指定表的结构 2.3 查看创建表时的详细信息 2.4 查看表中所有数据 三、修改表 3.1 修改表名 3.2 插入数据 3.3 添加列 3.4 修改列类型 3.5 删除列 3.6 修改列名 四、删除表 一、增加表 增加表的语法…

网站服务器监控:主机性能监测指标解读

监控易是一款功能强大的IT监控软件&#xff0c;能够实时监控网站服务器、中间件、数据库等IT资源的应用和业务状态&#xff0c;确保系统的稳定运行和高效性能。在网站服务器监控中&#xff0c;主机性能监测是至关重要的一环&#xff0c;它直接关系到服务器的整体运行效率和稳定…

UNI-SOP应用场景(1)- 纯前端预开发

在平时新项目开发中&#xff0c;前端小伙伴是否有这样的经历&#xff0c;hi&#xff0c;后端小伙伴们&#xff0c;系统啥时候能登录&#xff0c;啥时候能联调了&#xff0c;这是时候往往得到的回答就是&#xff0c;再等等&#xff0c;我们正在搭建系统呢&#xff0c;似曾相识的…

Unity中的GUIStyle错误:SerializedObject of SerializedProperty has been Disposed.

一运行就循环打印这个报错&#xff0c; 解决办法&#xff0c;每次改参数之后在HIerarchy中手动保存&#xff0c;就会停止循环打印&#xff0c;style中的字体也显示出来了&#xff0c; 或者 直接换个低版本的