# XXZFramework **Repository Path**: mengtest/XXZFramework ## Basic Information - **Project Name**: XXZFramework - **Description**: 一个简单的开源的Unity前端框架 - **Primary Language**: C# - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 4 - **Created**: 2021-09-23 - **Last Updated**: 2021-09-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # XXZFramework #### 介绍 一个简单的开源的Unity前端框架 #### 软件架构 目前框架一共十一个模块: 1.单例模式基类 2.对象池 3.事件中心处理池子 4.Mono公共生命周期 5.场景管理器 6.资源加载模块 7.结合资源管理器版的对象池 8.输入控制模块 9.音频管理模块 10.UGUI框架 11.热更新模块 #### 安装教程 1. Unity2019.4.2f1LTS 2. VsualStudio2019 #### 使用说明 1. 直接将这套框架包package导入到项目文件夹中即可 ### 前序: 目前只是很粗糙的简单形式版本的模块,供大家学习或者参考,再在此基础上根据项目的需求进行不同程度的拓展 ### 一.单例模式基类 #### 0.单例基类的介绍 ##### 0.1什么是单例? 一句话说就是创建一个全局变量,它具有唯一职责(单一职责原则),统一负责需要它处理的需求。 ##### 0.2为什么需要单例模式? 我们都知道,值类型跟引用类型都是有生命周期的(它们所被包裹的大括号"{}"),一旦离开了它们自身生命周期,那么就会被当成垃圾,等待下次GC的时候被回收。但是,因为某些原因,我们并不希望我们的这个类被系统自动回收,我们需要它与程序同生共死(从程序运行到程序结束),而且它在运行期间必须是唯一的(不能被new,不能重新对其赋值),所以单例就此诞生! ##### 0.3为什么需要单例基类? 首先,做过游戏的都知道,我们的游戏中是有许许多多的单例管理器(XXXManager),它们内部代码除了处理其自身业务代码外,其他的实例化方式高度相似,那么根据面向对象三大原则之一:继承,我们就可以把相似的代码抽离出来成为父类,子类继承了父类就会继承了父类的成员变量跟方法,这样,我们就可以减少编写重复代码的时间,提高开发效率。 ##### 0.4单例模式的利与弊? ###### 单例模式的优点: 按我个人的使用来讲,有了单例模式,我可以避免重复重新创建与销毁类;我可以拥有一个唯一的全局的类,而且由于系统内存中只存在这一个实例类,所以节约了内存。 ###### 单例模式的缺点 单例模式的缺点可能就是一直占用了内存却不能被清空销毁吧。 #### 1.单例模式基类 BaseSingleton.cs: ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; /// /// 不继承MonoBehaviour的单例基类 /// /// 类型 public class BaseSingleton where T : class, new() { /* * 我们的单例,为了外部不直接使用进行一些错误的操作, * 我们只会用成员属性提供一个get方法,而不提供set方法 * 因为如果我们设置为Public,那么外部是可以进行下面这句语句的: * BaseSingleton._instance = new BaseSingleTon(); * 上面这句是错误的,因为我们知道单例因该是唯一性,不可实例化的,还有不能修改引用的 * 还有可能有的人会说,我们可以加个private BaseSingleTon(){}这样的私有构造函数,这样外部就无法实例化了 * 但是,我们同理也顶替了我们内部本有的公共无参构造函数了,而我们的泛型约束T:new()是需要一个公共无参构造函数的 * 如果我们的子类再去继承这个单例类,就会报错了。 */ private static T _instance; // 提供给外界访问的属性 public static T GetInstance { // 供外界获得到我们的单例实例 get { // 其实这一块如果是多线程开发的话是需要有双锁的,但是,我们Unity是单主线程(生命周期)。 if (_instance == null) { // 对单例对象进行赋值 _instance = new T(); } // 返回我们的单例 return _instance; } } } ~~~ #### 1.继承MonoBehaviour的单例基类 MonoBaseSingleton.cs: ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; /// /// 我们的游戏中可能会遇到一些需要继承MonoBehaviour的游戏单例基类 /// 那么首先抛出问题: /// 1.继承MonoBehaviour的类是需要拖拽到场景中的游戏对象上才可以被运行的 /// 2.如果我们场景中有成千上百个游戏对象,我们怎么拖拽,一个一个拖拽太麻烦,我们能否使用代码帮助实现 /// 3.我们的单例物体都是一些空物体,名字为XXXManager, /// 那么如果我们有成千上百个Manager我们是不是要在显示面板上创建成千上百个Manager的空物体? /// 那么能否通过代码解决这个问题? /// /// 类的类型 public class MonoBaseSingleton : MonoBehaviour where T : MonoBehaviour { // 我们唯一的单例实例 private static T _instance; // 提供一个外部访问得到的属性 public static T GetInstance { // get方法,返回一个我们的单例 get { /* * 如果为空我们就进行赋值 * 但是,我们继承了MonoBehaviour的类是无法直接new()出来的,Unity的内部帮助我们实现了对象的实例化操作( * 如果想了解可以看看反射,Unity底层就是基于反射实现的) */ if (_instance == null) { // 我们先手动实例化出一个物体,在把我们的游戏物体生成到我们的场景中 GameObject obj = new GameObject(); // 再把我们生成出来的游戏对象的名字进行修改成我们需要的名字 obj.name = typeof(T).ToString() + "Manager"; // 把我们的XXXManager物体添加上对应的管理器单例类 _instance = obj.AddComponent(); // 我们的单例管理器是不需要过场景之后就被移除的,所以我们要加上这句代码保护我们的单例管理器 DontDestroyOnLoad(obj); } return _instance; } } } ~~~ ### 二.对象池(缓存池) #### 0.对象池的介绍 ##### 0.1什么是对象池? 一句话说就是管理我们场景不需要(暂时不需要激活)的物体,在适当的时机激活或者失活它们。 ##### 0.2为什么需要有对象池? 游戏中需要频繁的用到许许多多的物体,举个例子,我们常见的传统MMOARPG中一般野外场景是在一片区域内生成许许多多的怪物,玩家接取到击杀小怪的任务,然后去击杀,如果按我们最简单的方式,需要生成小怪的时候,我们就GameObject.Instantiate()生成,销毁的时候我们就直接DestroyImmediate(),那么这时候就会给我们的GC造成压力,我们会频繁GC回收我们实例化又死亡的怪物的内存,这时候就会造成一个情况就是玩家卡顿!这是很影响玩家体验感的一种巨大的问题!所以,对象池就应运而生了。我们会在场景加载完毕的时候,预先加载完且实例化出部分游戏对象,在玩家击杀了游戏对象的时候我们只是对其进行失活操作,而不直接销毁。 ##### 0.3对象池的优与弊 ###### 对象池的优点 减少频繁的GC,减少玩家卡顿感,而且对象池这种东西在开发中很常见也十分的重要,对象池的变种十分的多,但是最核心的思想还是没变的。 ###### 对象池的缺点 对象池的话,我们需要预先实例化一部分怪物,这需要占用着我们的一部分内存而且不被释放。 #### 1.简单的对象池模板 前期准备: 1.创建一个Resources文件夹 2.创建一个脚本,脚本名字为ObjectPool 3.在显示面板上创建两个任意的物体(创建两个Cube,一个命名就为"Cube",另一个随便命名,两个不重名就行),然后拖拽进Resources文件夹中做成预制体 BaseSingleton.cs: ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; public class BaseSingleton where T : class, new() { private static T _instance; public static T GetInstance { get { if (_instance == null) { _instance = new T(); } return _instance; } } } ~~~ ObjectPool.cs: ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; using System.Collections.Generic; /// /// 对象池模块 /// public class ObjectPool : BaseSingleton { // 创建我们的对象池池子 // 参数 1 我们存储对象的容器名字,参数 2 我们真正用于存储对象的容器 public Dictionary> objPool = new Dictionary>(); /// /// 使用完之后返还对象池子里面 /// /// 需要返还的游戏对象 public void PushObj(GameObject obj) { // 第一步.先把对象失活了,这条语句十分重要,不然我们的游戏对象即使回收进了对象池子里面,一样还是会存在于场景之中。 obj.SetActive(false); // 查找对象池子里面是否存在缓存我们对象容器 if (objPool.ContainsKey(obj.name)) { // 把使用完的对象添加进容器池子里面(返还对象池子) objPool[obj.name].Add(obj); } else { // 如果我们的对象池子里面没有存在可以用于缓存游戏对象的容器,那么就需要创建一个,再把要返还的对象存进去 objPool.Add(obj.name, new List() { obj }); } } /// /// 得到我们的对象池子中缓存的对象 /// 1.如果不存在对象的容器就创建一个容器并且生成我们预先设好的数量 /// 2.如果存在对象的容器但是容器内部缓存的游戏对象数量小于等于0,那么一样生成我们预先设好的数量 /// /// 生成游戏物体后的名字 /// 生成游戏物体的路径(Resources文件夹下游戏物体的路径) /// public GameObject GetObj(string objName, string objPath) { // obj是用于我们获取到生成后的游戏物体,方便我们对它们进行改名字,修改位置等等 GameObject obj = null; // 查看对象池子中是否存在缓存我们游戏对象的容器 if (!objPool.ContainsKey(objName)) { // 如果不存在就生成一个新的容器 objPool.Add(objName, new List()); } // 如果容器的数量小于等于0的时候,我们就预先初始化一小部分游戏对象 if (objPool[objName].Count <= 0) { for (int i = 0; i < 5; i++) { // 生成游戏对象 obj = GameObject.Instantiate(Resources.Load(objPath)); /* * 修改游戏对象的名字, * 因为新生成的游戏对象会在显示面板上会加上(clone),这很不方便我们查阅我们的对象。 * 所以我们需要对生成的游戏对象改变其名字为我们想要的名字方便我们管理 */ obj.name = objName; // 这句语句十分重要,我们预先生成的物体是不能暴露到场景中的 obj.SetActive(false); // 再把新生成的游戏物体添加进我们的容器中 objPool[objName].Add(obj); } } // 把我们容器的第一个物体给予出去 obj = objPool[objName][0]; // 再移出我们给出去的游戏物体,RemoveAt会帮助我们把空出来的位置进行重新排序 objPool[objName].RemoveAt(0); // 在把移出去的游戏对象重新激活 obj.SetActive(true); // 其实可以不需要返回这个物体的,但是为了方便后期项目可能需要对我们的游戏物体进行操作 // 所以我们就提供返回游戏物体,方便外部修改游戏物体的游戏参数 return obj; } /// /// 清空对象池 /// public void Clear() { objPool.Clear(); } } ~~~ #### 2.对象池第一次优化 ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; using System.Collections.Generic; /* 优化方向: * 1.我们可以明显的看见,我们创建的物体一直在显示面板上面,没有很好的整理的, * 那么我们需要优化的第一个方向就是,如何像文件夹一样管 *理好我们的对象池, * 例如:我们可以在显示面板上创建一个Pool空物体,然后让它们管理我们实例化出来的Cube和Sphere物体, * 这样是不是就很*方便我们查看哪些是还在对象池里面的,哪些是出了对象池的,也把抽象化的对象池子,具体到了我们的显示面板上。 */ /// /// 对象池模块 /// public class ObjectPool : BaseSingleton { // 创建我们的对象池池子 // 参数 1 我们存储对象的容器名字,参数 2 我们真正用于存储对象的容器 public Dictionary> objPool = new Dictionary>(); // 我们需要挂载的根节点 private GameObject poolRoot = null; /// /// 使用完之后返还对象池子里面 /// /// 需要返还的游戏对象 public void PushObj(GameObject obj) { // 第一步.先把对象失活了,这条语句十分重要,不然我们的游戏对象即使回收进了对象池子里面,一样还是会存在于场景之中。 obj.SetActive(false); // 判断我们显示面板上是否有我们用于管理对象池的空物体(Pool) if(poolRoot == null) poolRoot = new GameObject("Pool"); // 查找对象池子里面是否存在缓存我们对象容器 if (objPool.ContainsKey(obj.name)) { // 把使用完的对象添加进容器池子里面(返还对象池子) objPool[obj.name].Add(obj); } else { // 如果我们的对象池子里面没有存在可以用于缓存游戏对象的容器,那么就需要创建一个,再把要返还的对象存进去 objPool.Add(obj.name, new List() { obj }); } // 在显示面板上,把我们的物体放进根节点上 obj.transform.parent = poolRoot.transform; } /// /// 得到我们的对象池子中缓存的对象 /// 1.如果不存在对象的容器就创建一个容器并且生成我们预先设好的数量 /// 2.如果存在对象的容器但是容器内部缓存的游戏对象数量小于等于0,那么一样生成我们预先设好的数量 /// /// 生成游戏物体后的名字 /// 生成游戏物体的路径(Resources文件夹下游戏物体的路径) /// public GameObject GetObj(string objName, string objPath) { // obj是用于我们获取到生成后的游戏物体,方便我们对它们进行改名字,修改位置等等 GameObject obj = null; // 查看对象池子中是否存在缓存我们游戏对象的容器 if (!objPool.ContainsKey(objName)) { // 如果不存在就生成一个新的容器 objPool.Add(objName, new List()); } // 如果容器的数量小于等于0的时候,我们就预先初始化一小部分游戏对象 if (objPool[objName].Count <= 0) { for (int i = 0; i < 5; i++) { // 生成游戏对象 obj = GameObject.Instantiate(Resources.Load(objPath)); /* * 修改游戏对象的名字, * 因为新生成的游戏对象会在显示面板上会加上(clone),这很不方便我们查阅我们的对象。 * 所以我们需要对生成的游戏对象改变其名字为我们想要的名字方便我们管理 */ obj.name = objName; // 这句语句十分重要,我们预先生成的物体是不能暴露到场景中的 obj.SetActive(false); // 然后把我们生成的物体放进我们显示面板上Pool空物体中方便管理 // 放进去之前,我们需要判断一下我们的Pool空物体是否已经存在 if (poolRoot == null) poolRoot = new GameObject("Pool"); obj.transform.parent = poolRoot.transform; // 再把新生成的游戏物体添加进我们的容器中 objPool[objName].Add(obj); } } // 把我们容器的第一个物体给予出去 obj = objPool[objName][0]; // 再移出我们给出去的游戏物体,RemoveAt会帮助我们把空出来的位置进行重新排序 objPool[objName].RemoveAt(0); // 再继续把移出去的游戏对象重新激活 obj.SetActive(true); // 让它在显示面板上断开与父物体的联系 obj.transform.parent = null; // 其实可以不需要返回这个物体的,但是为了方便后期项目可能需要对我们的游戏物体进行操作 // 所以我们就提供返回游戏物体,方便外部修改游戏物体的游戏参数 return obj; } /// /// 用于过场景之后清空对象池 /// public void Clear() { // 清空对象池内部的键值 objPool.Clear(); // 把显示面板上的对象池的子物体全部清空 poolRoot = null; } } ~~~ #### 3.对象池第二次优化 ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; using System.Collections.Generic; /* 优化方向: *2.我们的对象池是用List去装载我们的游戏物体的,这其实有点违背了我们面向对象的思想,现在我们需要把List换成一个类PoolData,去优化 *我们的对象池。 */ /// /// 对象池中的容器模块 /// public class PoolData { /* * 该类中我们只需要考虑下面的问题: * 我们已经有了GetObj()与Push()方法,但是我们现在需要把List换成我们的PoolData,更加符合我们面向对象思想, * 那么我们就需要把这两个方法内部具体的实现抽离出来,成为PoolData中供对象池调用的行为。 */ // 这是我们的用来存储缓存对象的容器 private List objContainer = null; /// /// 我们的构造函数,用于 /// public PoolData() { if(objContainer == null) objContainer = new List(); } public void PushObjInContainer(GameObject obj) { // 放回对象池子的容器 // 第一步:把对象先失活,让其从场景中消失 obj.SetActive(false); // 第二步:把对象添加进对象容器中 objContainer.Add(obj); } public GameObject GetObjFromContainer() { // 移出对象池子的容器 // 第一步:我们需要先创建一个GameObject对象用来存储我们移出的物体 GameObject obj = objContainer[0]; // 第二步:把我们的对象从显示面板上激活出来 obj.SetActive(true); // 第三步:把我们的对象从我们的容器中移出 objContainer.RemoveAt(0); // 第四步:返回出去供外部使用 return obj; } /// /// 用于返回我们容器内部缓存的对象数量 /// /// 容器内部的对象数量 public int ContainerCount() { return objContainer.Count; } } /// /// 对象池模块 /// public class ObjectPool : BaseSingleton { // 创建我们的对象池池子 // 参数 1 我们存储对象的容器名字,参数 2 我们真正用于存储对象的容器 public Dictionary objPool = new Dictionary(); // 我们需要挂载的根节点 private GameObject poolRoot = null; /// /// 使用完之后返还对象池子里面 /// /// 需要返还的游戏对象 public void PushObj(GameObject obj) { // 判断我们显示面板上是否有我们用于管理对象池的空物体(Pool) if(poolRoot == null) poolRoot = new GameObject("Pool"); // 查找对象池子里面是否存在缓存我们对象容器 if (objPool.ContainsKey(obj.name)) { // 把使用完的对象添加进容器池子里面(返还对象池子) objPool[obj.name].PushObjInContainer(obj); } else { // 如果我们的对象池子里面没有存在可以用于缓存游戏对象的容器,那么就需要创建一个,再把要返还的对象存进去 objPool[obj.name] = new PoolData(); objPool[obj.name].PushObjInContainer(obj); } // 在显示面板上,把我们的物体放进根节点上 obj.transform.parent = poolRoot.transform; } /// /// 得到我们的对象池子中缓存的对象 /// 1.如果不存在对象的容器就创建一个容器并且生成我们预先设好的数量 /// 2.如果存在对象的容器但是容器内部缓存的游戏对象数量小于等于0,那么一样生成我们预先设好的数量 /// /// 生成游戏物体后的名字 /// 生成游戏物体的路径(Resources文件夹下游戏物体的路径) /// public GameObject GetObj(string objName, string objPath) { // obj是用于我们获取到生成后的游戏物体,方便我们对它们进行改名字,修改位置等等 GameObject obj = null; // 查看对象池子中是否存在缓存我们游戏对象的容器 if (!objPool.ContainsKey(objName)) { // 如果不存在就生成一个新的容器 objPool.Add(objName, new PoolData()); // 然后因为容器是空的,所以我们预先生成一部分游戏对象,再把它们都存储进我们的容器中 for (int i = 0; i < 5; i++) { obj = GameObject.Instantiate(Resources.Load(objPath)); if (poolRoot == null) poolRoot = new GameObject("Pool"); obj.name = objName; obj.transform.parent = poolRoot.transform; objPool[objName].PushObjInContainer(obj); } } // 如果容器内部缓存对象的数量小于等于0的时候,我们就继续预先实例化一小部分游戏对象 if (objPool[objName].ContainerCount() <= 0) { for (int i = 0; i < 5; i++) { // 生成游戏对象 obj = GameObject.Instantiate(Resources.Load(objPath)); if (poolRoot == null) poolRoot = new GameObject("Pool"); obj.name = objName; obj.transform.parent = poolRoot.transform; // 再把新生成的游戏物体添加进我们的容器中 objPool[objName].PushObjInContainer(obj); } } obj = objPool[objName].GetObjFromContainer(); // 让它在显示面板上断开与父物体的联系 obj.transform.parent = null; return obj; } /// /// 用于过场景之后清空对象池 /// public void Clear() { // 清空对象池内部的键值 objPool.Clear(); // 把显示面板上的对象池的子物体全部清空 poolRoot = null; } } ~~~ #### 4.对象池第三次优化 ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; using System.Collections.Generic; using UnityEngine.Events; /* 优化方向: * 1.我们可以明显的看见,我们创建的物体一直在显示面板上面,没有很好的整理的,那么我们需要优化的第一个方向就是,如何像文件夹一样管理好我们的对象池, * 例如:我们可以在显示面板上创建一个Pool空物体,然后再在Pool上创建对应对象名字的空物体,一个Cube空物体,一个Sphere空物体, * 让它们管理我们实例化出来的Cube和Sphere物体,这样就可以很方便的, * 让我们查看哪些是还在对象池里面的,哪些是出了对象池的,也把抽象化的对象池子,具体到了我们的显示面板上。 * 2.我们的对象池是用List去装载我们的游戏物体的,这其实有点违背了我们面向对象的思想,现在我们需要把List换成一个类PoolData,去优化我们的对象池。 */ /// /// 对象池中的容器模块 /// public class PoolData { /* * 该类中我们只需要考虑下面的问题: * 我们已经有了GetObj()与Push()方法,但是我们现在需要把List换成我们的PoolData,更加符合我们面向对象思想, * 那么我们就需要把这两个方法内部具体的实现抽离出来,成为PoolData中供对象池调用的行为。 */ // 这是我们的用来存储缓存对象的容器 private List objContainer = null; // 这是我们用于管理我们容器缓存对象的父节点 private GameObject fatherRoot = null; /// /// 我们的构造函数,用于初始化我们的成员 /// /// /// 我们容器空物体放置的节点 /// /// 生成游戏物体后的用于存储的容器名字 public PoolData(GameObject poolRoot, string objName) { if(objContainer == null) objContainer = new List(); if (fatherRoot == null) { fatherRoot = new GameObject(objName); fatherRoot.transform.parent = poolRoot.transform; } } public void PushObjInContainer(GameObject obj) { // 放回对象池子的容器 // 第一步:把对象先失活,让其从场景中消失 obj.SetActive(false); // 第二步:把对象添加进对象容器中 objContainer.Add(obj); // 第三步:把在显示面板上已经失活的对象放进我们的管理物体中 obj.transform.parent = fatherRoot.transform; } public GameObject GetObjFromContainer() { // 移出对象池子的容器 // 第一步:我们需要先创建一个GameObject对象用来存储我们移出的物体 GameObject obj = objContainer[0]; // 第二步:把我们的对象从显示面板上激活出来 obj.SetActive(true); // 第三步:把我们的对象从我们的容器中移出 objContainer.RemoveAt(0); // 第四步:把我们的对象从我们的父物体上断开联系 obj.transform.parent = null; // 第五步:返回出去供外部使用 return obj; } /// /// 用于返回我们容器内部缓存的对象数量 /// /// 容器内部的对象数量 public int ContainerCount() { return objContainer.Count; } } /// /// 对象池模块 /// public class ObjectPool : BaseSingleton { // 创建我们的对象池池子 // 参数 1 我们存储对象的容器名字,参数 2 我们真正用于存储对象的容器 public Dictionary objPool = new Dictionary(); // 我们需要挂载的根节点 private GameObject poolRoot = null; /// /// 使用完之后返还对象池子里面 /// /// 需要返还的游戏对象 public void PushObj(GameObject obj) { // 判断我们显示面板上是否有我们用于管理对象池的空物体(Pool) if(poolRoot == null) poolRoot = new GameObject("Pool"); // 查找对象池子里面是否存在缓存我们对象容器 if (objPool.ContainsKey(obj.name)) { // 把使用完的对象添加进容器池子里面(返还对象池子) objPool[obj.name].PushObjInContainer(obj); } else { // 如果我们的对象池子里面没有存在可以用于缓存游戏对象的容器,那么就需要创建一个,再把要返还的对象存进去 objPool[obj.name] = new PoolData(poolRoot, obj.name); objPool[obj.name].PushObjInContainer(obj); } } /// /// 得到我们的对象池子中缓存的对象 /// 1.如果不存在对象的容器就创建一个容器并且生成我们预先设好的数量 /// 2.如果存在对象的容器但是容器内部缓存的游戏对象数量小于等于0,那么一样生成我们预先设好的数量 /// /// 生成游戏物体后的名字 /// 生成游戏物体的路径(Resources文件夹下游戏物体的路径) /// 因为我们已经没有返回值了,如果需要对我们容器中的物体进行操作的话,我们就需要通过委托的函数把 /// 物体传出去 public void GetObj(string objName, string objPath, UnityAction callback) { // 判断我们显示面板上是否有我们用于管理对象池的空物体(Pool) if (poolRoot == null) poolRoot = new GameObject("Pool"); // 查看对象池子中是否存在缓存我们游戏对象的容器 if(objPool.ContainsKey(objName) && objPool[objName].ContainerCount() > 0) callback(objPool[objName].GetObjFromContainer()); // 查看对象池中存在的对象的数量是否等于0 else if (objPool.ContainsKey(objName) && objPool[objName].ContainerCount() == 0) { // 首先,生成游戏对象,再把新生成的游戏物体添加进我们的容器中 ResourcesMgr.GetInstance.LoadAysncResources(objPath, (obj) => { // 更改名字 obj.name = objName; callback(obj); }); } // 如果不存在就生成一个新的容器 else if (!objPool.ContainsKey(objName)) { objPool.Add(objName, new PoolData(poolRoot, objName)); ResourcesMgr.GetInstance.LoadAysncResources(objPath, (obj) => { // 更改名字 obj.name = objName; callback(obj); }); } } /// /// 用于过场景之后清空对象池 /// public void Clear() { // 清空对象池内部的键值 objPool.Clear(); // 把显示面板上的对象池的子物体全部清空 poolRoot = null; } } ~~~ #### 5.对象池还可以怎么优化 1.UnityAction配合Resources的异步加载资源(后面介绍完资源管理器后会继续优化外面现有的对象池)。 2.我们的对象池容器没有设置上限,所以我们的对象池是无限的生成且存储游戏物体,没有限制的话,是十分耗费我们的内存的。 ### 三.事件中心处理池(观察者模式) #### 0.事件中心处理池的介绍 ##### 0.1事件中心处理池 一句话说就是管理我们的所有对象中对于同一个事件的发生需要执行不同的反馈。 ##### 0.2为什么需要有事件中心处理池 解耦合、解决复杂的代码逻辑书写、易拓展高效率、方便后期项目大了统一管理要处理的事件。 ##### 0.3事件中心处理池的优与弊 ###### 事件中心处理池的优点: 我们可以不再使用transform.find()找到我们需要的物体,然后getcompent<>()得到其身上挂载的组件,然后再去.出来我们需要的方法。如果游戏场景场景中,我们的BOSS死了,那么我们如果有多少个脚本需要执行BOSS死亡后的方法,就需要重复上面那一句话里面的所用到的API很多次,这首先是有非常非常大的一个耦合性,不同的脚本之间相互关联了起来,一旦某个脚本丢失引用,将对工程直接造成致命的打击。而使用了我们的事件中心池子就是一种解耦合的方式,我们的物体不需要再去直接的执行某个函数,需要执行的函数统统都可以添加进我们的事件中心池子之中,建立监听,事件中心池子则作为我们的观察者,一旦某个事件触发了,我们就给监听了这个事件的函数配发事件。 ###### 事件中心处理池的缺点: 我们都知道任何被static修饰的变量都是存储在静态内存区的,而且静态变量具有唯一性且与程序同生共死,会在程序一直运行的时候占用我们的内存空间,但是,世界上是没有一个完美的银弹的,想要高效率就可能要牺牲某个方面,其实是个单例都会有这个问题,主要的对于事件中心的来说,事件中心池子里面存储的Value是一个委托函数,委托函数在C/C++里面像函数指针,因为C#禁止对指针的操作,才出来了这个玩意,在C++里面相当于callback。我的个人理解中,委托函数存储了建立监听的对象的引用,一旦我们忘记或者没有清除我们的引用(例如:我们的主角死了,那么他就需要移除对怪物死亡的监听了),就会造成严重性的内存泄漏问题,其次后面我们对象池子越来越复杂的时候,会出现装箱拆箱的操作(例如:我们怪物死了,那是小怪死了?精英怪死了?大BOSS死了?我们如何区别呢?)。 #### 1.事件中心处理池 EventPool.cs: ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; using System.Collections.Generic; using UnityEngine.Events; /// /// 事件中心处理池子 /// public class EventPool : BaseSingleton { // 用于处理事件派发的事件中心池子 private Dictionary> eventPool = new Dictionary>(); /// /// 在我们的事件中心池子中建立监听 /// /// 监听的事件 /// 配发的方法(我们收到事件触发时候需要处理的函数) public void AddEventListener(string name, UnityAction callback) { // 判断我们的事件中心池子中是否存在该事件 if (eventPool.ContainsKey(name)) { // 如果存在就直接建立监听 eventPool[name] += callback; } else { // 如果不存在就添加一个新事件,并且建立监听 eventPool.Add(name, callback); } } /// /// 移除事件监听 /// 注意:移除监听函数,我们因该放在MonoBehaviour生命周期函数中OnDestory()方法中, /// 如果不是继承了MonoBehaviour的类我们需要在适当的时机进行移除,不过我们需要在场景显示的 /// 物体都必须是MonoBehaviour,如果遇到特殊的,再进行特殊处理就行了。 /// /// 事件的名字 /// 监听的函数 public void RemoveEventListener(string name, UnityAction callback) { if (eventPool.ContainsKey(name)) { eventPool[name] -= callback; } } /// /// 配发事件(执行监听事件的所有函数) /// /// 事件的名字 public void TriggerEvent(string name, object o) { if(eventPool.ContainsKey(name) && eventPool[name] != null) eventPool[name].Invoke(o); } /// /// 这个函数是当我们过场景的时候帮助我们清空事件中心池 /// public void Clear() { eventPool.Clear(); } } ~~~ 测试我们的事件中心对象池: 1.场景中创建两个空物体一个叫Player,另一个叫Monster。 2.创建两个脚本分部拖拽到各自物体上,脚本命名为Player,另一个为Monster。 Player.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; public class Player : MonoBehaviour { private void Awake() { EventPool.GetInstance.AddEventListener("MonsterDead", MonsterDeadToDo); } void MonsterDeadToDo() { Debug.Log("怪物死亡,玩家获胜!"); } private void OnDestroy() { EventPool.GetInstance.RemoveEventListener("MonsterDead", MonsterDeadToDo); } } ~~~ Monster.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; public class Monster : MonoBehaviour { private void Start() { Dead(); } private void Dead() { Debug.Log("怪物死亡!"); EventPool.GetInstance.TriggerEvent("MonsterDead"); } } ~~~ #### 2.事件中心处理池的第一次优化 ~~~C# /* * 优化方向:我们现在的事件中心处理池子很蠢,它只能处理简单的事件,一旦事件复杂了就需要不停的添加事件。 * 举例:我们怪物死了,那是小怪死了?精英怪死了?大BOSS死了?我们如何区别呢?肯定会有人说, * 那么我们就加三个事件:小怪死亡的事件,精英怪死亡的事件,Boos死亡的事件,然后, * 所有监听怪物死亡的对象里面也添加小怪死亡后要处理啥啥啥的函数,精英怪死亡后要处理啥啥啥的函数,Boss死亡后要处理啥啥啥的函数,是不是光听着就很累了, * 我要是怪物的类型不止小怪、精英怪、BOSS,我还有飞行怪物,水下怪物,我BOSS还根据不同属性划分不同的大BOSS掉落的东西也不一样,那么你觉得这个方法还可靠了嘛? * 接下来我们就按最简单版的方式解决我们的问题 */ // 我们只需要对我们事件中心处理池的委托加一个泛型,泛型的类型为万物基类Object即可解决,以下是代码: /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; using System.Collections.Generic; using UnityEngine.Events; /// /// 事件中心处理池子 /// public class EventPool : BaseSingleton { // 用于处理事件派发的事件中心池子 private Dictionary> eventPool = new Dictionary>(); /// /// 在我们的事件中心池子中建立监听 /// /// 监听的事件 /// 配发的方法(我们收到事件触发时候需要处理的函数) public void AddEventListener(string name, UnityAction callback) { // 判断我们的事件中心池子中是否存在该事件 if (eventPool.ContainsKey(name)) { // 如果存在就直接建立监听 eventPool[name] += callback; } else { // 如果不存在就添加一个新事件,并且建立监听 eventPool.Add(name, callback); } } /// /// 移除事件监听 /// 注意:移除监听函数,我们因该放在MonoBehaviour生命周期函数中OnDestory()方法中, /// 如果不是继承了MonoBehaviour的类我们需要在适当的时机进行移除,不过我们需要在场景显示的 /// 物体都必须是MonoBehaviour,如果遇到特殊的,再进行特殊处理就行了。 /// /// 事件的名字 /// 监听的函数 public void RemoveEventListener(string name, UnityAction callback) { if (eventPool.ContainsKey(name)) { eventPool[name] -= callback; } } /// /// 配发事件(执行监听事件的所有函数) /// /// 事件的名字 public void TriggerEvent(string name, object o) { eventPool[name].Invoke(o); } /// /// 这个函数是当我们过场景的时候帮助我们清空事件中心池 /// public void Clear() { eventPool.Clear(); } } ~~~ Player.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; public class Player : MonoBehaviour { private void Awake() { EventPool.GetInstance.AddEventListener("MonsterDead", MonsterDeadToDo); } void MonsterDeadToDo(object o) { Debug.Log("怪物死亡,玩家获胜!" + (o as Monster)); } private void OnDestroy() { EventPool.GetInstance.RemoveEventListener("MonsterDead", MonsterDeadToDo); } } ~~~ Monster.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; public class Monster : MonoBehaviour { private void Start() { Dead(); } private void Dead() { Debug.Log("怪物死亡!"); EventPool.GetInstance.TriggerEvent("MonsterDead", this); } } ~~~ #### 事件中心池还可以怎么优化 1.我们是否能够避免装箱拆箱? 2.我们可以看见,我们现在执行的委托函数都是有一个参数的无返回值的函数,如果我们有两个参数无返回值的函数呢?如果我们有没有参数的有返回值的函数等等系列事件呢? ### 四.Mono公共生命周期模块 #### 0Mono公共生命周期的介绍 ##### 0.1什么是Mono公共生命周期模块 一句话说就是让没有继承MonoBehaviour的脚本也可以在生命周期函数被调用。 ##### 0.2为什么需要有Mono公共生命周期模块 试想一个场景:我们写了几个类,但是我们并不想让这些类继承Monobehaviour(因为只有继承了MonoBehaviour的类,且拖拽到了我们的显示面板上的才能被执行),却想它能够进行帧更新Update操作,那要怎么办?这就是为什么我们需要有Mono公共生命周期模块的原因。 ##### 0.3Mono公共生命周期模块的优与弊 ###### Mono公共生命周期模块的优点 Mono公共生命周期模块的优点是十分显而易见的,一切继承了MonoBehaviour的类,都要继承MonoBehaviour类中的成员变量、成员方法,所以每个继承了MonoBehaviour的类都是占用了十分可观的内存的,我们如果有了Mono公共生命周期模块就可以帮助我们节省了我们的内存空间;第二点,继承了MonoBehaviour的脚本想要被执行还要放置在显示面板上。 ###### Mono公共生命周期模块的缺点 缺陷上,我觉得可能就是跟单例模式的缺点一样,需要占用一段内存空间,并且是从程序运行到结束这段时间会一直占用着,不会被释放,但是也是通过空间换取了效率吧。 #### 1.Mono公共生命周期模块 ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using System.Collections; using UnityEngine; using UnityEngine.Events; /// /// 公共生命周期函数模块 /// 需要提供的服务: /// 1.公用的Update()方法用于帮助未继承MonoBehaviour脚本对象的帧更新操作 /// 2.让没有继承MonoBehaviour的脚本也可以开启协程StartCoroutine /// 3.我们可以根据我们的需求重写我们的生命周期函数,甚至我们可以实现让所有没有继承 /// MonoBehaviour的类,却能够使用MonoBehaviour中的生命周期函数 /// public class MonoSystem : MonoBaseSingleton { /* * 这是我们所有未继承MonoBehaviour类却想执行生命周期中帧更新函数的一个容器 * 顺便讲一下为什么使用event事件不用Action委托,然后其两者的区别: * 1.event不能被外部类访问,只能在内部类进行访问,它不像Action委托可以在 * 外部类通过.(点)的方式使用。 * 2.event不能在外部类中赋值,只能进行+=、-+,所以我们就不会出现我们的event * 被置空null的行为。 * 3.event也不能在外部中被执行,如果我们想在外部类中执行event,需要进行一层封装 * 总结:event是在委托Action上的进行了一层安全的封装,具体的使用方式与委托基本完全一样 * ,但是比委托更安全。 */ private event UnityAction monoUpdateEvent; // 这是我们所有未继承MonoBehaviour类却想执行生命周期中固定帧更新函数的一个容器 private event UnityAction monoFixedUpdateEvent; // 帧更新函数,正常一帧约等于0.16s private void Update() { if (monoUpdateEvent != null) monoUpdateEvent.Invoke(); } // 固定帧更新函数,正常一帧约等于0.2s,可以在Unity中修改 private void FixedUpdate() { if (monoFixedUpdateEvent != null) monoFixedUpdateEvent.Invoke(); } /// /// 添加未继承MonoBehaviour的类的方法想进行Update帧更新 /// /// 需要在Update中执行帧更新的函数 public void AddUpdateEvent(UnityAction function) { monoUpdateEvent += function; } /// /// 移除未继承MonoBehaviour的类的方法已经添加进Update帧更新的方法 /// /// 需要在Update中执行帧更新的函数 public void RemoveUpdateEvent(UnityAction function) { monoUpdateEvent -= function; } /// /// 供外部没有继承MonoBehaviour却想开启协程的使用的无参方法 /// 注意!!:我们不使用通过方法名的形式开启协程,因为该方式只适用于类的内部方法,外部类的方法是无法被开启的 /// /// public void MonoStartCorountine(IEnumerator e) { // 开启协程 StartCoroutine(e); } /// /// 过场景时候清除我们的mono公共事件容器 /// public void Clear() { monoUpdateEvent = null; monoFixedUpdateEvent = null; } } ~~~ 测试我们的Mono公共生命周期模块: 测试我们的Mono公共生命周期模块是十分简单的。首先,因为我们的Mono公共生命周期模块继承了我们的MonoSingleton所以它会自动的帮助我们在场景上生成一个物体,自动的帮我挂上Mono公共生命周期脚本,我们只需要在文件夹下创建一个脚本即可,脚本命名为:MonoTest,然后再创建一个空物体,把脚本挂上去。 MonoTest.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; public class MonoTest : MonoBehaviour { private void Start() { Test test = new Test(); MonoSystem.GetInstance.AddUpdateEvent(test.Update); } } public class Test { // 这是我们需要进行update帧更新的函数,因为我们是没有继承MonoBehaviour的, // 所以它自身是不可能进行update帧更新的 public void Update() { Debug.Log("我居然可以进行帧更新啦啦啦啦!!!!"); } } ~~~ ### 五.场景管理器 #### 0.场景管理器模块的介绍 ##### 0.1什么是场景管理器模块 一句话说就是让我们在加载场景完成后需要处理的逻辑(动态生成物体,更新角色信息等等)。 ##### 0.2为什么需要有场景管理器 我们传统的场景是什么?假设,我们有两个场景,场景A,场景B,那么我们会事先在场景A和场景B中摆放好我们需要的物体等等;但是,试想一下我们如今很火的一款抽卡回合制游戏阴阳师,它从主界面到进入游戏战斗界面的时候,是需要玩家放置我们的式神的,那么如果按传统的方式,我们就无法知道玩家需要上阵的式神,也就无法实现摆放好,于是,场景管理器就可以解决这种当我们加载场景完毕后需要执行其他方法或者行为的问题。 ##### 0.3场景管理器的优与弊 ###### 场景管理器的优点 我们可以在场景加载完毕后再去执行我们想要处理的逻辑。 ###### 场景管理器的缺点 缺点就是它是单例,一直会在系统内存中占用着不被释放掉。 #### 1.场景管理器 SceneMgr.cs ~~~C# using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; /// /// 场景切换模块 /// 知识点 /// 1.场景异步加载 /// 2.协程 /// 3.委托 /// public class ScenesMgr : BaseManager { /// /// 切换场景 同步 /// /// public void LoadScene(string name, UnityAction fun) { //场景同步加载 SceneManager.LoadScene(name); //加载完成过后 才会去执行fun fun(); } /// /// 提供给外部的 异步加载的接口方法 /// /// /// public void LoadSceneAsyn(string name, UnityAction fun) { MonoMgr.GetInstance().StartCoroutine(ReallyLoadSceneAsyn(name, fun)); } /// /// 协程异步加载场景 /// /// /// /// private IEnumerator ReallyLoadSceneAsyn(string name, UnityAction fun) { AsyncOperation ao = SceneManager.LoadSceneAsync(name); //可以得到场景加载的一个进度 while(!ao.isDone) { //事件中心 向外分发 进度情况 外面想用就用 EventCenter.GetInstance().EventTrigger("进度条更新", ao.progress); //这里面去更新进度条 yield return ao.progress; } //加载完成过后 才会去执行fun fun(); } } ~~~ 测试我们的场景管理器: 测试我们的场景管理器是十分简单的。首先,我们需要两个场景,一个game场景,一个SceneTest场景,在File->BuildSetting中把两个场景添加进去,然后再在game场景找任意一个物体挂上我们的测试脚本。 SceneTest.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; public class SceneTest : MonoBehaviour { private void Start() { // 测试同步加载场景 // SceneMgr.GetInstance.LoadScene("SceneTest");、 // SceneMgr.GetInstance.LoadScene("SceneTest", () => { Debug.Log("同步加载场景成功!"); }); // 测试异步加载场景 // SceneMgr.GetInstance.LoadAsyncScene("SceneTest"); SceneMgr.GetInstance.LoadAsyncScene("SceneTest", () => { Debug.Log("异步加载场景成功!"); }); } } ~~~ ### 六.资源加载模块 #### 0.资源加载模块的介绍 ##### 0.1什么是资源加载模块 一句话说就是让我们在加载完成资源后需要处理的逻辑(根据我们加载物体的类型进行动态的处理我们的逻辑)。 ##### 0.2为什么需要有资源加载模块 单单的Resources提供的静态方法已经不足以解决我们的需求的时候,我们需要对它进行一层封装(如同SceneManager一样),进而解决或者处理我们不同的业务需求。 ##### 0.3资源加载模块的优与弊 ###### 资源加载模块的优点 我们可以在资源加载完毕后再去执行我们想要处理的逻辑。 ###### 资源加载模块的缺点 缺点就是它是单例,一直会在系统内存中占用着不被释放掉。 ResourcesMgr.cs #### 1.资源加载模块 ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; using System.Collections; using UnityEngine.Events; public class ResourcesMgr : BaseSingleton { /// /// 同步加载资源的泛型方法 /// /// 泛型类型 /// 加载资源路径 /// 返回加载后的物体 public T LoadResources(string path) where T : Object { // 加载资源 T o = Resources.Load(path); // 判断是否是游戏物体资源,一般游戏物体都是需要加载后再去实例化它 if (o is GameObject) return GameObject.Instantiate(o as T); else return o; } /// /// 异步加载资源的泛型方法 /// /// 泛型类型 /// 资源路径 /// 返回加载后的物体 public void LoadAysncResources(string path, UnityAction callback = null) where T : Object { // 这里又调用了我们之前写的mono公共生命周期,让没继承MonoBehaviour的类也能开启协程 MonoSystem.GetInstance.StartCoroutine(IELoadAysncResources(path, callback)); } /// /// 异步加载资源的协程方法 /// /// 泛型类型 /// 资源路径 /// 回调函数(加载后传回我们加载的资源,供回调函数下一步执行具体的逻辑) /// IEnumerator IELoadAysncResources(string path, UnityAction callback) where T : Object { // 异步加载资源 ResourceRequest r = Resources.LoadAsync("path"); yield return r; // 判断是否是游戏物体,如果是的话,我们直接实例化后再去返回 if (r.asset is GameObject) callback.Invoke(GameObject.Instantiate(r.asset) as T); else callback.Invoke(r.asset as T); } } ~~~ 测试我们的资源管理器: 测试我们的资源管理器是十分简单的。我们在写对象池模块的时候就已经创建好了两个预制体了,所以我们就可以拿它们来做测试案例,测试之前记得把挂载了对象池的代码移除。 ResTest.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; public class ResTest : MonoBehaviour { private void Start() { // 我们在写对象池的时候就存了两个预制体,我们可以使用它来做测试 // 测试的时候,记得把我们预制体上挂载的对象池的测试脚本取下去 // 测试同步加载 GameObject obj = ResourcesMgr.GetInstance.LoadResources("Cube"); obj.transform.localScale *= 2; } } ~~~ ### 七.结合资源管理器版的对象池 ObjectPool.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; using System.Collections.Generic; using UnityEngine.Events; /* 优化方向: * 1.我们可以明显的看见,我们创建的物体一直在显示面板上面,没有很好的整理的,那么我们需要优化的第一个方向就是,如何像文件夹一样管理好我们的对象池, * 例如:我们可以在显示面板上创建一个Pool空物体,然后再在Pool上创建对应对象名字的空物体,一个Cube空物体,一个Sphere空物体, * 让它们管理我们实例化出来的Cube和Sphere物体,这样就可以很方便的, * 让我们查看哪些是还在对象池里面的,哪些是出了对象池的,也把抽象化的对象池子,具体到了我们的显示面板上。 * 2.我们的对象池是用List去装载我们的游戏物体的,这其实有点违背了我们面向对象的思想,现在我们需要把List换成一个类PoolData,去优化我们的对象池。 */ /// /// 对象池中的容器模块 /// public class PoolData { /* * 该类中我们只需要考虑下面的问题: * 我们已经有了GetObj()与Push()方法,但是我们现在需要把List换成我们的PoolData,更加符合我们面向对象思想, * 那么我们就需要把这两个方法内部具体的实现抽离出来,成为PoolData中供对象池调用的行为。 */ // 这是我们的用来存储缓存对象的容器 private List objContainer = null; // 这是我们用于管理我们容器缓存对象的父节点 private GameObject fatherRoot = null; /// /// 我们的构造函数,用于初始化我们的成员 /// /// /// 我们容器空物体放置的节点 /// /// 生成游戏物体后的用于存储的容器名字 public PoolData(GameObject poolRoot, string objName) { if(objContainer == null) objContainer = new List(); if (fatherRoot == null) { fatherRoot = new GameObject(objName); fatherRoot.transform.parent = poolRoot.transform; } } public void PushObjInContainer(GameObject obj) { // 放回对象池子的容器 // 第一步:把对象先失活,让其从场景中消失 obj.SetActive(false); // 第二步:把对象添加进对象容器中 objContainer.Add(obj); // 第三步:把在显示面板上已经失活的对象放进我们的管理物体中 obj.transform.parent = fatherRoot.transform; } public GameObject GetObjFromContainer() { // 移出对象池子的容器 // 第一步:我们需要先创建一个GameObject对象用来存储我们移出的物体 GameObject obj = objContainer[0]; // 第二步:把我们的对象从显示面板上激活出来 obj.SetActive(true); // 第三步:把我们的对象从我们的容器中移出 objContainer.RemoveAt(0); // 第四步:把我们的对象从我们的父物体上断开联系 obj.transform.parent = null; // 第五步:返回出去供外部使用 return obj; } /// /// 用于返回我们容器内部缓存的对象数量 /// /// 容器内部的对象数量 public int ContainerCount() { return objContainer.Count; } } /// /// 对象池模块 /// public class ObjectPool : BaseSingleton { // 创建我们的对象池池子 // 参数 1 我们存储对象的容器名字,参数 2 我们真正用于存储对象的容器 public Dictionary objPool = new Dictionary(); // 我们需要挂载的根节点 private GameObject poolRoot = null; /// /// 使用完之后返还对象池子里面 /// /// 需要返还的游戏对象 public void PushObj(GameObject obj) { // 判断我们显示面板上是否有我们用于管理对象池的空物体(Pool) if(poolRoot == null) poolRoot = new GameObject("Pool"); // 查找对象池子里面是否存在缓存我们对象容器 if (objPool.ContainsKey(obj.name)) { // 把使用完的对象添加进容器池子里面(返还对象池子) objPool[obj.name].PushObjInContainer(obj); } else { // 如果我们的对象池子里面没有存在可以用于缓存游戏对象的容器,那么就需要创建一个,再把要返还的对象存进去 objPool[obj.name] = new PoolData(poolRoot, obj.name); objPool[obj.name].PushObjInContainer(obj); } } /// /// 得到我们的对象池子中缓存的对象 /// 1.如果不存在对象的容器就创建一个容器并且生成我们预先设好的数量 /// 2.如果存在对象的容器但是容器内部缓存的游戏对象数量小于等于0,那么一样生成我们预先设好的数量 /// /// 生成游戏物体后的名字 /// 生成游戏物体的路径(Resources文件夹下游戏物体的路径) /// 因为我们已经没有返回值了,如果需要对我们容器中的物体进行操作的话,我们就需要通过委托的函数把 /// 物体传出去 public void GetObj(string objName, string objPath, UnityAction callback) { // 判断我们显示面板上是否有我们用于管理对象池的空物体(Pool) if (poolRoot == null) poolRoot = new GameObject("Pool"); // 查看对象池子中是否存在缓存我们游戏对象的容器 if(objPool.ContainsKey(objName) && objPool[objName].ContainerCount() > 0) callback(objPool[objName].GetObjFromContainer()); // 如果不存在就生成一个新的容器 if (!objPool.ContainsKey(objName)) objPool.Add(objName, new PoolData(poolRoot, objName)); else if (objPool[objName].ContainerCount() <= 0) { // 首先,生成游戏对象,再把新生成的游戏物体添加进我们的容器中 ResourcesMgr.GetInstance.LoadAysncResources(objPath, (obj) => { // 更改名字 obj.name = objName; callback(obj); }); } } /// /// 用于过场景之后清空对象池 /// public void Clear() { // 清空对象池内部的键值 objPool.Clear(); // 把显示面板上的对象池的子物体全部清空 poolRoot = null; } } ~~~ 测试脚本 test.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; public class test : MonoBehaviour { private void Update() { if (Input.GetMouseButtonDown(0)) { //ObjectPool.GetInstance.GetObj("Cube", "Cube"); ObjectPool.GetInstance.GetObj("Cube", "Cube", (o)=> { o.transform.localScale = Vector3.one * 2; }); } if (Input.GetMouseButtonDown(1)) { //ObjectPool.GetInstance.GetObj("Sphere", "Sphere"); ObjectPool.GetInstance.GetObj("Sphere", "Sphere", (o) => { o.transform.localScale = Vector3.one * 2; }); } } } ~~~ delayBack.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; public class delayBack : MonoBehaviour { private void OnEnable() { Invoke("BackPool", 1); } void BackPool() { ObjectPool.GetInstance.PushObj(this.gameObject); } } ~~~ ### 八.输入控制模块 #### 0.输入控制模块的介绍 ##### 0.1什么是输入控制模块模块 一句话说就是用来统一管理玩家在输入设备中的检测。 ##### 0.2为什么需要有输入控制模块 试着想一想,如果我们有一个玩家类Player,我们需要移动这个玩家的话,我们是不是要先让这个Player继承MonoBehaviour,然后再在其Update帧更新函数中去检测玩家输入。那么如果我们场景中还有个玩家2呢?那么是不是又要重复一次上面的操作,还要重写写一遍重复的逻辑代码?如果我的玩家还可以操纵怪物(类比玩家3)行走呢?是不是就越来越高的重复度了,越来越多的重复简单逻辑性的代码与继承了MonoBehaviour的脚本? ##### 0.3输入控制模块的优与弊 ###### 输入控制模块的优点 简单的说就是我们的统一管理玩家的输入设备,而且在事件中心池子中,我们对项目中的不同类却需要互相联系进行解耦合。 ###### 输入控制模块的缺点 缺点就是它是单例,一直会在系统内存中占用着不被释放掉。 #### 1.输入控制模块 ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; /// /// 输入管理器 /// 作用: /// 1.管理我们是否要检测玩家的输入(例如在过场景的放过场动画的时候,我们就不监听玩家输入) /// 2.统一管理所有输入相关的操作 /// public class InputMgr : BaseSingleton { // 是否检测玩家输入 private bool isChecking = false; /// /// 在构造函数中实例化之后,添加我们的帧更新事件 /// public InputMgr() { // 添加我们的帧更新事件 MonoSystem.GetInstance.AddUpdateEvent(InputUpdate); } /// /// 是否开启或者关闭输入检测 /// /// 开启或者关闭 public void StartCheakOrEnd(bool isOpen = true) { isChecking = isOpen; } /// /// 封装我们输入相关的方法 /// /// 输入的键值 private void InputCheack(KeyCode keyCode) { if (Input.GetKeyDown(keyCode)) { EventPool.GetInstance.TriggerEvent("某键按下", keyCode); } if (Input.GetKeyUp(keyCode)) { EventPool.GetInstance.TriggerEvent("某键抬起", keyCode); } } /// /// 需要进行帧更新检测的方法 /// private void InputUpdate() { // 如果不需要检测则直接返回 if (!isChecking) return; // 需要检测的键值 InputCheack(KeyCode.W); InputCheack(KeyCode.S); InputCheack(KeyCode.A); InputCheack(KeyCode.D); } } ~~~ 测试我们的输入管理器: 找到我们场景中的任意一个对象,然后创建一个InputTest脚本挂载上去即可。 InputTest.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; public class InputTest : MonoBehaviour { private void Start() { // 实例化InputMgr单例,并且开启输入检测 InputMgr.GetInstance.StartCheakOrEnd(); // 注册监听输入事件 EventPool.GetInstance.AddEventListener("某键按下", MyInputDown); EventPool.GetInstance.AddEventListener("某键抬起", MyInputUp); } /// /// 接收输入按键按下的方法 /// /// 按下的键值 private void MyInputDown(object code) { switch ((KeyCode)code) { case KeyCode.W: Debug.Log("前进"); break; case KeyCode.S: Debug.Log("后退"); break; case KeyCode.A: Debug.Log("左转"); break; case KeyCode.D: Debug.Log("右转"); break; default: break; } } /// /// 接收输入按键抬起的方法 /// /// 抬起的键值 private void MyInputUp(object code) { switch ((KeyCode)code) { case KeyCode.W: Debug.Log("停止前进"); break; case KeyCode.S: Debug.Log("停止后退"); break; case KeyCode.A: Debug.Log("停止左转"); break; case KeyCode.D: Debug.Log("停止右转"); break; default: break; } } } ~~~ ### 九.音频管理模块 #### 0.音频管理模块的介绍 ##### 0.1什么是音频管理模块 一句话说就是管理我们游戏中所有的与音频有关的AudioSource\AudioClip等。 ##### 0.2为什么需要音频管理模块 1.可以管理我们所有的背景音乐; 2.可以管理我们所有的音效音乐; 3.可以根据不同的类型划分出不同的管理(例如:角色里面我既有走路的音效,还有技能音效,还有角色的背景音乐等等,然后我角色可能不止要播放一个音效); 4.可以根据我们的怪物、人物、NPC这样管理其身上的所有音乐; ##### 0.3音频管理模块的优与弊 缺点就是它是单例,一直会在系统内存中占用着不被释放掉。 MusicMgr.cs #### 1.音频管理模块 ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; using System.Collections.Generic; /// /// 音频管理器 /// 作用: /// 1.可以管理我们所有的背景音乐 /// 2.可以管理我们所有的音效音乐 /// 3.可以根据不同的类型划分出不同的管理(例如:角色里面我既有走路的音效, /// 还有技能音效,还有角色的背景音乐等等,然后我角色可能不止要播放一个音效) /// 4.可以根据我们的怪物、人物、NPC这样管理其身上的所有音乐 /// public class MusicMgr : BaseSingleton { // 这是我们用于管理我们所有音频组件AudioSource的空物体 private GameObject musicRoot = null; // 我们存储所有音频资源 private Dictionary audioClipDic = null; // 这是管理我们的音频组件AudioSource private List sourcesPool = null; // 这是用于管理我们正在播放的音频组件 private Dictionary asInUsing= null; // 默认背景音量与音效音量大小 private float Volume = 0.5f; public MusicMgr() { // 因为MusicMgr继承了我们的单例基类,所以我们它的构造函数中去实例化musicRoot if (musicRoot == null) musicRoot = new GameObject("MusicMgr"); // 实例化我们音频资源的容器 if (audioClipDic == null) audioClipDic = new Dictionary(); // 实例化我们音频组件的容器 if (sourcesPool == null) sourcesPool = new List(); // 实例化我们正在使用中的音频组件的容器 if (asInUsing == null) asInUsing = new Dictionary(); } /// /// 添加音频资源 /// /// 加载音频资源的路径 /// 音频资源的名字 public void AddAudioClip(string path, string clipName) { if(!audioClipDic.ContainsKey(clipName)) ResourcesMgr.GetInstance.LoadAysncResources(path, (clip)=> { audioClipDic.Add(clipName, clip); }); } /// /// 播放音乐 /// /// 播放的音频资源名字 /// 是否循环播放 public void PlayMusic(string clipName, bool isLoop = false) { // 先判断场景中该音频的资源是否被播放 if (!asInUsing.ContainsKey(clipName)) { // 再判断是否存在该音频资源 if (audioClipDic.ContainsKey(clipName)) { AudioSource audioSource = GetAudioSource(); audioSource.clip = audioClipDic[clipName]; audioSource.volume = Volume; asInUsing.Add(clipName, audioSource); audioSource.loop = isLoop; audioSource.Play(); } } } /// /// 暂停音乐 /// /// 音频名字 public void PuaseMusic(string clipName) { // 判断该片段是否正在播放 if (asInUsing.ContainsKey(clipName)) // 如果正在播放就把它暂停 asInUsing[clipName].Pause(); } /// /// 停止音乐 /// /// 音频名字 public void StopMusic(string clipName) { // 判断该片段是否正在播放 if (asInUsing.ContainsKey(clipName)) { // 如果正在播放的话就停止播放 asInUsing[clipName].Stop(); // 把它返回AudioSource池子中 PushAudioSource(asInUsing[clipName]); // 再把它从正在播放的AudioSource池子中移除 asInUsing.Remove(clipName); } } /// /// 从我们的AudioSourcePool中得到一个AudioSource /// /// 返回我们音频组件池子中未被使用的AudioSource private AudioSource GetAudioSource() { AudioSource audioSource = null; // 如果我们的AudioSource池子中没有可用的AudioSource,就实例化一个 if (sourcesPool.Count <= 0) audioSource = musicRoot.AddComponent(); // 返回我们的第一个 audioSource = sourcesPool[0]; // 移除我们的第一个 sourcesPool.RemoveAt(0); // 把它激活 audioSource.enabled = true; // 返回出去 return audioSource; } /// /// 返还一个AudioSource给我们的AudioSourcePool /// /// private void PushAudioSource(AudioSource audioSource) { // 返回之前先把组件失活 audioSource.enabled = false; // 再把组件添加进去 sourcesPool.Add(audioSource); } /// /// 这是设置背景音乐音量的大小 /// public void SetBGMValue(string clipName, float BGMVolume) { // 判断该音频片段是否正在播放 if (asInUsing.ContainsKey(clipName)) // 如果正在播放就设置其音量大小 asInUsing[clipName].volume = BGMVolume; } /// /// 这是设置音效音量的大小 /// public void SetAudioValue(string clipName, float soundVolume) { // 判断该音频片段是否正在播放 if (asInUsing.ContainsKey(clipName)) // 如果正在播放就设置其音量大小 asInUsing[clipName].volume = soundVolume; } } ~~~ 测试我们的音频管理器: 找到我们场景中的任意一个对象,然后创建一个MusicTest脚本挂载上去即可。 MusicTest.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; public class MusicTest : MonoBehaviour { private void Start() { MusicMgr.GetInstance.AddAudioClip("music01", "music01"); MusicMgr.GetInstance.AddAudioClip("sound01", "sound01"); } private void OnGUI() { if(GUI.Button(new Rect(0, 0, 100, 100), "播放音乐")) { MusicMgr.GetInstance.PlayMusic("music01"); } if (GUI.Button(new Rect(100, 0, 100, 100), "暂停音乐")) { MusicMgr.GetInstance.PuaseMusic("music01"); } if (GUI.Button(new Rect(200, 0, 100, 100), "停止音乐")) { MusicMgr.GetInstance.StopMusic("music01"); } if (GUI.Button(new Rect(0, 100, 100, 100), "播放音效")) { MusicMgr.GetInstance.PlayMusic("sound01"); } if (GUI.Button(new Rect(100, 100, 100, 100), "暂停音效")) { MusicMgr.GetInstance.PuaseMusic("sound01"); } if (GUI.Button(new Rect(200, 100, 100, 100), "停止音效")) { MusicMgr.GetInstance.StopMusic("sound01"); } } } ~~~ ### 十.UI框架模块 #### 0.UI框架模块的介绍 ##### 0.1什么是UI框架模块 一句话说就是管理我们所有与面板相关的UI组件。 ##### 0.2为什么需要有UI框架模块 我们常用的传统的做法可能有两种:第一种是先写一个与面板同名的类,然后类中写好一个Public访问类型的方法,再拖拽到Button的事件监听之中,这是我们的拖拽的方法也是常用的拖拽方法,比较方便,但是一旦项目中有成千上百个按钮要实现点击事件的时候,这种方法就十分不适用,最大缺点还是们在更新到新版本Unity过程中可能会丢失了我们的引用,这时候又要重新进行拖拽,十分的麻烦;第二种是在外面写的类里面,用代码的形式用onClick.AddListener()的方式进行监听,但是一样的,当我们有多个要监听的事件时候,就会产生很多重复代码。 当我们的游戏越做越大的时候,UI就会越变越复杂,那么UI资源的大小就越来越大,传统的把UI进行失活激活这样的操作,必须要知道,我们在面板上对某个物体进行了失活,但是它一样是会占用我们的内存的(我们还存有对它的引用),所以我们的场景就会越来越大,而这时候我们动态的加载与销毁反而会减小性能上的开销;然后就是,我们游戏是分层级的,不同的层级显示不同的信息,例如,我们的充值成功信息肯定是会渲染在最前面,那么UI管理器就可以很方便的去管理我们的所有面板层级关系。 ##### 0.3UI框架模块的优与弊 缺点就是它是单例,一直会在系统内存中占用着不被释放掉。 #### 1.UI框架模块 BasePanel.cs ~~~C# /**************************************************** 文件:$FILE_BASE$.$FILE_EXT$ 作者:小小泽 邮箱: 1245615197@qq.com 日期:$DATE$ $HOUR$:$MINUTE$ 功能:$end$ *****************************************************/ using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using System.Collections.Generic; /// /// 面板基类 /// 作用 /// 1.查找自身以及自身子物体所有挂载的UI组件 /// 2.存储自身以及自身子物体所有组件的引用 /// 3.根据不同的组件添加不同的事件(Buttom的OnClick, Toggle的IsValueChange等等) /// 4.我们只在我们的面板脚本里面去控制我们面板自身及其子面板的所有控件的使用 /// public class BasePanel : MonoBehaviour { /* * contorlsDic管理我们所有面板下挂载的所有组件 * 1.参数一 挂载了该组件的物体名字(例如:我们有一个LoginPane,Panel里面有一个StartButtom,那么我们就存入一个StartButtom这个挂载了Buttom组件 * 的物体名字) * 2.参数二 存入真正的物体所挂载的组件类型,为什么是UIBehaviour,因为我们的UI有许许多多不同的组件,但是它们都有一个共同的基类就是UIBehaviour * 我们利用里氏替换原则可以做到父类装子类,最后需要某个组件的时候再把它as成对应类型的即可。 */ private Dictionary> controlsDic = new Dictionary>(); protected virtual void Awake() { // 每个面板开始激活之后,我们就先把把常用的UI组件都查找一遍,查看其身上以及子物体中是否挂载了该组件 FindControls