内存池是所有游戏制作中必须的模块,之前做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设计结束,一天的时候搞出来的比不上成熟插件,但是如果有其他喜欢自己写模块的同学
可以过来一起研究下。里面还是花了些心思的。