针对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));
            });
        }
    }
}

 

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注