在游戏开发中,我们常常遇到在一帧中需要创建大量的对象。比如飞行设计游戏中的子弹,和刷新出来的敌机,这些对象我们需要在一帧中大量创建,而对象不使用时需要大量的销毁操作。而在一帧中大量的创建和销毁操作不仅会占用大量的系统资源和计算时间,也会产生大量的内存碎片,严重的话这会拖慢游戏进程的时间线导致卡顿,也会让大量小的内存碎片无法分配给大的内存块使用。就像下图所示总量充足的内存,其连续内存却十分匮乏。
我们会发现上面举的例子中的对象我们是可以复用的。无非是在使用时从新初始化它的数据就可以了。我们可以使用固定的对象池重用对象,取代单独地分配和释放对象,以此来达到提升性能和优化内存使用的目的。对于内存管理器而言,我们仅分配一大块内存直到游戏结束才释放它,对于内存池的使用者而言,我们可以按照自己的意愿来分配和释放对象。
在以下情况下你需要使用对象池:
1.当你需要频繁地创建和销毁对象时。
2.对象的大小一致时。
3.在堆上进行对象内存分配较慢或者会产生内存碎片时。
4.每个对象封装着获取代价昂贵且可重用的资源,如数据库、网络的连接。
之前在用C++,lua基于cocos2dx进行开发游戏时,我们一般都会在框架里面实现一个对象池。现在CocosCreator帮我们设计了一个对象池,你可以很方便的使用了。
//创建一个对象池
this.myPool = new cc.NodePool('MyTemplateHandler');
// 创建对象,并把它放到对象池中。回收对象也使用put
let myNode = cc.instantiate(this.template);
this.myPool.put(myNode);
// 通过get方法从对象池中拿到一个闲置对象
let newNode = this.myPool.get();
// 我们可以通过size判断对象池中有多想可用对象
this.myPool.size() > 0;
// 通过clear销毁对象池中缓存的所有节点
this.myPool.clear();
具体使用参考官方文档https://docs.cocos.com/creator/manual/zh/scripting/pooling.html
在《Game Programming Patterns》一书中举了一个粒子渲染的粒子。实际上大多数的粒子系统也是离不开对象池的。因为大量的粒子频繁的创建销毁,正符合对象池的应用场景,把不用的粒子对象回收到对象池中,等需要时再取出一个闲置状态的对象,进行初始化,然后就可以进行粒子对应的算法运算了。
书中还列举了一些小技巧。比如为闲置的对象维护一个空闲表时,为了减少空闲列表的内存开销,直接在粒子对象中使用union。因为粒子对象闲置时,那些被占用时需要的状态是用不到,这时候我们就可以使用联合,利用这块内存保存闲置对象链表的下一个对象。还有一种方式就是在数据局部性一章里提到的在对象池的头部保存活跃的对象,后面保存闲置的对象。当需要一个对象时,就把第一个不活跃的对象状态设置为活跃状态。当回收一个对象时,就把他和最后一个活跃的对象交换一个位置,然后把回收对象的状态设置为闲置即可。
使用对象池将那些类型相同的对象在内存上整合,能够帮助你在遍历这些对象时利用好CPU的缓存区。
参考:Object Pool · Optimization Patterns · Game Programming Patterns