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

文章目录

  • 前言
  • 一、不使用单例
  • 二、普通单例模式
    • 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.mzph.cn/diannao/54851.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

QT窗口无法激活弹出问题排查记录

问题背景 问题环境 操作系统: 银河麒麟V10SP1qt版本 : 5.12.12 碰见了一个问题应用最小化,然后激活程序窗口无法弹出 这里描述一下代码的逻辑,使用QLocalServer实现一个单例进程,具体的功能就是在已存在一个程序A进程时,再启动这个程序A,新的程序A进程会被杀死,然后激活已存…

linux信号| 学习信号三步走 | 学习信号需要打通哪些知识脉络?

前言: 本节内容主要讲解linux下信号的预备知识以及信号的概念&#xff0c; 信号部分我们将会分为几个阶段进行讲解&#xff1a;信号的概念&#xff0c; 信号的产生&#xff0c; 信号的保存。本节主要讲解信号 ps:本节内容适合学习了进程相关概念的友友们进行观看哦 目录 什么是…

电脑上数据丢了怎么找回来 Win系统误删文件如何恢复

无论是在工作中&#xff0c;还是生活中&#xff0c;电脑都是不可缺少的重要工具&#xff0c;尤其是在工作中&#xff0c;电脑不仅可以高效的完成工作&#xff0c;还可以存储工作中的重要资料。不过在使用电脑的时候&#xff0c;也会遇到数据丢失的情况。针对这一问题&#xff0…

水面巡检船垃圾漂浮物检测系统源码分享

水面巡检船垃圾漂浮物检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of …

在GPU计算型实例中安装Tesla驱动超详细过程

摘要&#xff1a;在深度学习、AI等通用计算业务场景或者OpenGL、Direct3D、云游戏等图形加速场景下&#xff0c;安装了Tesla驱动的GPU才可以发挥高性能计算能力&#xff0c;或提供更流畅的图形显示效果。如果您在创建GPU计算型实例&#xff08;Linux&#xff09;时未同时安装Te…

Linux服务器安装Anaconda环境

Linux浪潮云服务器安装Anaconda环境 读研之后在导师的帮助下&#xff0c;获得了浪潮的一台公共云服务器。以后做实验跑代码就可以使用云服务器上的虚拟环境了。减少了自己笔记本的压力。在创建并保存完成镜像环境之后。最重要的就是安装好深度学习需要的Anaconda环境&#xff0…

“类型名称”在Go语言规范中的演变

Go语言规范&#xff08;The Go Programming Language Specification&#xff09;[1]是Go语言的核心文档&#xff0c;定义了该语言的语法、类型系统和运行时行为。Go语言规范的存在使得开发者在实现Go编译器时可以依赖一致的标准&#xff0c;它确保了语言的稳定性和一致性&#…

python -tkinter

在Button的command绑定中&#xff0c;如果给的一个函数&#xff0c;则表示执行一次。 import tkinter as Tkfrom tkinter import messageboxi 0def myLabel():global root,is Tk.Label(root,text"the import thing is :" )s.pack()root Tk.Tk()def fun1():if mess…

[PICO VR眼镜]眼动追踪串流Unity开发与使用方法,眼动追踪打包报错问题解决(Eye Tracking/手势跟踪)

前言 最近在做一个工作需要用到PICO4 Enterprise VR头盔里的眼动追踪功能&#xff0c;但是遇到了如下问题&#xff1a; 在Unity里面没法串流调试眼动追踪功能&#xff0c;根本获取不到Device&#xff0c;只能将整个场景build成APK&#xff0c;安装到头盔里&#xff0c;才能在…

【Python语言初识(五)】

一、文件和异常 在Python中实现文件的读写操作其实非常简单&#xff0c;通过Python内置的open函数&#xff0c;我们可以指定文件名、操作模式、编码信息等来获得操作文件的对象&#xff0c;接下来就可以对文件进行读写操作了。这里所说的操作模式是指要打开什么样的文件&#…

泛型(Java)

1.泛型&#xff1a; 将数据类型作为参数进行传递。(传递的数据类型必须是引用数据类型) 本质是参数化类型。 泛型集合&#xff1a;可以约束集合内的元素类型 典型泛型集合ArrayList<E>、HashMap<K,V> <E>、<K,V>表示该泛型集合中的元素类型泛型集合中的…

计算机视觉学习路线

计算机视觉&#xff08;Computer Vision&#xff09;是计算机科学的一个重要分支&#xff0c;旨在使计算机能够理解和解释视觉数据。以下是一个详细的计算机视觉学习路线&#xff0c;帮你系统地掌握这个领域所需的知识和技能。 1. 基础数学和编程 在深入学习计算机视觉之前&…

引入Scrum激发研发体系活力

引言 在当今快速变化的技术环境中&#xff0c;IT企业面临着持续的市场压力和竞争&#xff0c;传统的瀑布式开发模式已经难以满足现代企业的需要。瀑布模型过于僵化&#xff0c;缺乏灵活性&#xff0c;导致项目经常延期&#xff0c;成本增加&#xff0c;最终可能无法达到预期效果…

【Qualcomm】高通SNPE框架的使用 | 原始模型转换为量化的DLC文件 | 在Android的DSP端运行模型

目录 ① 激活snpe环境 ② 设置环境变量 ③ 模型转换 ④ run 首先&#xff0c;默认SNPE工具已经下载并且Setup相关工作均已完成。同时&#xff0c;拥有原始模型文件&#xff0c;本文使用的模型文件为SNPE 框架示例的inception_v3_2016_08_28_frozen.pb文件。image_file_list…

数据集-目标检测系列-口罩检测数据集 mask>> DataBall

数据集-目标检测系列-口罩检测数据集 mask>> DataBall 数据集-目标检测系列-口罩检测数据集 mask 数据量&#xff1a;1W DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;享有百种数据集&#xff0c;持续增加中。 数据项目地址&#xff1a; gitcode: https…

【Python报错已解决】TypeError: list indices must be integers or slices, not str

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

打造同城O2O平台:外卖跑腿APP的架构与功能设计详解

今天&#xff0c;小编将于大家共同讨论外卖跑腿APP的架构设计及其核心功能&#xff0c;旨在为开发者提供一份详尽的参考。 一、外卖跑腿APP的架构设计 1.整体架构概述 通常包括前端、后端和数据库。 2.前端设计 用户端提供直观的界面&#xff0c;方便用户下单、查询订单状态…

初学51单片机之I2C总线与E2PROM

首先先推荐B站的I2C相关的视频I2C入门第一节-I2C的基本工作原理_哔哩哔哩_bilibili 看完视频估计就大概知道怎么操作I2C了&#xff0c;他的LCD1602讲的也很不错&#xff0c;把数据建立tsp和数据保持thd&#xff0c;比喻成拍照时候的摆pose和按快门两个过程&#xff0c;感觉还是…

C语言实现归并排序(Merge Sort)

目录 一、递归实现归并排序 1. 归并排序的基本步骤 2.动图演示 3.基本思路 4.代码 二、非递归实现 1.部分代码 2.代码分析 修正后代码&#xff1a; 归并过程打印 性能分析 复杂度分析 归并排序是一种高效的排序算法&#xff0c;采用分治法&#xff08;Divide and Con…

javase复习day35反射

反射 获取class对象的方法 public class Demo1 {public static void main(String[] args) throws ClassNotFoundException {//获取反射的三种方式//第一种 Class.forName(全类名)//用法&#xff1a;最为常用Class<?> clazz1 Class.forName("Reflection.Student&q…