对象池实现

PoolAttribute

PoolAttribute类负责对需要Object对象池管理的对象进行Pool属性标记,无参数故无内容,只作用于类,不允许重复定义。

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class PoolAttribute : Attribute
    {}

GameObject对象池

alt

GameObjectPoolData类负责对PoolManager中的gameObjectPoolDic字典中具体存储的一个GameObject对象池数据进行管理,包含对象池父节点fatherObj和对象队列容器PoolQueue 。设计PushObj、GetObj方法和构造方法GameObjectPoolData实现对GameObject对象池的初始化、对象放入、对象取出,需要注意的是,PoolManager中的push和get相关操作是面向外界操作逻辑的,负责从所有对象池中找到特定的那一个对象池,具体的数据存储交由GameObjectPoolData类中的push和get方法完成。

       	//class GameObjectPoolData
		// 对象池中 父节点
        public GameObject fatherObj;
        // 对象容器
        public Queue<GameObject> poolQueue;

        public GameObjectPoolData(GameObject obj, GameObject poolRootObj)
        {
            // 创建父节点 并设置到对象池根节点下方
            fatherObj = new GameObject(obj.name);
            fatherObj.transform.SetParent(poolRootObj.transform);
            poolQueue = new Queue<GameObject>();
            // 把首次创建时候 需要放入的对象 放进容器
            PushObj(obj);
        }

对于新创建的一类GameObjectPoolData,首先要创建其父节点(与物体同名)挂载到PoolRoot下,提供好对象容器并将对象放入对象池,所以实际有三层(对象池根节点-某一类对象池父节点-实际对象),先做好第一层第二层的层级设置,数据操作由PoolManager完成。

       /// <summary>
        /// 将对象放进对象池
        /// </summary>
        public void PushObj(GameObject obj)
        {
            // 对象进容器
            poolQueue.Enqueue(obj);
            // 设置父物体
            obj.transform.SetParent(fatherObj.transform);
            // 设置隐藏
            obj.SetActive(false);
        }

        /// <summary>
        /// 从对象池中获取对象
        /// </summary>
        /// <returns></returns>
        public GameObject GetObj(Transform parent = null)
        {
            GameObject obj = poolQueue.Dequeue();

            // 显示对象
            obj.SetActive(true);
            // 设置父物体
            obj.transform.SetParent(parent);
            if (parent == null)
            {
                // 回归默认场景
                UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(obj, UnityEngine.SceneManagement.SceneManager.GetActiveScene());
            }

            return obj;
        }
    }

在第三层放入对象时,首先为对象设置第二层的父节点并放入容器,设置隐藏(放回对象池代表着回收不用),取出时显示对象并解开层级关系(parent代表取出的对象放到哪个对象下面,默认为null,代表对象放到最外层,此时需要从DontDestroyOnload场景回到原来的Scene)。

这里需要说明的是,第三层放入对象时,每个对象池的种类已经确定,Queue不需要做种类区分,区分普通的对象池应该是第一、二层PoolManager的工作,第三层实际是往一个确定类型的对象池内放入/取出一个或多个同类型的对象。

Object对象池

ObjectPoolData负责对PoolManager中的objectPoolDic字典中具体存储的一个Object对象池数据进行管理,类似的,其也有相应的push、get方法和构造函数。

       public ObjectPoolData(object obj)
        {
            PushObj(obj);
        }
        // 对象容器
        public Queue<object> poolQueue = new Queue<object>();

        /// <summary>
        /// 将对象放进对象池
        /// </summary>
        public void PushObj(object obj)
        {
            poolQueue.Enqueue(obj);
        }

        /// <summary>
        /// 从对象池中获取对象
        /// </summary>
        /// <returns></returns>
        public object GetObj()
        {
            return poolQueue.Dequeue();
        }

Object对象池管理C#脚本对象,面板不可见,也不需要考虑Hierarchy层级关系(压根没有PoolRoot),构造函数直接将对象放入池子内即可,放入取出逻辑也不需要考虑HIerarchy层级(Object对象池实际就是字典-队列-对象三层,而GameObject对象池除此之外还要同步维护好Hierarchy中PoolRoot-FatherRoot-具体对象三层关系)。

PoolManager

PoolManager继承自ManagerBase管理器基类,挂载在GameRoot下,在游戏运行时调用Init方法完成单例实例化,负责对以上两种对象池进行管理,两个字典gameObjectPoolDic和objectPoolDic对对象池索引进行存储,poolRootObj作为所有对象池的父节点。

		//class PoolManager
        // 根节点
        [SerializeField]
        private GameObject poolRootObj;

        /// <summary>
        /// GameObject对象容器
        /// </summary>
        public Dictionary<string, GameObjectPoolData> gameObjectPoolDic = new Dictionary<string, GameObjectPoolData>();
        /// <summary>
        /// 普通类 对象容器
        /// </summary>
        public Dictionary<string, ObjectPoolData> objectPoolDic = new Dictionary<string, ObjectPoolData>();

        public override void Init()
        {
            base.Init();
        }

GameObject相关操作

PoolManager通过GetGameObject方法获取对象池中对象,传入GameObject作为查询目标提取Name得到对象池String类型Key值,在GameObject对象池字典中查找是否存在对应的对象池,存在调用下一层GetObj方法返回GameObject,如果不存在说明对象池中没有这一个对象池,实例化一个对象返回,也可额外传入泛型参数T来获取游戏对象上的某个组件(对于组件查不到就是查不到,实例化无意义,返回Null)。

        /// <summary>
        /// 获取GameObject
        /// </summary>
        /// <typeparam name="T">你最终组件</typeparam>
        public T GetGameObject<T>(GameObject prefab, Transform parent = null) where T : UnityEngine.Object
        {
            GameObject obj = GetGameObject(prefab, parent);
            if (obj != null)
            {
                return obj.GetComponent<T>();
            }
            return null;
        }

        /// <summary>
        /// 获取GameObject
        /// </summary>
        public GameObject GetGameObject(GameObject prefab, Transform parent = null)
        {
            GameObject obj = null;
            string name = prefab.name;
            // 检查有没有这一层
            if (CheckGameObjectCache(prefab))
            {
                obj = gameObjectPoolDic[name].GetObj(parent);
            }
            // 没有的话给你实例化一个
            else
            {
                // 确保实例化后的游戏物体和预制体名称一致
                obj = GameObject.Instantiate(prefab, parent);
                obj.name = name;
            }
            return obj;
        }

设计CheckGameObjectCache方法在对象放入出过程中进行键值校对,确保对象池中有对应的一层数据记录且对象容器不为空。

        /// <summary>
        /// 检查有没有某一层对象池数据
        /// </summary>
        private bool CheckGameObjectCache(GameObject prefab)
        {
            string name = prefab.name;
            return gameObjectPoolDic.ContainsKey(name) && gameObjectPoolDic[name].poolQueue.Count > 0;
        }

设计PushGameObject方法来将GameObject放回对象池,放入的检查逻辑较简单,直接在字典查Key,有就放进去,没有添加到字典中,并调用GameObjectData构造函数创建新数据,且在创建过程中会自动把本次的对象放到容器中。

        /// <summary>
        /// GameObject放进对象池
        /// </summary>
        public void PushGameObject(GameObject obj)
        {
            string name = obj.name;
            // 现在有没有这一层
            if (gameObjectPoolDic.ContainsKey(name))
            {
                gameObjectPoolDic[name].PushObj(obj);
            }
            else
            {
                gameObjectPoolDic.Add(name, new GameObjectPoolData(obj, poolRootObj));
            }
        }

设计CheckCacheAndLoadGameObject方法对二级目录进行分割来快速获取预制体的名字,再在字典中进行查找,适用于UI资源。

        /// <summary>
        /// 检查缓存 如果成功 则加载游戏物体 不成功返回Null
        /// </summary>
        /// <returns></returns>
        public GameObject CheckCacheAndLoadGameObject(string path, Transform parent = null)
        {
            // 通过路径获取最终预制体的名称 "UI/LoginWindow"
            string[] pathSplit = path.Split('/');
            string prefabName = pathSplit[pathSplit.Length - 1];

            // 对象池有数据
            if (gameObjectPoolDic.ContainsKey(prefabName) && gameObjectPoolDic[prefabName].poolQueue.Count > 0)
            {
                return gameObjectPoolDic[prefabName].GetObj(parent);
            }
            else
            {
                return null;
            }
        }

Object对象相关操作

在普通对象相关操作中,类似的,设计GetObject、PushObject、CheckObjectCache方法实现对普通脚本对象进行收纳、检查。

        /// <summary>
        /// 获取普通对象
        /// </summary>
        public T GetObject<T>() where T : class, new()
        {
            T obj;
            if (CheckObjectCache<T>())
            {
                string name = typeof(T).FullName;
                obj = (T)objectPoolDic[name].GetObj();
                return obj;
            }
            else
            {
                return new T();
            }
        }

        /// <summary>
        /// GameObject放进对象池
        /// </summary>
        /// <param name="obj"></param>
        public void PushObject(object obj)
        {
            string name = obj.GetType().FullName;
            // 现在有没有这一层
            if (objectPoolDic.ContainsKey(name))
            {
                objectPoolDic[name].PushObj(obj);
            }
            else
            {
                objectPoolDic.Add(name, new ObjectPoolData(obj));
            }
        }

        private bool CheckObjectCache<T>()
        {
            string name = typeof(T).FullName;
            return objectPoolDic.ContainsKey(name) && objectPoolDic[name].poolQueue.Count > 0;
        }

清空对象池

清空整个对象池中所有内容(GameObjectPool,ObjectPool),GameObject删除PoolRoot下所有子物体即可并清空字典,Object清空字典即可。

          /// <summary>
        /// 删除全部
        /// </summary>
        /// <param name="clearGameObject">是否删除游戏物体</param>
        /// <param name="clearCObject">是否删除普通C#对象</param>
        public void Clear(bool clearGameObject = true, bool clearCObject = true)
        {
            if (clearGameObject)
            {
                for (int i = 0; i < poolRootObj.transform.childCount; i++)
                {
                    Destroy(poolRootObj.transform.GetChild(i).gameObject);
                }
                gameObjectPoolDic.Clear();
            }

            if (clearCObject)
            {
                objectPoolDic.Clear();
            }
        }

清空GameObjectPool或ObjectPool。

        public void ClearAllObject()
        {
            Clear(false, true);
        }
                                                                     
        public void ClearAllGameObject()
        {
            Clear(true, false);
        }                                                                     
                                                                     

清空某一类GameObject对象池,根据Name查找子节点,删除并移除字典记录即可。

         public void ClearGameObject(string prefabName)
        {
            GameObject go = poolRootObj.transform.Find(prefabName).gameObject;
            if (go != null)
            {
                Destroy(go);
                gameObjectPoolDic.Remove(prefabName);
            }

        }     

        public void ClearGameObject(GameObject prefab)
        {
            ClearGameObject(prefab.name);
        }                                                                     

清空某一类Object对象池,传泛型或者Type移除字典记录即可。

        public void ClearObject<T>()
        {
            objectPoolDic.Remove(typeof(T).FullName);
        }
  
        public void ClearObject(Type type)
        {
            objectPoolDic.Remove(type.FullName);
        }                                                                     

PS:上文所说的三层是从数据角度描述的,从代码实现角度是两层,PoolManager持有PoolRoot和每个池子的父节点是第一层,具体的池子数据是第二层。

使用示例

链接