单例模式(Singleton Pattern)——游戏开发常用设计模式(一)

前言

单例模式应该是在unity游戏开发中最简单最常用的设计模式之一,无论是管理全局游戏状态、处理资源加载,还是控制音频播放,单例模式都能为我们提供一个简洁的解决方案——确保一个类只有一个实例,并允许在游戏的任何地方轻松访问它。

然而,这种便利性也伴随着潜在的风险,比如代码耦合度增加、测试难度提升,甚至可能引发难以调试的多线程问题。

本文记录了我在学习单例模式中总结的重点和实用方法

本文脚本均为基于unity的C#脚本

一、简介

1.概念

单例模式是一种设计模式,它通过限制类的实例化过程,确保在整个应用程序生命周期中只有一个实例存在,并提供一个全局访问点以便在代码的任何地方都能方便地使用这个唯一实例。应用在Unity里面,可以用来作为数据类、工具类、实现类,方便脚本调用。

2.优缺点

优点:

它确保一个类只有一个实例,避免了重复创建对象的开销,从而节省了系统资源;

单例模式提供了一个全局访问点,使得在程序的任何地方都能方便地访问这个实例。

单例模式能简化代码结构,减少因多实例管理带来的复杂性。

缺点:

单例模式可能导致代码的耦合度增加,因为全局访问的特性使得其他类直接依赖于单例,降低了代码的可测试性和可维护性;

单例模式在多线程环境下可能引发线程安全问题,需要额外的同步机制来保证实例的唯一性;

单例的生命周期通常与应用程序一致,如果设计不当,可能会导致资源无法及时释放,从而引发内存泄漏等问题。

3.分类

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

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

懒汉式单例模式:在用到单例对象的时候才会创建单例对象。

在使用unity进行游戏开发时,单例模式的实现方式又分为继承MonoBehaviour和不继承MonoBehaviour

二、代码实现

1.继承MonoBehaviour的单例模式

写法一

可将此脚本挂载在任意对象上

public class UnitySingleton : MonoBehaviour{

static UnitySingleton instance;

public static UnitySingleton Instance {

get{

if(instance == null){

instance = GameObject.FindObjectOfType(typeof(UnitySingleton)) as UnitySingleton ;

}

return instance;

}

}

}

写法二

这是一种更容易理解的写法,需要将此脚本挂载在单例对象上

public class UnityManager : MonoBehaviour{

public static UnityManager Instance;

void Awake(){

if(instance == null){

intstance = this;

}

else{

Destory(gameObject);

}

}

}

写法三

切换场景不销毁单例对象

public class TestUI : MonoBehaviour

{

//定义私有的构造方法,防止外部实例化对象

private Test(){}

private static Test instance;

public static Test Instance {

get {

//保证对象的唯一性

if (instance == null){

instance = FindObjectOfType();

if(instance == null){

GameObject go = new GameObject("Test");//创建游戏对象

instance = go.AddComponent();//挂载脚本到游戏对象

}

DontDestroyOnLoad(instance);//创建不销毁的对象

}

return instance;

}

}

}

2.不继承MonoBehaviour的单例模式

public class Singleton{

static Singleton instance;

public static Singleton Instance{

get {

if (instance == null) {

instance = new Singleton();

}

return instance;

}

}

}

还有一种泛型单例基类的写法,可以提高代码复用率,因为我暂时用不到,现在这里留个坑,等以后用到了再回来补上。泛型基类的写法在这篇文章里有详细教程:【unity框架开发1】最详细的单例模式的设计和应用,继承和不继承MonoBehaviour的单例模式,及泛型单例基类的编写_unity继承monobehaviour单例模式-CSDN博客

3.使用方法

以上写法均可使用 类名.Instance.---- 进行访问

三、关于静态类和单例

静态类和单例都可以提供全局访问用来传输数据,那么他们有什么不同呢,该在什么时候用静态类什么时候用单例呢。我的理解是这样的:

在数据传输读取这方面这两种方法差不多,静态字段和单例中的public字段都可以用作传输数据,静态类在性能消耗上略优于单例。

但是静态方法和单例中的方法就不太一样了。由于静态类不能被实例化,所以静态类不能独自执行功能,必须由别的实例调用;而单例为实例,单例存在后可以独自完成某些功能。

拿扫地举例,静态类中的静态方法就像是扫帚,扫帚只有被人使用,才能扫地;而单例就像是扫地机器人,它可以自己扫地,而且人还可以随时查看扫地机器人的电量、水量等状态。

四、总结

单例虽然方便,但如果我们在一个项目中 过度使用单例模式,就会造成该项目的 耦合性非常高,一个单例中的某个变量,可能同时被十几个对象引用,导致牵一发而动全身,使得项目难以维护。

在使用unity进行游戏开发的过程中,使用单例时需要注意:

如果其它脚本与单例有依赖关系,且单例中有初始化代码在Awake或者Start方法中执行,最好手动控制单例的初始化顺序。因为在unity中多个脚本间的Start方法和Awake方法是以随机顺序进行的,手动控制单例的初始化顺序以防止空引用异常。