文章目录
- 前言
- 一、不使用单例
- 二、普通单例模式
- 三、泛型单例基类
-
- 1、不继承MonoBehaviour的单例模式基类
-
- 1.1、基本实现
- 1.2、防止外部实例化对象
- [1.3、 多线程访问单例时会遇到问题](#1.3、 多线程访问单例时会遇到问题)
- 1.3、最终代码
- 2、继承MonoBehaviour的单例模式基类
- 完结
前言
在游戏开发中,单例模式应该是我们最常见也是用的最多的设计模式了,但是你真的了解它吗?
本文通过实例分析,我们将阐述如何设计和应用单例模式,以提高代码的可维护性和复用性。无论是初学者还是经验丰富的开发者,这篇文章都将为你提供实用的技巧和深入的理解,帮助你在 Unity 项目中更有效地管理资源和对象。
一、不使用单例
为什么要是有单例?我们先来看看不使用单例的情况下如何访问不同类方法
新增TestModel ,新增Log测试方法
csharp
public class TestModel
{
public int money = 100;
public int level = 2;
public void Log(){
Debug.Log($"打印金额:{money}");
Debug.Log($"打印等级:{level}");
}
}
调用Log方法
csharp
TestModel testModel = new TestModel();
testModel.Log();
运行效果,打印日志信息
可以发现,每次调用Log方法,我们都需要先实例化TestModel。如果我们还希望TestModel 数据应该保证整个游戏只有一份的,但是现在我们可以随意实例化多份TestModel 数据,这样我们就分不清哪个才是我们要的真正的数据
单例模式就可以很好的解决这个问题,而且访问的时候也可以非常方便的访问
二、普通单例模式
1、单例模式介绍
如果要让一个类只有唯一的一个对象,则可以使用单例模式来写。使用的时候用"类名.Instance.成员名
"的形式来访问这个对象的成员。
实现步骤:
- 1、把构造函数私有化,防止外部创建对象。
- 2、提供一个属性给外部访问,这个属性就相当于是这个类唯一的对象。
单例模式分为饿汉式和懒汉式两种。
- 1、饿汉式单例模式:
在程序一开始的时候就创建了单例对象。但这样一来,这些对象就会在程序一开始时就存在于内存之中,占据着一定的内存。 - 2、懒汉式单例模式:
在用到单例对象的时候才会创建单例对象。
2、不继承MonoBehaviour的单例模式
2.1、基本实现
按前面的介绍编写代码
csharp
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方法
csharp
TestModel.Instance.Log();
运行效果,打印日志信息,和之前的一样
现在无论你如何访问,都是同一个实例
2.2、防止外部实例化对象
当然,你会发现目前还是可以通过之前非单例模式进行访问TestModel数据
csharp
TestModel testModel = new TestModel();
如果你想防止外部实例化对象,其实也很简单,只要定义私有的构造方法即可
csharp
private TestModel(){}
2.3、最终代码
csharp
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
的第一个实例。
csharp
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("打印日志:访问成功");
}
}
调用
csharp
TestUI.Instance.Log();
直接执行肯定报空引用异常错误
因为我们继承了monobehavior的脚本,所以要要挂载到游戏场景身上才有用,记得挂载脚本
运行效果
3.2、自动创建和挂载单例脚本
如果每次访问单例我们都需要手动挂载脚本,那也太麻烦了,所以一般我们都通过代码自动创建和挂载对应脚本
csharp
GameObject go = new GameObject("TestUI");//创建游戏对象
instance = go.AddComponent<TestUI>();//挂载脚本到游戏对象
3.3、切换场景不销毁单例对象
即使我们在当前场景创建了单例对象,但是切换到一个新场景,必然会销毁之前的所有对象,这样就找不到之前创建的单例对象了
我们可以使用DontDestroyOnLoad(instance);
,用于确保指定的游戏对象在加载新场景时不会被销毁。通常,当场景切换时,Unity 会销毁当前场景中的所有对象,但使用这个方法后,调用的对象(例如单例模式中的实例)将保持存在。
3.4、最终代码
csharp
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,所以使用反射
csharp
/// <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泛型单例基类即可,就不需要重复写那么多代码了
csharp
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}");
}
}
调用,调用和之前一样
csharp
TestModel.Instance.Log();
结果,正常打印
1.2、防止外部实例化对象
不过现在我们又可以通过实例化的方式直接进行访问TestModel数据
csharp
TestModel testModel = new TestModel();
我们可以和前面一样,定义私有的构造方法,防止外部实例化对象
csharp
private TestModel(){}
不过为了方便,我们通常都不这么做,因为这样每个类我们又要新增这段构造方法,完全没有必要。多人协作时,我们只需要内部沟通好,单例不要通过实例化访问即可。
1.3、 多线程访问单例时会遇到问题
我们可以使用lock
线程锁和volatile
关键字进行处理。
lock
线程锁当多线程访问时,同一时刻仅允许一个线程访问。
volatile
关键字修饰的字段,当多个线程都对它进行修改时,可以确保这个字段在任何时刻呈现的都是最新的值。
csharp
private static object locker = new object();
private volatile static T instance;
1.3、最终代码
csharp
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、基本实现
csharp
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() { }
}
使用
csharp
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("打印日志:访问成功");
}
}
调用
csharp
TestUI.Instance.Log();
效果,正常访问
2.2、切换场景不销毁单例对象
和前面一样,同样加上DontDestroyOnLoad(instance);
即可
2.3、在OnDestroy方法中访问单例对象
如果直接在在OnDestroy方法中访问单例对象
csharp
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= true
,OnDestroy
时IsExisted=false
csharp
public static bool IsExisted { get; private set; } = false;
在OnDestroy调用时,先判断IsExisted
是否为true
csharp
private void OnDestroy() {
if(TestUI.IsExisted) TestUI.Instance.Log();
}
效果,开始运行执行一次,结束运行调用OnDestroy又执行一次,且无报错
2.4、最终代码
csharp
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
一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~