针对GameObject的对象池(缓存池)
对象池是什么?
游戏中可能会出现某一类对象需要频繁的创建和销毁
我们都知道垃圾回收也就是gc是有性能消耗的代价的
所以为了减少gc的发生我们就引入了对象池这一概念
举例:射击游戏中频繁出现的子弹
假设我们每一发子弹都去销毁 那短时间内可能就会卡顿
所以与其销毁我们不如把它隐藏起来
等需要的时候再拿出来用就可以了
对象池的结构
对象池可以理解为一个大型的衣柜
这个衣柜里面有很多个不同的抽屉
一个抽屉放春天的衣服 另一个放夏天的衣服
需要的时候我们就从抽屉里拿出来 不需要的时候再放回去即可
那么什么可以用来做这样的结构呢
对象池的代码实现
我们用字典来作为外层的这个大衣柜
字典的键string是Resources文件夹下预制体的路径
一般我们会在创建新对象的时候命名 例如item/xx
在场景上创建一个新的空物体叫pool
然后底下再生成子对象item/xx 然后所有同类型预制体都放在这个子对象下
就能够起到分类的作用
字典的值存放的是我们自定义的类 也就是所谓的抽屉
类里要有我们刚刚提到的父节点
然后用一个List作为分类的容器
构造函数里完成放入到pool下对应子对象的操作
然后要有放入和取出两种方法
当然大衣柜同样也要实现放入和取出
只不过是要找到对应的抽屉然后再放入和取出
还有个问题是你不能让它一直无限量的膨胀
所以我写了一个回收机制 隔多少秒就会保留指定数量的物品
确保它不会膨胀到使游戏卡顿的数量
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; //把对象池本身和对象池里的分类分开来写 //对象池里的分类操作 //对象池的操作 /// <summary> /// 对象池里的某一分类 /// </summary> public class PoolData { //单个分类挂载的父节点 这样在unity数据显示中会体现层级 public GameObject rootObj; //对象的分类容器 public List<GameObject> poolList; /// <summary> /// 构造函数 /// </summary> /// <param name="obj">传入分类的名称</param> /// <param name="poolObj">分类的父节点(最外层的pool)</param> public PoolData(GameObject obj, GameObject poolObj) { //pool下放入分类的文件夹 rootObj = new GameObject(obj.name); rootObj.transform.SetParent(poolObj.transform, true); poolList = new List<GameObject>(); PushObj(obj); } /// <summary> /// 往分类里放入对象 /// </summary> /// <param name="obj">放入分类的对象</param> public void PushObj(GameObject obj) { obj.SetActive(false);//隐藏 poolList.Add(obj); obj.transform.SetParent(rootObj.transform, true); } /// <summary> /// 从分类中取出对象 /// </summary> /// <returns></returns> public GameObject GetObj() { GameObject obj = poolList[0]; poolList.RemoveAt(0); //记得一定要激活 obj.SetActive(true); //不显示在pool里的分类下 而是显示在外面 obj.transform.SetParent(null, true); return obj; } } public class PoolManager : BaseManager<PoolManager> { public Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>(); private GameObject poolObj; public PoolManager() { //有需要再启用 改成合适的回收冗余的上限和间隔时间 if(MonoManager.GetInstance().controller != null) { MonoManager.GetInstance().StartCoroutine(ClearCache(20,60)); } } /// <summary> /// 回收对象池冗余 /// </summary> /// <param name="max">单个分类正常允许存在的最大数量</param> /// <param name="time">间隔几秒集中回收一次</param> /// <returns></returns> IEnumerator ClearCache(int max, float time) { while (true) { yield return new WaitForSeconds(time); if(poolDic.Count > 0) { foreach(PoolData data in poolDic.Values) { if (data.poolList.Count > max) { Debug.Log(data.poolList.Count); for (int i = data.poolList.Count - 1; i > max - 1; i--) { GameObject.Destroy(data.poolList[i]); data.poolList.RemoveAt(i); } } } } } } /// <summary> /// 从分类里异步加载对象 /// </summary> /// <param name="name">分类名</param> /// <param name="action">action委托</param> public void GetObj(string name, UnityAction<GameObject> action) { if (poolDic.ContainsKey(name) && poolDic[name].poolList.Count > 0) { action(poolDic[name].GetObj()); } else { ResourceManager.GetInstance().LoadAsync<GameObject>(name, (obj) => { obj.name = name; action(obj); }); } } /// <summary> /// 放入对象到对应分类里 /// </summary> /// <param name="name">分类名</param> /// <param name="obj">放入的对象</param> public void PushObj(string name, GameObject obj) { if(poolObj == null) { poolObj = new GameObject("Pool"); } if(poolDic.ContainsKey(name)) { poolDic[name].PushObj(obj); } else { //没有对应的分类 poolDic.Add(name, new PoolData(obj,poolObj)); } } /// <summary> /// 清空缓存池 防止在场景切换的时候溢出 /// </summary> public void Clear() { poolDic.Clear(); poolObj = null; } }
对象池的运行和使用
当场景中没有此类对象的时候
会先创建pool以及对应名字的文件夹
然后生成该对象并对该对象改名 然后放置到对应层级
假如有此类对象并且是失活状态 那么就重新取出来使用
假如所有对象都是激活 就扩容 创建一个新的对象
当然这里就要涉及到回收的事情了
方法有很多种 例如我这里直接用延迟函数在激活的时候回收
using System.Collections; using System.Collections.Generic; using UnityEngine; public class DelayRemove : MonoBehaviour { // Start is called before the first frame update void OnEnable() { Invoke("Push", 2); } void Push() { PoolManager.GetInstance().PushObj(this.gameObject.name, this.gameObject); } }
使用的时候只要直接通过单例来调用即可
例如下面这个 左键就会生成cube 右键就会生成sphere
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TestPool : MonoBehaviour { // Update is called once per frame void Update() { if(Input.GetMouseButtonDown(0)) { PoolManager.GetInstance().GetObj("Test/Cube", (obj) => { obj.transform.position = new Vector3(Random.Range(0f, 10f), Random.Range(0f, 10f),Random.Range(0f,10f)); }); } else if(Input.GetMouseButtonDown(1)) { PoolManager.GetInstance().GetObj("Test/Sphere", (obj) => { obj.transform.position = new Vector3(Random.Range(0f, 10f), Random.Range(0f, 10f), Random.Range(0f, 10f)); }); } } }