Unity设计模式总结(持续更新)
单例模式
单例模式,即当前场景中有且只有一个的对象组件。通常单例被做成 AudioManager
,GameManager
这种功能
特性
- 保证一个类仅有一个实例,也有生命周期
- 提供一个全局访问的接口,任何地方都可以获取到
关于单例的风险:
- 如果单例使用懒汉模式,那么其初始化就是不可控的,因为在第一次使用时才会创建
- 由于可以全局访问,逻辑散落到各处,因此逻辑之间的引用就可能会非常混乱
- 因为可以通过
Instance
获取实例对象,所以内部的成员变量就会暴露出来,从而带来被修改的风险
懒汉
懒汉模式即程序延迟创建对象,只有在程序第一次调用此单例对象时才会创建,由此就导致了可能会有多个线程同时访问导致线程安全问题1
2
3
4
5
6
7
8
9
10
11
12
13
14// 非线程安全
public sealed class Singleton
{
private static Singleton instance = null;
public static Singleton Instance {
get {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
}
此处的 instance == null
在多个线程中判断并不准确,会导致创建多个实例。
1 | // 简单的线程安全写法 |
此时线程安全了,但是性能上却出现了问题,因为每次获取无论实例有没有创建都在上锁。
1 | // 双检查锁 |
当第一次判断 instance == null
的时候,有可能另外一条线程正在对其实例化,但是还没有完成,因此进入锁之后还继续判断了一次 instance == null
。
注意,此处的双检查锁实现并不能保证线程安全问题,因为 c#
中的指令重排问题:instance = new Singleton();
这行代码其实是分为了三个步骤:
- (1) 为
Singleton
分配内存空间; - (2) 调用
Singleton
的构造函数,初始化成员字段; - (3) 将
instance
对象指向分配的内存空间。
本来程序是按照 1-2-3 的指令顺序执行,但是因为指令重排优化导致其执行顺序变成了 1-3-2。那么此时就会引发一个问题:
Thread A
检查instance
为null
,此时进入第一个if
语句Thread A
获得锁Thread A
发现instance
仍然为null
,于是创建一个新对象但还未初始化- 由于指令重排,此时
instance
被赋值,但对象尚未初始化 Thread B
检查instance
不为null
,直接返回未初始化的 instance
解决办法:给 instance
加上 volatile
关键字避免对其指令重排优化1
private static volatile Singleton instance = null;
饿汉
饿汉就是在程序一开始就初始化唯一实例,不会有线程安全问题1
2
3
4
5
6
7
8
9
10public sealed class Singleton
{
private static readonly Singleton instance = new Singleton();
public static Singleton Instance {
get {
return instance;
}
}
}
MonoBehaviour
对于 unity
的 MonoBehaviour
,因为其本来就是单线程的,因此也不用考虑多线程资源争抢的问题。可以直接实现一个对于其的泛型单例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
public static T Instance { get; private set; }
protected virtual void Awake() {
if (Instance != null && Instance != this) {
// Destroy(gameObject);
Debug.LogError("Multiple instances of Singleton detected. Please fix this issue.");
} else {
Instance = (T)this;
// DontDestroyOnLoad(gameObject);
}
}
public static bool IsInitialized() {
return Instance != null;
}
protected virtual void OnDestroy() {
if (Instance == this) {
Instance = null;
}
}
}
其中有两处为根据自己需求的可选项:
- 在发现有多个实例的时候销毁或者直接LogError崩掉
- 可以让此单例对象在加载场景时是否销毁
观察者模式
观察者模式是一种一对多的依赖关系,使得当一个对象(被观察者)的状态发生变化的时候,所有依赖于它的对象(观察者)都会得到通知并自动更新。观察者模式有助于实现对象之间的解耦。
核心思想
弱化两个对象之间的耦合关系,观察者仅在主题中心事件改变时才参与活动。其目的就是使得关注游戏的一部分的所有代码集中到一起,降低各功能之间的耦合性。
实现原理
Subject
(主题):主题是一个接口,定义用于添加,删除和通知观察者的方法。ConcreteSubject
(具体主题):实现了主题接口的类,其会维护一个观察者列表,并实现通知观察者的方法。Observer
(观察者):观察者是一个接口,定义用于更新观察者的方法ConcreteObserver
(具体观察者):实现了观察者接口的类,实现在被通知时更新自己的状态的方法。
适用场景
- 当一个对象需要通知其他对象,但并不知道这些对象是谁时。比如:事件分发管理器,当用户进行一系列输入时(如键盘),将事件分发给添加了事件监听的对象。
unity 中的应用
比如角色收到伤害会有一些什么反应:例如增加 buff
,暂时进入无敌,更新血量显示之类的事件。那么我们就可以将角色对象看作一个 Subject
,定义具体发生事件效果为 Observer
。在程序开始,就将其添加到 Subject
中角色受伤事件的观察者列表当中。角色受伤时,只需执行 Notify(TakeDamageEvent)
,即可广播给所有注册了这个事件的 Observer
对象。
缺点
- 如果观察者和被观察者之间存在循环依赖,可能导致系统崩溃。
- 通知观察者的顺序无法保证。