一、单例模式
1.单例模式的好处
- 单例模式只会在第一次请求时被创建,不会自主的创建,可以节约内存
- 只存在一个对象进行运作,不用经历对象创建和销毁,节省性能
- 可以很轻松的链接游戏各个模块,例如你的任何类都可以轻松的调用单例类的属性和方法
2.单例模式的坏处
但是随着项目体积的越来越大,单例模式也存在很大的弊端。
- 代码耦合度上升,维护困难(滥用单例模式,例如可能存在多个文件内修改同一个变量,这时候去找错代价很大)
- 扩展难度上升(多个脚本之间使用单例类中的变量进行开发时候,后期扩展起来会很麻烦,之前涉及到的脚本使用到的单例类中的变量值多次在其他脚本文件中开发了)
综上,单例模式需要正确评估风险,清除它的好处和可能带来的弊端,合理使用单例模式!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//使用SingleTon实现泛型单例
public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
private static T instance;
public static T Instance
{
get => instance;//静态属性
}
protected virtual void Awake()
{
if (instance != null) Destroy(gameObject);
else instance = (T)this;
}
protected virtual void Ondestroy()
{
if (instance == this) instance = null;
}
}
子类继承父类单例,这时候子类也就是单例了,这时候在其他脚本文件可以使用单例类里面的变量和方法了
二、命令模式
定义:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。其实简单来说,就是将命令封装为一个对象,从而实现解耦、可改变命令对象、以及撤销功能。
自我理解:实现一个物体移动的功能,在物体类中,只写调用物体移动的方法即可,如何实现物体移动的代码是写在另一个类中,并且功能分模块化,比如上下左右四个类实现抽象基类,实现对应的抽象方法。最常见的可以使用抽象基类的应用,可以对上一步操作进行简单的回滚。也就是简单的撤销命令
三、观察者模式
代码层面,各管各的;执行层面,协同操作。也就是我们在项目中用到的事件中心,举个例子,比如说:当我们玩家血量为0的时候,动画系统需要获得动画系统的引用,播放死亡动画,需要世界系统的引用、音效系统、存档系统的引用,并触发对应的方法。在一个判断里面,我们需要多次的引用系统调用不同操作,这时候代码耦合度高,并且如果其中一个引用失败,则无法运行。这时候我们使用观察者模式可以实现解耦,不会使得代码很冗余。我们只需要在血量为0的时候,执行对应的血量为0的事件,同时在四个系统内+=注册事件,这时候四个系统其实就是观察者了,当观察血量为0的时候,就可以注册事件,执行对应的事件操作。
事件中心,EventHandler静态类
public static class EventHandler
{
//添加一个加载场景之后要执行的事件
public static event Action afterLoadSceneEvent;
public static void callbackAfterLoadScene()
{
afterLoadSceneEvent?.Invoke();
}
}
ItemManager脚本
//注册事件
private void OnEnable()
{
EventHandler.afterLoadSceneEvent += afterLoadScene;
}
//销毁事件
private void OnDisable()
{
EventHandler.afterLoadSceneEvent -= afterLoadScene;
}
//加载场景之后需要做的具体操作
private void afterLoadScene()
{
}
当ItemManager脚本激活运行的时候,这时候相对于就是观察者,当我们EventHander.afterLoadSceneEvent()在哪调用的时候,也就是触发事件中心事件的时候,与该事件所绑定的事件也会依次同步触发。
四、对象池模式
预先定义一个可包含对象的池子,在初始化池子的时候创建好对象,并设置为非激活状态。当我们创建物体时候,激活池子的对象,当我们销毁物体时,将物体的状态设置为非激活。与传统的创建销毁物体相比,对象池销毁在CPU和内存的消耗上都会有性能极大的提升。
传统的克隆物体生成方式在两个方面性能消耗远远大于线程池(对象池)技术。原因有两个,第一个是我们使用创建物体时,系统会根据大小为我们分配了对应的内存,但是由于不是一次性生成的,所以分配的内存是不连续的,当我们销毁其中的部分内存空间的时候,空出来的部分如果没有合适的新的任务进行占用,则会造成相应的性能碎片,随着性能碎片的增加,我们的内存的占用也会增加。相反的线程池技术则是一口气分配好一长串的内存,不会造成内存碎片;第二个原因是当我们销毁创建物体的计算量远远大于单纯激活物体和取消激活物体的,因此当创建销毁的物体数量达到一定规模的时候,在游戏性能上开销是十分庞大的。而线程池由于在初始化的时候就已经完成了大规模的计算,在游戏运行过程中用激活和取消激活两个操作来代替创建和销毁,因此CPU的性能也会下降。
PoolManager.CS 对象池技术
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 对象池的逻辑上使用预制体的加载方式
/// </summary>
public class PoolManager
{
private static PoolManager instance;
//缓冲池字典
private Dictionary<GameObject,List<GameObject>> poolDataDic = new Dictionary<GameObject,List<GameObject>>();
//创建根目录
private GameObject poolObj;
//对于外界来说,只能看到这层属性
public static PoolManager Instance {
get {
if(instance == null)
{
instance = new PoolManager();
}
return instance;
}
}
//每一个prefab对应一个抽屉,list对应抽屉里面的格子
public GameObject GetObj(GameObject prefab)
{
GameObject obj = null;
if(poolDataDic.ContainsKey(prefab) && poolDataDic[prefab].Count > 0)
{
//返回list中的第一个
obj = poolDataDic[prefab][0];
//从list中移除第一个
poolDataDic[prefab].RemoveAt(0);
}
//没有这种资源
else
{
//实例化一个,然后传过去
obj= GameObject.Instantiate(prefab);
}
//传出去让他显示
obj.SetActive(true);
//让其没有父物体
obj.transform.SetParent(null);
return obj;
}
/// <summary>
/// 把物体放进缓冲池
/// </summary>
/// prefab代表放进来的是什么物体,obj是放进来的具体物体
public void PushObj(GameObject prefab,GameObject obj)
{
//判断有没有根目录
if (poolObj == null) poolObj = new GameObject("PoolObj");
//判断字典中有没有预制体数据
if (poolDataDic.ContainsKey(prefab))
{
//把物体放进去
poolDataDic[prefab].Add(obj);
}
else
{
//字典中没有,就是创建预制体的缓冲池数据
poolDataDic.Add(prefab,new List<GameObject>() { obj});
}
//如果根目录下没有这个预制体命名的子物体
if (poolObj.transform.Find(prefab.name) == false)
{
//如果父物体没有找到对应的预制体名称,则当前预制体创建一个根目录
new GameObject(prefab.name).transform.SetParent(poolObj.transform);
}
//隐藏
obj.SetActive(false);
//设置父物体
obj.transform.SetParent(poolObj.transform.Find(prefab.name));//当前的预制体最外层还包含了一个原来预制体的名称,也就是PoolObj>>Sun>>Sun
}
/// <summary>
/// 清除所有数据
/// </summary>
public void Clear()
{
poolDataDic.Clear();
}
}
只要是创建(克隆)物体或者销毁物体,我们调用对象池中的GetObj()和PushObj()即可,也就是克隆出来的物体控制显示,不在对象池中,相反销毁的物体就是放入对象池中,控制物体隐藏。
五、工厂模式
1.前言:
工厂模式是一种最佳的在游戏当中创建对象的模式,当我们开发游戏时,我们通常需要创建大量的对象,有时候我们需要手动的去创建这些对象,但是往往这种方式容易出现错误,而且难以维护和扩展。在这种模式下,工厂模式就会变得非常有用。工厂模式是一种创建型设计模式,用于创建对象,将对象的创建过程封装在一个类中,以便在需要的时候创建对象。
工厂模式可以通过客户端代码与实际创建对象的过程分离来提高代码得到可维护性、可扩展性和可读性。在游戏开发中,工厂模式通常用于创建不同类型的游戏对象,例如武器、角色、道具等等,在工厂模式中,通常会使用以下三种类型:
简单工厂模式是最基本的工厂模式,在简单工厂模式中,我们只需要一个工厂类,它根据客户端的请求返回一个聚义的对象实例,简单工厂模式通常只适用于创建单一的对象类型。
工厂方法模式是一种扩展简单工厂模式的模式,它定义了一个工厂方法接口用于创建对象,这些对象由子类决定实例化哪一个类,工厂方法模式通常用于创建不同的类型对象,每个对象类型对应一个工厂方法。
抽象工厂模式是一种创建一组相关或依赖对象的工厂模式,在抽象工厂模式中,客户端请求一个工厂,并从工厂中获取一个产品族中的一个对象,在抽象工厂模式中,工厂不仅仅是一个单独的类,而是由多个工厂组成的层次结构。工厂模式具有以下优点:
工厂模式的缺点:
2.工厂模式的应用
2.1简单工厂模式
首先创建一个道具的基类以及子类,并且子类实现了自己特定的use方法
定义一个简单工厂类ItemFactory,该类包含一个用于创建道具的静态方法CreateItem,根据传入参数的不同,创建不同的道具实例并返回
最后我们可以在游戏中使用ItemFactory来创建道具实例并使用它们,通过使用简单工厂模式,我们可以将道具的创建过程封装在ItemFactory类中,使得代码更加清晰,易于维护和扩展
2.2工作方法模式
首先定义一个工厂接口ItemFactory,该接口包含了一个用于创建道具的抽象方法CreateItem,具体的工厂类实现该方法来创建具体的道具实例,我们可以根据每一种道具类型创建具体的工厂类,例如HealthPotionFactory,ManaPotionFactory,SpeedBoostFactory,它们都实现了ItemFactory接口,并且实现了自己特定的CreateItem方法来创建具体的道具实例。
最后我们可以在游戏中使用具体的工厂类来创建道具实例并使用它们
2.3抽象工厂模式
当使用抽象工厂模式时,通常会涉及一组相关产品,在游戏开发中,我们可以将不同类型的武器视为相关的产品,使用抽象工厂模式来创建不同种类的武器,我们可以定义一个抽象的武器接口Weapon,包含攻击方法attack,然后定义两种不同类型的武器(近战武器和远程武器),为了实现抽象工厂模式,我们需要为每个武器类型定义一个工厂接口,并实现相应的工厂类。然后我们定义具体的武器类,包括剑、弓、空近战武器和空远程武器
最后我们可以在游戏中使用具体的工厂类来创建武器实例并使用它们,通过使用抽象工厂模式,我们可以将不同种类的武器的创建和使用代码解耦,使得代码更加灵活、可维护。同时,当需要新增武器类型时,只需要实现相应的工厂类和武器类即可,不需要修改原有的代码。