【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 NonMonoSingleton<T> where T : NonMonoSingleton<T>
{
    private static T instance;

    public static T Instance
    {
        get
        {
            // 保证对象的唯一性
            if (instance == null)
            {
                instance = Activator.CreateInstance(typeof(T), true) as T; // 使用反射创建实例
            }
            return instance;
        }
    }

    // 私有构造函数,防止外部实例化
    protected NonMonoSingleton() { }
}

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

public class TestModel : NonMonoSingleton<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 NonMonoSingleton<T> where T : NonMonoSingleton<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 NonMonoSingleton() { }
}

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

2.1、基本实现

using UnityEngine;

/// <summary>
/// 继承MonoBehaviour的泛型单例基类
/// </summary>
public class MonoSingleton<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 MonoSingleton() { }
}

使用

public class TestUI : MonoSingleton<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?)

在这里插入图片描述
修改MonoSingleton基类,我们可以新增变量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 MonoSingleton<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 MonoSingleton() { }

    private void OnDestroy() {
        IsExisted = false;
    }
}

完结

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/882821.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

每日刷题(算法)

目录 我们N个真是太厉害了 思路&#xff1a; 代码&#xff1a; 折返跑 思路&#xff1a; 代码&#xff1a; 好好好数 思路&#xff1a; 代码&#xff1a; 魔法之森的蘑菇 思路&#xff1a; 代码&#xff1a; 我们N个真是太厉害了 思路&#xff1a; 我们先给数组排序…

安全常用的kali linux是怎样的,如何安装?

黑客或者安全在用的kali linux是怎样&#xff0c;安装 kali Linux的历史 Kali Linux由Offensive Security公司维护,可以追溯到BackTrack Linux这个著名的渗透测试发行版。BackTrack于2006年首次发布,基于Knoppix,集成了许多安全工具。它因功能强大而深受安全研究人员的喜爱。…

keepalived+lvs集群

目录 一、环境 二、配置 1、master 1.在master上安装配置Keepalived 2.在master上修改配置文件 2、backup 1.在backup&#xff08;192.168.229.12&#xff09;上安装keepalived 2.在backup上修改配置文件 3、master和backup上启动服务 4、web服务器配置 1.web1和web…

【计算机网络篇】物理层

本文主要介绍计算机网络第二章节的物理层&#xff0c;文中的内容是我认为的重点内容&#xff0c;并非所有。参考的教材是谢希仁老师编著的《计算机网络》第8版。跟学视频课为河南科技大学郑瑞娟老师所讲计网。 文章目录 &#x1f3af;一.基本概念及公式 &#x1f383;基本概念…

Python编码系列—Python模板方法模式:定义算法骨架,让子类实现细节

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

MySQL 主从复制部署与优化

文章目录 前言 在现代数据库管理中&#xff0c;MySQL 主从复制是一种关键技术&#xff0c;用于提高数据的可用性和性能。随着 Docker 容器技术的普及&#xff0c;利用 Docker 搭建 MySQL 主从复制环境已成为一种趋势&#xff0c;它提供了一种简便、高效且可扩展的解决方案。本…

【全网最全】2024年华为杯研赛A题成品论文获取入口(后续会更新)

您的点赞收藏是我继续更新的最大动力&#xff01; 一定要点击如下的卡片&#xff0c;那是获取资料的入口&#xff01; 【全网最全】2024年华为杯研赛A题保奖思路matlab/py代码成品论文等&#xff08;后续会更新完整 点击链接加入【2024华为杯研赛资料汇总】&#xff1a;https:…

Leetcode尊享面试100题-252.会议室

给定一个会议时间安排的数组 intervals &#xff0c;每个会议时间都会包括开始和结束的时间 intervals[i] [starti, endi] &#xff0c;请你判断一个人是否能够参加这里面的全部会议。 示例 1&#xff1a; 输入&#xff1a;intervals [[0,30],[5,10],[15,20]] 输出&#xff…

代码随想录算法训练营第三十九天 | 198.打家劫舍 ,213.打家劫舍II,337.打家劫舍III

第三十九天打卡&#xff0c;今天解决打家劫舍系列问题&#xff0c;树形dp比较难。 198.打家劫舍 题目链接 解题过程 dp[i]&#xff1a;考虑下标i&#xff08;包括i&#xff09;以内的房屋&#xff0c;最多可以偷窃的金额为dp[i]。 要么不偷这一间&#xff0c;那就是前面那间…

SQL面试常见题目

SQL面试常见题目涉及多个方面&#xff0c;包括数据查询、数据操作、表的设计与优化等。以下列举一些经典的SQL面试题目&#xff0c;并附上解析答案&#xff1a; 1. 查询一张表中重复的数据 题目&#xff1a; 给定一个表 employees&#xff0c;包含 id, name, salary 列。如何…

Uniapp时间戳转时间显示/时间格式

使用uview2 time 时间格式 | uView 2.0 - 全面兼容 nvue 的 uni-app 生态框架 - uni-app UI 框架 <text class"cell-tit clamp1">{{item.create_time}} --- {{ $u.timeFormat(item.create_time, yyyy-mm-dd hh:MM:ss)}} </text>

【Java 问题】基础——基础语法

基础语法 7. Java 有哪些数据类型8.自动类型转换、强制类型转换&#xff1f;9.什么是自动拆箱/封箱&#xff1f;10.&和&&有什么区别&#xff1f;11.switch 是否能作用在 byte/long/String上&#xff1f;12.break ,continue ,return 的区别及作用&#xff1f;13.用最…

Java ----常用类

包装类 包装类的分类 1) 针对八种基本数据类型相应的引用类型—包装类2) 有了类的特点&#xff0c;就可以调用类中的方法。3) 如图 包装类和基本数据的转换 jdk5 前的手动装箱和拆箱方式&#xff0c;装箱&#xff1a;基本类型转包装类型&#xff0c;拆箱&#xff1a;包装类型…

Flutter 安装,配置,运行第一个app 1

起因&#xff0c; 目的: flutter, 其实几年前&#xff0c;我就写过。 当时纯属是个人兴趣&#xff0c;随意探索。 当时我也写了几篇笔记: 比如这一篇还有这个 flutter&#xff0c;其实不难&#xff0c;比较繁琐&#xff0c;小的知识点很多. flutter&#xff0c; 又是环境配…

树与图的深度优先遍历(dfs的图论中的应用)

模板题 846. 树的重心 给定一颗树&#xff0c;树中包含 n 个结点&#xff08;编号 1∼n&#xff09;和 n−1条无向边。 请你找到树的重心&#xff0c;并输出将重心删除后&#xff0c;剩余各个连通块中点数的最大值。 重心定义&#xff1a;重心是指树中的一个结点&#xff0…

7天速成前端 ------学习日志 (继苍穹外卖之后)

前端速成计划总结&#xff1a; 全26h课程&#xff0c;包含html&#xff0c;css&#xff0c;js&#xff0c;vue3&#xff0c;预计7天内学完。 起始日期&#xff1a;9.16 预计截止&#xff1a;9.22 每日更新&#xff0c;学完为止。 学前计划 课…

《粮油与饲料科技》是什么级别的期刊?是正规期刊吗?能评职称吗?

问题解答 问&#xff1a;《粮油与饲料科技》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的第一批认定 学术期刊。 问&#xff1a;《粮油与饲料科技》级别&#xff1f; 答&#xff1a;省级。主管单位&#xff1a;中文天地出版传媒集团股份有限公司…

漏洞复现_永恒之蓝

1.概述 永恒之蓝&#xff08;EternalBlue&#xff09;是一个影响Windows操作系统的远程代码执行漏洞&#xff0c;编号为CVE-2017-0144&#xff0c;最初由美国国家安全局&#xff08;NSA&#xff09;开发并利用&#xff0c;后来被黑客组织Shadow Brokers泄露。该漏洞存在于SMBv…

Gitee Pipeline 从入门到实战【详细步骤】

文章目录 Gitee Pipeline 简介Gitee Pipeline 实战案例 1 - 前端部署输入源NPM 构建Docker 镜像构建Shell 命令执行案例 2 - 后端部署全局参数输入源Maven 构建Docker 镜像构建Shell 命令执行参考🚀 本文目标:快速了解 Gitee Pipeline,并实现前端及后端打包部署。 Gitee Pi…

【d46】【Java】【力扣】876.链表的中间结点

思路 先获得总体长度&#xff0c; 再得到中间节点 的索引&#xff0c;&#xff0c;这里的索引是从1开始的索引&#xff0c;而不是从0开始的索引(这种理解方式更简单) 排错&#xff1a;另一个思路&#xff1a;将链表都放进list&#xff0c;获得中间的数字&#xff0c;然后遍历…