单例模式 Singleton

单例模式 是一个非常复杂的话题,在此,我们尝试使用 C#了解 Unity 中 Singleton 的基础知识和各种实现。

1.概述

单例模式是一种基本的设计模式。实现 单例模式 的类将确保在任何时候都只存在对象的 唯一 实例。对于不需要在游戏过程中多次复制的内容,建议使用单例。这对于 GameManagerAudioController 等控制器类非常有用。

2.方案

在 Unity 中有几种实现 单例模式 的方式,我们将在本教程中进行一些实现:

2.1 最简单的实现

以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SingletonController : MonoBehaviour
{
public static SingletonController instance;

private void Awake()
{
if (instance != null)
{
Destroy(gameObject);
}
else
{
instance = this;
}
}
}

上面的代码是单例的最简单的实现,但是有一些问题需要我们解决

  • Singleton 在 Unity 场景中不是持久的。
  • 所有可执行代码都必须附加到层次结构中的 GameObject
  • 不建议 Controller.Instance 在任何 Awake() 方法中调用 Singleton ,因为我们不知道Awake()将通过所有脚本执行的顺序,我们可能会以 Null Reference Exception 结束。
  • 此代码仅适用于 SingletonController 类,但如果您想要另一个单例控制器,例如 AudioController ,我们必须将相同的代码复制粘贴到 AudioController 类中,并对工作进行一些小的更改,但这会导致 样板代码

以下我们依次解决这些问题:

2.2 解决问题1

问题1即:

  • Singleton 在 Unity 场景中不是持久的。

这个很容易修复,只需添加 DontDestroyOnLoad(gameObject) ,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SingletonController : MonoBehaviour
{
public static SingletonController instance;

private void Awake()
{
if (instance != null)
{
Destroy(gameObject);
}
else
{
instance = this;
DontDestroyOnLoad(gameObject);
}
}
}

以上。

2.3 解决问题2和3

问题2和3即:

  • 所有可执行代码都必须附加到层次结构中的 GameObject
  • 不建议 Controller.Instance 在任何 Awake() 方法中调用 Singleton ,因为我们不知道Awake()将通过所有脚本执行的顺序,我们可能会以 Null Reference Exception 结束。

对于这两个问题,我们必须在需要的时候创建 Singleton,这意味着我们必须在 Unity 中延迟实例化 SingletonController

以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class SingletonController : MonoBehaviour
{
private static SingletonController instance = null;

//用属性Get方法,在使用时即时创建实例
public static SingletonController Instance
{
get
{
if (instance == null)
{
instance = FindObjectOfType<SimpleSingleton>();
if (instance == null)
{
GameObject go = new GameObject();
go.name = "SingletonController";
instance = go.AddComponent<SingletonController>();
DontDestroyOnLoad(go);
}
}
return instance;
}
}

//在 Awake 时创建实例
void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(this.gameObject);
}
else
{
Destroy(gameObject);
}
}
}

上面的代码解决了两个不同的问题:

它首先在场景中搜索 SingletonController 的实例,如果没有找到 SingletonController 组件,则创建一个新的 GameObject并附加一个SingletonController 组件。因此我们不需要将 SingletonController 预先存在于场景中,并且此代码还将销毁它找到的任何其他副本。

其次,因为在这个实现中我们懒惰地实例化单例,所以现在我们不用担心空引用异常。

2.4 解决问题4

问题4即:

  • 此代码仅适用于 SingletonController 类,但如果您想要另一个单例控制器,例如 AudioController ,我们必须将相同的代码复制粘贴到 AudioController 类中,并对工作进行一些小的更改,但这会导致 样板代码

第四个问题是样板代码,所以我们需要 Singleton 的通用实现来解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class GenericSingletonClass<T> : MonoBehaviour where T : Component
{
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = FindObjectOfType<T>();
if (instance == null)
{
GameObject obj = new GameObject();
obj.name = typeof(T).Name;
instance = obj.AddComponent<T>();
}
}
return instance;
}
}

public virtual void Awake()
{
if (instance == null)
{
instance = this as T;
DontDestroyOnLoad(this.gameObject);
}
else
{
Destroy(gameObject);
}
}
}

所以有了这个类,我们可以使任何类成为单例,而无需重复代码。只需从 GenericSingletonClass 继承您的类 ,它就可以使用了。

例如:

1
2
3
4
public class MyAudioController : GenericSingletonClass<MyAudioController>
{
//...
}

以上。

3.结论

Singleton 有点复杂,但它在游戏开发中扮演着非常重要的角色。在复杂的游戏中,我们可能需要很多 Singleton,所以最好有一个 Singleton 的通用实现,以减少代码重复。第四个解决方案处理大部分问题。

同样的,我们也可以直接使用以下解决方案:

直接给出了相应的 Singleton 解决方案和示例代码。

image-20210824181402785

以上。