Unity框架之对象池GameObjectPool

Unity框架之对象池GameObjectPool

对象池的核心思想

将需要频繁创建销毁的游戏对象缓存起来,将创建销毁行为替换成显示和隐藏,大大提高游戏运行效率。

典型的以空间换时间的思想

对象池的使用流程

在这里插入图片描述

对象池的设计

根据上图对象池的使用流程分析对象池的特点

  • 每个类型的对象应该有属于自己的独立的池子(子弹复用子弹对象,不能获取子弹却返回一个敌人对象)
    • 考虑用字典的键值对 (名称 , 池)–(key,value)的形式缓存每个独立的池子
  • 每个独立的池子中保存着若干个游戏对象供使用
    • 池:List<GameObject>用列表存储个数不固定的游戏对象
  • 需要提供一个获取游戏对象的方法和回收游戏对象的方法
  • 全局唯一且经常使用,考虑使用单例模式

通用的对象池框架

对象池的数据结构

根据设计时的分析,应该用(key,List)结构的字典

private Dictionary<string, List<GameObject>> objCache; //创建对象缓存字典
//单例模式提供的Init初始化方法
protected override void Init()
{
	base.Init();
	//初始化字典
	objCache = new Dictionary<string, List<GameObject>>();
}
关键方法:获取游戏对象
		/// <summary>
        /// 创建对象(从对象池创建/读取对象)
        /// </summary>
        /// <param name="key">类别---自行定义</param>
        /// <param name="prefab">需要创建实例的预制件</param>
        /// <param name="pos">创建位置</param>
        /// <param name="rotate">创建角度</param>
        /// <returns></returns>
        public GameObject CreateObject(string key,GameObject prefab,Vector3 pos , Quaternion rotate)
        {
            GameObject go;
            go = FindUsableObject(key);  //查找是否有可用的对象 若无则返回null
            //若没有查找到--没有键/没有空闲对象
            if(go == null)
            {
                //添加对象
                go = AddObject(key, prefab);
            }   
            
            UseObject(go,pos,rotate); //使用对象 设置基本的位置和旋转

            return go;
            
        }
        /// <summary>
        /// 查找是否有可用的对象
        /// </summary>
        private GameObject FindUsableObject(string key)
        {

            //List的Find也是委托,类似于ArrayHelper自己定义的,可同样使用
            if (objCache.ContainsKey(key))
            {
                //返回有被禁用的物体,若无则返回null
                return objCache[key].Find(go => !go.activeInHierarchy);
            }
            else return null;
            
        }
        /// <summary>
        /// 向对象池中添加对象
        /// </summary>
        /// <param name="key">类别</param>
        /// <param name="prefab">预制件</param>
        /// <returns>返回实例对象</returns>
        private GameObject AddObject(string key,GameObject prefab)
        {
            //创建预制件的实例对象
            GameObject go = Instantiate(prefab);
            //如果缺少键则创建键
            if (!objCache.ContainsKey(key)) objCache.Add(key, new List<GameObject>());
            //向对象池中添加对象
            objCache[key].Add(go);

            return go;
        }

        /// <summary>
        /// 使用对象(配置对象的一些位置和旋转)
        /// </summary>
        /// <param name="go">实例对象</param>
        /// <param name="pos">位置</param>
        /// <param name="rotate">旋转</param>
        private void UseObject(GameObject go,Vector3 pos,Quaternion rotate)
        {
            go.transform.position = pos;
            go.transform.rotation = rotate;
            go.SetActive(true);

            //遍历执行所有需要被重置的逻辑(实现了IResetable接口的脚本)后面会进行解释
            foreach (var item in go.GetComponents<IResetable>())
                item.onReset();
            
        }
关键方法:回收游戏物体

这里提供三种回收游戏物体的方法

  • 回收单个游戏对象(支持延迟回收)
  • 回收一类游戏对象
  • 回收全部的游戏对象
		/// <summary>
        /// 回收对象
        /// </summary>
        /// <param name="go">实例对象</param>
        /// <param name="delay">延迟时间(默认参数)</param>
        public void CollectObject(GameObject go,float delay=0)
        {
            //延迟调用
            StartCoroutine(CollectObjectDelay(go,delay));
        }

        private IEnumerator CollectObjectDelay(GameObject go,float delay)
        {
            yield return new WaitForSeconds(delay);
            go.SetActive(false);
        }

        /// <summary>
        /// 清楚指定类别的对象
        /// </summary>
        /// <param name="key">类别</param>
        public void Clear(string key)
        {
            //Destroy
            if(objCache.ContainsKey(key))
            {
                foreach (GameObject obj in objCache[key])
                    Destroy(obj);
                objCache.Remove(key);
            }
        }
        /// <summary>
        /// 清除对象池中所有内容
        /// </summary>
        public void ClearAll()
        {
            //foreach只允许读元素,不允许修改删除等
            //可以通过new List<string>(objCahce.Keys)解决
            foreach(var item in new List<string>(objCache.Keys))
            {
                //原理,遍历的List元素,删除的字典 遍历A删B 不允许遍历B删B
                Clear(item);
            }
        }
对象池的拓展

使用对象池最容易的易错点也是难点就是 游戏对象的复用

如果对象每次使用都仅需要修改位置和旋转,则当前版本的对象池已经可以完美做到。

但如果一些对象每次生成后都要注册一些事件,动态生成一些属性等等,每次复用都要注销上一次的这些引用,替换成当前所需要的新事件,新引用。

  • 一种解决方案是在此对象的每个脚本的OnEnable和OnDisable中添加需要复用重置的属性逻辑,但这样做会造成紧耦合,并且不利于解决一些较为复杂的属性重置复用。
  • 此通用对象池框架提供一个IResetable接口,每次获取对象时,都会查询对象身上是否有实现此接口的脚本,然后调用此接口的onReset()方法,这样当一个较为复杂的对象复用时,就可以在相应脚本实现此接口,在接口中的OnReset方法中完成属性的重置。
public interface IResetable
{
    //用以重置对象池中的对象
    void onReset();
}

对象池的完整源码

本对象池用到了Mono脚本的单例模式,有关单例模式较为简单,读者可以自行查阅了解,笔者在此也贴上源码供参考使用

MonoSingleton.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Common
{
    ///<summary>
    ///脚本单例类,负责为唯一脚本创建实例
    ///<summary>

    public class MonoSingleton<T> : MonoBehaviour where T:MonoSingleton<T> //注意此约束为T必须为其本身或子类
    {
        /*
        相较于直接在需要唯一创建的脚本中创建实例,Awake初始化的过程需要解决的问题
        1.代码重复
        2.在Awake里面初始化,其它脚本在Awake中调用其可能会为Null的异常情况
         */

        //解决1:使用泛型创建实例   解决2:使用按需加载(即有其它脚本调用时在get中加载)

        private static T instance; //创建私有对象记录取值,可只赋值一次避免多次赋值

        public static T Instance
        {
            //实现按需加载
            get
            {
                //当已经赋值,则直接返回即可
                if (instance != null) return instance;

                instance = FindObjectOfType<T>();

                //为了防止脚本还未挂到物体上,找不到的异常情况,可以自行创建空物体挂上去
                if (instance == null)
                {
                    //如果创建对象,则会在创建时调用其身上脚本的Awake即调用T的Awake(T的Awake实际上是继承的父类的)
                    //所以此时无需为instance赋值,其会在Awake中赋值,自然也会初始化所以无需init()
                    /*instance = */
                    new GameObject("Singleton of "+typeof(T)).AddComponent<T>();
                }
                else instance.Init(); //保证Init只执行一次

                return instance;

            }
        }

        private void Awake()
        {
            //若无其它脚本在Awake中调用此实例,则可在Awake中自行初始化instance
            instance = this as T;
            //初始化
            Init();
        }

        //子类对成员进行初始化如果放在Awake里仍会出现Null问题所以自行制作一个init函数解决(可用可不用)
        protected virtual void Init()
        {

        }
    }

}

GameObjectPool.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Common
{
    public interface IResetable
    {
        //用以重置对象池中的对象
        void onReset();
    }


    ///<summary>
    ///对象池--唯一
    ///<summary>
    public class GameObjectPool : MonoSingleton<GameObjectPool>
    {
        private Dictionary<string, List<GameObject>> objCache; //创建对象缓存字典

        protected override void Init()
        {
            base.Init();
            //初始化字典
            objCache = new Dictionary<string, List<GameObject>>();
        }


        /// <summary>
        /// 创建对象(从对象池创建/读取对象)
        /// </summary>
        /// <param name="key">类别---自行定义</param>
        /// <param name="prefab">需要创建实例的预制件</param>
        /// <param name="pos">创建位置</param>
        /// <param name="rotate">创建角度</param>
        /// <returns></returns>
        public GameObject CreateObject(string key,GameObject prefab,Vector3 pos , Quaternion rotate)
        {
            GameObject go;
            go = FindUsableObject(key);  //查找是否有可用的对象 若无则返回null
            //若没有查找到--没有键/没有空闲对象
            if(go == null)
            {
                //添加对象
                go = AddObject(key, prefab);
            }   
            
            UseObject(go,pos,rotate); //使用对象 设置位置旋转和启用

            return go;
            
        }
        /// <summary>
        /// 查找是否有可用的对象
        /// </summary>
        private GameObject FindUsableObject(string key)
        {

            //List的Find也是委托,类似于ArrayHelper自己定义的,可同样使用
            if (objCache.ContainsKey(key))
            {
                //返回有被禁用的物体,若无则返回null
                return objCache[key].Find(go => !go.activeInHierarchy);
            }
            else return null;
            
        }
        /// <summary>
        /// 向对象池中添加对象
        /// </summary>
        /// <param name="key">类别</param>
        /// <param name="prefab">预制件</param>
        /// <returns>返回实例对象</returns>
        private GameObject AddObject(string key,GameObject prefab)
        {
            //创建预制件的实例对象
            GameObject go = Instantiate(prefab);
            //如果缺少键则创建键
            if (!objCache.ContainsKey(key)) objCache.Add(key, new List<GameObject>());
            //向对象池中添加对象
            objCache[key].Add(go);

            return go;
        }

        /// <summary>
        /// 使用对象(配置对象的一些位置和旋转)
        /// </summary>
        /// <param name="go">实例对象</param>
        /// <param name="pos">位置</param>
        /// <param name="rotate">旋转</param>
        private void UseObject(GameObject go,Vector3 pos,Quaternion rotate)
        {
            go.transform.position = pos;
            go.transform.rotation = rotate;
            go.SetActive(true);

            //遍历执行所有需要被重置的逻辑(实现了IResetable接口的脚本)
            foreach (var item in go.GetComponents<IResetable>())
                item.onReset();
            
        }

        /// <summary>
        /// 回收对象
        /// </summary>
        /// <param name="go">实例对象</param>
        /// <param name="delay">延迟时间(默认参数)</param>
        public void CollectObject(GameObject go,float delay=0)
        {
            //延迟调用
            StartCoroutine(CollectObjectDelay(go,delay));
        }

        private IEnumerator CollectObjectDelay(GameObject go,float delay)
        {
            yield return new WaitForSeconds(delay);
            go.SetActive(false);
        }

        /// <summary>
        /// 清楚指定类别的对象
        /// </summary>
        /// <param name="key">类别</param>
        public void Clear(string key)
        {
            //Destroy
            if(objCache.ContainsKey(key))
            {
                foreach (GameObject obj in objCache[key])
                    Destroy(obj);
                objCache.Remove(key);
            }
        }
        /// <summary>
        /// 清除对象池中所有内容
        /// </summary>
        public void ClearAll()
        {
            /*
            foreach(var item in objCache)
            {
                foreach (var obj in item.Value)
                    Destroy(obj);
            }
            objCache.Clear();
            */

            //foreach只允许读元素,不允许修改删除等
            //可以通过new List<string>(objCahce.Keys)解决
            foreach(var item in new List<string>(objCache.Keys))
            {
                //原理,遍历的List元素,删除的字典 遍历A删B 不允许遍历B删B
                Clear(item);
            }
        }

    }

}

  • 9
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值