Unity 内存池设计理解

来源:互联网 发布:上海威纳的数据怎么样 编辑:程序博客网 时间:2024/06/03 19:01

内存池是所有游戏制作中必须的模块,之前做cocos游戏的时候习惯制作一个工厂类,用来动态管理精灵等资源,让游戏场景中大量动态产生和销毁的对象进行复用,核心代码(c++版),后面再说Unity的:

class RecycleFactory{public:    DEFINE_SINGLETON(RecycleFactory);        /**     *  从回收链表中生成     *     *  @param RECYCLE_TYPE_ 回收类型     *  @param T    当前类型     *     *  @return node     */    template <class T>    CCNode* getOne(RECYCLE_TYPE_ _t, T* obj){        if(!m_recycleArray[_t]){            m_recycleArray[_t] = new CCArray();        }                if(0 == m_recycleArray[_t]->count()){            obj = T::createObj();        }else{            obj = (T*)m_recycleArray[_t]->lastObject();            m_recycleArray[_t]->removeLastObject();        }        return obj;    }        /**     *  回收node     *  @param RECYCLE_TYPE_    回收类型     *  @param cocos2d::CCNode*  回收对象     */    template<typename T>    void recycleOne(RECYCLE_TYPE_ _type,T _t){        if(!m_recycleArray[_type]){            m_recycleArray[_type] = new cocos2d::CCArray();        }else{            m_recycleArray[_type]->addObject(_t);        }    }        /**     *  @param RECYCLE_TYPE_    回收类型     *  清除所有回收     */    void removeAllRecycle(RECYCLE_TYPE_);    void cleanUpAllRecycle();    private:    RECYCLE_TYPE_ m_type;    //回收链表    cocos2d::CCArray *m_recycleArray[RECYCLE_SUM_];};
实现就不介绍了,无非是维护一个链表的增减,保证内存不会频繁的申请和销毁。

这样设计的好处很明显就是通用性很强,基本上定义一种回收的类型就直接使用接口就ok了,内存交给cocos底层去处理。


最近开始做Unity,内存回收模块又要重写一遍,不过Unity的资源还是超丰富的,各种现成插件比如PoolManager都可以找到,可能cocos时间做长了,别人写的东西不搞清楚实在不想拿过来用,而且原理比较简单,所以还是喜欢自己写一套,设计方面如果有欠缺的地方欢迎指正,本文只当做个笔记。

开始设计

首先,内存池必须要满足的需求列一下:

1、支持所有类型的回收(自定义类或者prefab等)

2、实现单例(个人认为还是工厂单例模式更省内存)

3、简单灵活(不需要考虑状态重置等因素,这些交给内存池对象的脚本去做吧)

一个个分析一下:

第一个支持所有类型,那肯定就是泛型了

第二个我辩解一下,可能很多人不认同,单例好处是代码耦合低,内存占用比较少。当然缺点也有,无法被继承(与第一点可能有冲突),但是我还是喜欢用单例去控制内存池,安全性和代码耦合方面还是放心的,如果用单例,我特别反感在单例中定义public对象用来在Inspector中拖拽,不是不可以,感觉怪怪的,单例类挂载到一个对象中还要依赖这个对象的生命周期,对与我这个全局有效的单例类来说很是难受,虽然单例类不会被销毁,但是必然造成父亲的丢失等不利情况。然后拖拽对象的做法灵活但不长久,游戏做复杂后,如果内存池中的对象种类增加或者减少,我们还要操作Inspector界面?所以,我认为动态加载即可,虽然费代码,但是更加灵活可控,我们是在做内存池,编辑器少用点没什么大不了,又不是做界面。方案确定:单例+动态加载内存池对象。

第三个很多网友写PoolManger的时候都考虑了对象状态的重置封装,我个人认为封装有点过度,这些交给对象的脚本完成更好,内存池就是管理这个对象的出生和死亡,不必要控制它中途状态的变化,我设计中在复用内存池对象的时候返回这个实例,状态的调整交给这个实例脚本去完成就好了,这样代码耦合会更低,灵活度更高,极大可能的让poolMangager适应所有项目。

我整理了下架构图:

解释下这个架构的原因:

之前听过一次Unity框架方面的技术交流,内存池的分类的划分是必要的,每一种预设体最好都是单独的一个池子,我这边原则大致一样,因为

不同种类的预设放在一个池子里很难管理,所以我把功能相同的预设体归为一类(比如挂载相同脚本,实现相同功能的预设体,像游戏中的子弹)

,每种类型我们都派生出一个内存池分别管理他们,只要基类设计 的完善,你会发现这不是什么费时的事。


开始写代码了

如果读到这里的朋友可能会预料到下面的问题。

开始上手的时候发现有些尴尬的地方,首先我希望用单例,但是单例是不能继承的,so只能是末端的派生类为单例,

基类全部不要是单例(单例的构造是私有的,不信可以试试)。

还有,我希望池子有自动销毁功能,我还是把总基类继承了MonoBehaviour,因为我在管理prefab对象的时候不能Destroy它!!

代码贴上

总基类

/// <summary>/// 对象池总基类,比如预设体对象池可以继承_PoolbaseManager<GameObject>/// 这个对象池不提供自动销毁机制,但是提供了计时器,有需要可以写销毁逻辑/// 对象池是单例模式,所以只支持动态加载/// 提供了预加载功能接口:__preLoadAnyOne  原理是先实例化内容后放入池中使用/// </summary>public class __PoolBaseManager<T> : MonoBehaviour where T : class ,new(){//ctorpublic __PoolBaseManager(){//初始化属性__initAttribute();}private int     __F = 0;       //帧数private List<T> __POOLLIST;           //池子public int      __MAXCAMACITY         {get;set;}   //最大容器数量public int      __FRAMEDESTROYCOUNT   {get;set;}   //每多少帧销毁public bool     __ISAUTODESTROY       {get;set;}   //是否自动销毁/// <summary>/// 初始化属性,默认池子容量50个,如果自动销毁的话5帧销毁一个/// 如果尺寸属性需要修改,直接调用此方法即可/// </summary>public void __initAttribute(int __mc = 50,int __fd = 5){//初始化相关属性 __POOLLIST     = new List<T>();__MAXCAMACITY       = __mc;__FRAMEDESTROYCOUNT = __fd;}/// <summary>/// 预加载一批对象/// __id : 种类/// </summary>/// <param name="preLoadNum">Pre load number.</param>public void __preLoadAnyOne(int __preLoadNum,int __id = -1){__MAXCAMACITY = (__preLoadNum > __MAXCAMACITY) ? __preLoadNum:__MAXCAMACITY;for(int __i = 0; __i<__preLoadNum; __i++){T __t = __instantiate(__id);__recycleOne(__t);}}/// <summary>/// 预加载一个对象/// __id : 种类/// </summary>/// <param name="preLoadNum">Pre load number.</param>public T __preLoadAnyOne(int __id = -1){T __t = __instantiate(__id);__recycleOne(__t);return __t;}/// <summary>/// 生成一个/// </summary>public T __produceOne(int __id = -1){T __t = null;if(0 == __POOLLIST.Count){__t = __instantiate(__id);}else{__t = __POOLLIST[0];__POOLLIST.RemoveAt(0);}__produceOneFinish(__t);return __t;}/// <summary>/// 回收/// </summary>/// <returns>The one.</returns>public void __recycleOne(T __t){__recycleAction(__t);__POOLLIST.Add(__t);}/// <summary>/// 实例化,应该是个纯虚函数/// __id 随机的对象池对象种类下标,-1代表随机/// </summary>public virtual T __instantiate(int __id){return new T();}/// <summary>/// 实例化或者重用结束,可以在这个地方重新预加载或者重置状态的方法/// </summary>/// <param name="t">T.</param>public virtual void __produceOneFinish(T __t){}/// <summary>/// 回收,纯虚函数/// </summary>/// <param name="t">T.</param>public virtual void __recycleAction(T __t){}/// <summary>/// 清理管理器/// 比如池子中放的是gameobject,那这个方法需要重写:MonoBehaviour.Destroy(this.gameObject);/// </summary>public virtual void __destroyManager(){}/// <summary>/// 从顶部移除/// </summary>public void __DestroyFromHead(){//从头部删除if(__POOLLIST.Count > 0 && __POOLLIST[0] != null){__destroy(__POOLLIST[0]);__POOLLIST.RemoveAt(0);}}/// <summary>/// 销毁单例/// </summary>public void __DestroyManager(){__destroyManager();}/// <summary>/// 清理(从内存中)/// </summary>/// <param name="t">T.</param>public virtual void __destroy(T t){}/// <summary>/// 清理,如果对象没有标记DonotDestroyOnLoad 跳转场景要清理 /// </summary>public void __clear(){__POOLLIST.Clear();}/// <summary>/// 自动销毁的计时器/// </summary>void Update(){if(__ISAUTODESTROY){__F += 1;if(__F >= __FRAMEDESTROYCOUNT){__F = __F - __FRAMEDESTROYCOUNT;__DestroyFromHead();}}}/// <summary>/// 自动销毁/// </summary>public void __autoDestroy(){__ISAUTODESTROY = true;}}
里面很多虚函数(想写纯虚函数,c#不支持吧?)需要重写,包括对象的实例方法和销毁方法,里面也有预加载的方法,放在loading中调用,可以让界面流畅

PrefabPoolManager类(主要以这个为例,大多数我们操作的都是游戏中的perfab)

/// <summary>/// 预设体管理池/// 我们希望预设体管理池能有一下功能/// 1、预设体都是GameObject对象,所以它继承_PoolBaseManager<GameObject>/// 2、因为最后我们使用的内存池管理都是单例,所以只能动态加载预设体(实际稍微复杂的游戏,需要内存池控制的对象群一般都是要动态的,比如跑酷游戏/// 场景很多,每个场景障碍物的预设体不同,放在不同的资源路径下,如果不用动态控制很难统一管理,所以我把目前需要使用内存池的预设体都做成动态加载),/// 既然动态加载,所有这些预设体分门别类放入Resources资源下,等待load/// 3、我们希望预设体可以被预先加载,所以提供预先加载的接口:/// </summary>public class _PoolPrefabManage: __PoolBaseManager<GameObject>{//当前种类的预设体群(动态加载)public List<GameObject> _prefabs;#region ctorpublic _PoolPrefabManage(){_prefabs = new List<GameObject>();}#endregionvoid Awake(){//初始化prefab_reLoadPrefabs();}/// <summary>/// 如果移除管理器,清理链表/// </summary>public virtual void OnDestroy(){//销毁管理器的时候会自动调用清理池子的操作//(销毁管理器有两种方式:1、当池子切换场景不会销毁的时候,在合适的时机调用总基类的__DestroyManager方法;2、当池子切换场景被销毁的时候自动执行)// 也就是说当池子管理器实例化的过程中调用了DontDestroyOnLoad(singleton)  那么请在合适的时候调用__DestroyManager方法手动移除它// 当池子管理器实例化的过程中没有调用DontDestroyOnLoad(singleton) 那么切换场景的时候自然会走进来,总之,这边做池子销毁后的通用逻辑即可//清理链表__clear(); }/// <summary>/// 清理管理器/// </summary>public override void __destroyManager(){MonoBehaviour.Destroy(this.gameObject);}/// <summary>/// 实例化,应该是个纯虚函数/// </summary>public override GameObject __instantiate(int _id){if(0 == _prefabs.Count) return null;//随机从预设体群中实例化if(-1 == _id) return (GameObject)MonoBehaviour.Instantiate(_prefabs[Random.Range(0,_prefabs.Count-1)]);else return (GameObject)MonoBehaviour.Instantiate(_prefabs[_id]);}/// <summary>/// 生成对象结束/// </summary>/// <param name="t">T.</param>/// <param name="o">O.</param>public override void __produceOneFinish(GameObject o){o.SetActive(true);}/// <summary>/// 回收,预设体我们希望直接隐藏,等待下次使用/// </summary>/// <param name="t">T.</param>public override void __recycleAction(GameObject o){o.SetActive(false);}/// <summary>/// 清理(从内存中)/// </summary>/// <param name="t">T.</param>public override void __destroy(GameObject o){MonoBehaviour.Destroy(o);}/// <summary>/// 方法必须重写,加载预设的内容不一样/// </summary>public virtual void _reLoadPrefabs(){}/// <summary>/// 获取预设种类数量/// </summary>public int _getPrefabsTypesNum(){return _prefabs.Count;}}
好,基类已经设计差不多了,后面写具体的池子方法(单例)

/// <summary>/// 障碍物预设体池/// 必须写单例/// </summary>public class ObstaclePrefabPoolManager : _PoolPrefabManage {private static ObstaclePrefabPoolManager _instance;  private static object _lock = new object();  public static ObstaclePrefabPoolManager Instance  {  get  {  lock (_lock)  {  if (_instance == null)  {  _instance = (ObstaclePrefabPoolManager)FindObjectOfType(typeof(ObstaclePrefabPoolManager));  if (FindObjectsOfType(typeof(ObstaclePrefabPoolManager)).Length > 1)  {  Debug.LogError("[Singleton] Something went really wrong " +  " - there should never be more than 1 singleton!" +  " Reopening the scene might fix it.");  return _instance;  }  if (_instance == null)  {  GameObject singleton = new GameObject();  _instance = singleton.AddComponent<ObstaclePrefabPoolManager>();  singleton.name = "(singleton) " + typeof(ObstaclePrefabPoolManager).ToString();  //切换场景销毁//DontDestroyOnLoad(singleton);  Debug.Log("[Singleton] An instance of " + typeof(ObstaclePrefabPoolManager) +  " is needed in the scene, so '" + singleton +  "' was created with DontDestroyOnLoad.");  }  else  {  Debug.Log("[Singleton] Using instance already created: " +  _instance.gameObject.name);  }  }  return _instance;  }  }  set  {  if (_instance != null)  {  Destroy(value);  return;  }  _instance = value;  //切换场景销毁//DontDestroyOnLoad(_instance);  }  }  /// <summary>/// 添加预设到_prefabs中/// </summary>public override void _reLoadPrefabs(){int i = 1;while(true){GameObject o = (GameObject)Resources.Load(string.Format("GamePrefabs/Obstacles_Prefabs/Obstacle_map1_{0}",i));if(o)  _prefabs.Add(o);else return;i++;}}}

c#啊,你怎么不能多继承啊,写个继承MonoBehivour的单例还是很费代码的。


使用方法:这些脚本不需要挂载到对象中,设计好每个池子的_reLoadPrefabs方法,把对象模板放进去,然后调用

__preLoadAnyOne预加载接口

__produceOne生成对象接口

__recycleOne回收接口就可以了

因为它继承MonoBehaviour,它的OnDestroy()会处理池子回收链表的,所以不用担心,

如果担心内存一直被池子内容占用,可以调用autoDestory接口,每5帧destroy一个,也可以自己写,update方法都

可以重写的。

至此,我这个PoolManager设计结束,一天的时候搞出来的比不上成熟插件,但是如果有其他喜欢自己写模块的同学

可以过来一起研究下。里面还是花了些心思的。

原创粉丝点击