Unity 内存池设计理解

内存池是所有游戏制作中必须的模块,之前做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(){
	//ctor
	public __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 ctor
	public _PoolPrefabManage(){
		_prefabs = new List<GameObject>();
	}
	#endregion

	void 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设计结束,一天的时候搞出来的比不上成熟插件,但是如果有其他喜欢自己写模块的同学

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

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值