Unity的C#编程教程_63_单一实例 Singleton 详解及应用练习
目录:
Singleton UI Manager
Challenge: Singleton Spawn Manager
Singleton: Lazy Instantiation
1. Lazy Instantiation
2. Downfall of Lazy Instantiation
MonoSingleton
Singleton Design Pattern
- 游戏设计模式之一:单一实例模式
- 全局都可以访问的 class,该 class 仅存在一个
- 比如我们的 Manager class:Game Manager,Item Manager,Player Manager,UI Manager,Spawn Manager等
- 我们一般不用通过 GetComponent 访问,而是直接进行访问
- 使用 singleton,确保这个 class 仅有一个
创建 Game Manager 脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
创建一个 Player,挂载同名脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
private GameManager _gm;
// Start is called before the first frame update
void Start()
{
_gm = GameObject.Find("Game Manager").GetComponent<GameManager>();
// 传统方法找到对应的 GameManager 脚本,需要先找到对应的游戏对象,然后获取游戏对象下面挂载的脚本组件
}
// Update is called once per frame
void Update()
{
}
}
改成设计成 singleton:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
private static GameManager _instance;
// 生成一个静态实例,确保了仅有一个实例
public static GameManager Instance
{
get
{
if (_instance == null)
{
Debug.Log("Game Manager is null");
}
return _instance;
}
}
// 用于别的脚本与 GameManager 交互
// 这个 property 仅有一个 get 方法,确保了别的脚本只能对其只读
// 读取的就是 _instance 实例对象
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
private void Awake() // 加载脚本实例的时候进行调用
{
_instance = this; // 赋值为该游戏对象
}
public void Test()
{
Debug.Log("Testing");
}
}
在 Player 中调用:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
// private GameManager _gm;
// Start is called before the first frame update
void Start()
{
// _gm = GameObject.Find("Game Manager").GetComponent<GameManager>();
// 传统方法找到对应的 GameManager 脚本,需要先找到对应的游戏对象,然后获取游戏对象下面挂载的脚本组件
GameManager.Instance.Test();
// 使用了 singleton 后就可以直接调用了
}
// Update is called once per frame
void Update()
{
}
}
使用 singleton 的时候,我们可以让所有的游戏对象(非静态)访问 Manager 的脚本进行交互,这也是仅有的和 Manager class 交互的方式,Manager classes 之间也可以交互,但是 Manager 脚本不会访问其他的游戏对象进行交互,所以这种交互是单向的。
Singleton UI Manager
- 使用 singleton 方式创建 UI Manager
创建一个空的游戏对象,命名为 UI Manager,挂载同名脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // 需要引入 UI 的库
public class UIManager : MonoBehaviour
{
private static UIManager _instance;
// static 确保只有一个,所有游戏对象都访问这个
public static UIManager Instance // 这里的 property 也是 static
{
get
{
if (_instance == null) // 检测脚本有没有挂载到游戏对象上
{
Debug.Log("The UIManager instance is null");
}
return _instance;
}
// 这设定 property,外部脚本只读访问 _instance
}
private void Awake()
{
_instance = this; // 对实例进行赋值为该游戏对象
}
public void ShowScore(int score)
{
Debug.Log("The score is: " + score);
GameManager.Instance.Test();
// Manager classes 之间也可以相互访问
// 但是 Manager class 不访问别的游戏对象
// 仅由别的游戏对象访问 Manager class
}
}
UI Manager 脚本负责管理游戏中的所有 UI 游戏对象,整个游戏中只会有一个 UI Manager,所以可以使用 singleton 使得访问更简便。
还可以在 Manager classes 之间进行交互:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
private static GameManager _instance;
// 生成一个静态实例,确保了仅有一个实例
public static GameManager Instance
{
get
{
if (_instance == null)
{
Debug.Log("Game Manager is null");
}
return _instance;
}
}
private void Awake() // 加载脚本实例的时候进行调用
{
_instance = this; // 赋值为该游戏对象
}
public void Test()
{
Debug.Log("Testing");
}
}
Player 可以简便地访问 Manager class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
UIManager.Instance.ShowScore(77);
// 使用了 singleton 后就可以直接调用了
}
// Update is called once per frame
void Update()
{
}
}
Challenge: Singleton Spawn Manager
- 任务说明:
- 创建一个 Spawn Manager
- 将其设计为 singleton
- 创建一个 SpawnEnemy 方法,然后在 Player 脚本中进行尝试调用
创建游戏对象 Spawn Manager 并挂载同名脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpawnManager : MonoBehaviour
{
private static SpawnManager _instance;
public static SpawnManager Instance
{
get
{
if (_instance == null)
{
Debug.Log("The Spawn Manager instance is null");
}
return _instance;
}
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
private void Awake()
{
_instance = this;
}
public void SpawnEnemy()
{
Debug.Log("Spawn one Enemy");
}
}
在 Player 中调用:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
SpawnManager.Instance.SpawnEnemy();
// 使用了 singleton 后就可以直接调用了
}
// Update is called once per frame
void Update()
{
}
}
Singleton: Lazy Instantiation
1. Lazy Instantiation
- 最优的做法是,在运行应用之前声明 Manager
- 比如我们应该建立对应的 class,还有对应的游戏对象,挂载对应脚本
- 然后还需要检验 instance 是不是存在
但是我们也可以先不创建游戏对象,自然创建的脚本也不用挂载了:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // 需要引入 UI 的库
public class UIManager : MonoBehaviour
{
private static UIManager _instance;
// static 确保只有一个,所有游戏对象都访问这个
public static UIManager Instance // 这里的 property 也是 static
{
get
{
if (_instance == null) // 检测脚本有没有挂载到游戏对象上
{
GameObject obj = new GameObject("UI Manager");
// 新建一个游戏对象叫做 UI Manager
obj.AddComponent<UIManager>();
// 为该游戏对象增加组件:该脚本
}
return _instance;
}
// 这设定 property,外部脚本只读访问 _instance
}
private void Awake()
{
_instance = this; // 对实例进行赋值为该游戏对象
}
public void ShowScore(int score)
{
Debug.Log("The score is: " + score);
}
}
我们可以把原有的 UI Manager 删除,然后 Player 中照样可以调用:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
UIManager.Instance.ShowScore(88);
}
}
}
运行以后会自动创建游戏对象 UI Manager,停止运行后该游戏对象会被删除。
使用 singleton 的两大优势:
- global access conmunication
- lazily instantiate an object
2. Downfall of Lazy Instantiation
- 使用 lazy instantiation 的缺点
- 比如我们使用 Spawn Manager
- 我们会通过这个脚本去生成 Enemy,这需要我们首先由 Enemy 的 prefab,然后在 inspector 中拖拽赋值
- 如果一开始不建立 Spawn Manager 游戏对象,那就没法拖拽赋值了
- 需要我们知道 prefab 存在的文件夹,然后通过代码进行赋值,比较麻烦
MonoSingleton
- 把任何 manager class 简单转化为 singleton
之前我们制作 singleton 的时候,每一个 manager class 都需要进行初始化设定以及实例赋值,假设我们有 50 个,那就要进行 50 次类似的操作。
即然需要重复做,那我们就可以想办法进行模块化操作。
建立一个 singleton 的模版,脚本命名为 MonoSingleton:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class MonoSingleton<T> : MonoBehaviour where T: MonoSingleton<T>
// 设定为抽象类,这样就不能挂载到游戏对象上,仅作为模版使用
// <T> 设定了一个 generic type,即不同的类型都可以使用该模版,多态性
// T 继承的应用模版的时候所输入的类型
// 这里这么做的原因在于,我们不但要继承 MonoSingleton,还需要知道具体的类型
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
Debug.LogError(typeof(T).ToString()+ " has no instance");
// 显示该类没有实例化,即没有赋值游戏对象(游戏对象挂载脚本)
}
return _instance;
}
}
private void Awake()
{
_instance = this as T;
// 或者 _instance = (T)this;
Int();
// 一旦加载实例,即进指定的行初始化
// 如果没有被重写,那不执行任何操作
}
public virtual void Int() // 设定一个初始化方法
{
// 虚方法,可以选择重写,也可以不重写
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
我们尝试把 Player 的脚本来使用一下这个模版:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoSingleton<Player> // 继承自模版,类型为 Player
{
public override void Int()
{
base.Int(); // 调用默认的初始化方法,可去除
Debug.Log("Player initialized!");
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}
另外尝试创建一个 Level Manager,挂载同名脚本:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LevelManager : MonoSingleton<LevelManager>
{
public override void Int()
{
Debug.Log("Level Manager initialized");
}
public void SetLevel()
{
Debug.Log("Set a level");
}
}
通过 Player 尝试直接调用:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoSingleton<Player>
{
public override void Int()
{
Debug.Log("Player initialized!");
LevelManager.Instance.SetLevel(); // 可以直接调用
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}