本文首发于:https://github.com/bigo-frontend/blog/ 欢迎关注、转载。
egret是一款小游戏开发引擎,支持跨平台开发,之前使用这款引擎开发了一款捕鱼游戏,在这里简单聊下再egret中关于对象池的使用,虽然该引擎已经停止维护了,但是对象池的概念适合通用的游戏场景,不限框架。
起因
关于对象池的使用主要是有两个场景,一个是捕鱼游戏里面鱼的生成,还有一个是射击过程中子弹的生成。当用户处于游戏时,这两个场景会频繁的生成对象并展示对应的动画,会造成大量的内存碎片以及频繁的分配内存空间。而通过对象池能够比较好的解决这个问题。
对象池模式
首先定义一个池对象,用来存储可复用的对象。池对象包含两组列表,一组为可复用hash对象,一组为正在使用的hash对象。在游戏加载游戏资源的时候,对池对象进行初始化,创建了一个鱼对象集合,并且同时初始化可复用hash对象作为备用池。
当游戏资源加载完成的同时,向对象池尝试获取一个可复用的鱼对象,对象池查询可复用hash对象是否存在可以复用的鱼对象,存在可复用对象的时候将对象进行初始化并将对象的索引从可复用hash对象中移除加入正在使用的hash对象。当鱼对象在游戏舞台中被移除的时候,将其进行回收到可复用hash对象等待下一轮渲染。通过这种方式避免了大量的重复对象的创建跟销毁,减少不必要的内存分配。
实现方式
首先设计一个工厂方法,实现一个FishGenerator类,负责各种类型Fish的生成,Distributor类负责各类鱼的回收与存储。
鱼的实现如下,前期开发过程中,不同种类的鱼没有单独分开类实现,都是通过传入骨骼动画与类型内部实现的,所以这里通过 type
来识别不同的鱼进行获取与复用。 hashc
是 egret
本身的 hashCode
,通过 hashc
允许外部访问,并且由于其具有唯一性,所以将其作为存储管理的下标。
interface IFish {
hashc:number; //hashCode
type:string; //类型标识
isIdle:boolean; //标记是否空闲
lastUsed: number // 上次使用时间
dispose():void; //释放对象内部引用
del():void; //彻底释放对象
reset():void; //重置
setProtocol( val:IDistributor ):void; //设置协议
}
Distributor
的实现如下, distribution
负责将生成或者复用的鱼分配到对应的hash对象,并删除其再原先hash对象上的引用;
clearRegularly
则是定时清除过久未复用的鱼对象,减少复用hash对象所占的内存
interface IDistributor {
distribution(val:IFish):void; //分配
addFish(val:IFish):void; //添加元素
getFish(type:string):IFish; //获取元素
clear():void; //清除所有未使用的对象
clearRegularly(): void; // 定期清除过久未使用的对象
}
BashFish
源代码如下, reset
方法重置鱼的空闲状态,重新在空闲池跟使用池之间进行分配,记录鱼的使用时间作为定期清除的标识。 dispose
方法则是释放鱼,将鱼的状态重置为空闲状态,并将鱼重新分配。
class FishBase extends egret.Sprite implements IFish {
private _isIdle: boolean = false;
private _dis:IDistributor = null;
public type:string = '';
public lastUsed: number = 0;
...
//more code
...
constructor(factory: dragonBones.EgretFactory, config: fishTypeConfig) {
super();
this.type = config.type;
...
// more code
...
}
public reset():void {
// 重置当前鱼的对象空闲状态为false,并且更新使用时间,将其进行对象的交互
this._isIdle = false;
this.lastUsed = Date.now();
this._dis.distribution(this);
}
public get hashc():number {
return this.hashCode;
}
public get isIdle():boolean {
return this._isIdle;
}
public dispose():void {
this._isIdle = true;
// 重新进行分配
this._dis.distribution(this);
}
public del():void {
this.dispose();
this._dis = null;
}
public setProtocol( val:IDistributor ):void {
// 建立Distributor链接
this._dis = val;
}
// 离开舞台后移除鱼
remove() {
this.dispose();
}
...
// more code
...
}
...
FishGenerator
源代码如下,FishGenerator
对外暴露expansion
和getFish
方法,expansion
方法是当空闲池不足时,一次性创建大量的空闲的鱼备用,getFish
方法是通过Distributor
获取空闲池中的鱼,当空闲池中的鱼不足的时候创建新的鱼分配到空闲池中并将鱼进行返回。
class FishGenerator {
private _dis:IDistributor = null;
...
//more code
...
public constructor( val:IDistributor ) {
this.init(val);
}
private init(val:IDistributor):void {
this._dis = val;
}
public expansion(factoryList: any, num) {
const len = factoryList.length;
for (let i = 0; i < len; i += 1) {
const item = factoryList[i];
const fish = this.createFish(item.factory, item.config);
this._dis.addFish(fish);
fish.reset();
}
}
public getFish(factory: any, config:any):IFish {
let fish:IFish = this._dis.getFish(config.type);
// 无法进行复用,则创建一个新的鱼对象
if(fish === null) {
fish = this.createFish(factory, config);
this._dis.addFish(fish);
fish.reset();
}
return fish;
}
private createFish(factory: any, config:any):IFish {
return new FishBase(factory, config);
}
...
//more code
...
}
分配器源代码如下,对外提供distribution
,addFish
, getFish
,clear
以及clearRegularly
方法
distribution
:根据鱼的状态进行重新分配
addFish
:建立鱼跟Distributor
的链接,并将鱼分配到空闲池
getFish
:获取可复用的鱼
clear
:清除空闲鱼群
clearRegularly
: 获取鱼次数达到500的倍数的时候,清除过久未使用的空闲鱼群
class Distributor implements IDistributor {
private _UsedPool:Object = null; //使用中的鱼
private _IdlePool:Object = null; //空闲的鱼
private count:number= 0;
...
//more code
...
public constructor() {
this._IdlePool = {};
this._UsedPool = {};
}
// 将鱼进行重新分配,空闲状态分配到空闲hash对象,使用中分配到使用hash对象
// 移除原先的引用
public distribution( val:IFish ):void {
if(val.isIdle) {
this._IdlePool[val.hashc] = val;
delete this._UsedPool[val.hashc];
} else {
this._UsedPool[val.hashc] = val;
delete this._IdlePool[val.hashc];
}
}
public addFish(val:IFish):void {
val.setProtocol(this);
if(val.isIdle) {
this._IdlePool[val.hashc] = val;
} else {
this._UsedPool[val.hashc] = val;
}
}
public getFish(type:string):IFish {
this.count += 1;
let obj:IFish = null;
const keys = Object.keys(this._IdlePool);
for (let i = 0,len = keys.length; i < len; i += 1) {
const key = keys[i];
obj = this._IdlePool[key] as IFish;
// 如果类型一致,则为可复用对象,返回引用并重置状态
if (obj.type === type) {
obj.reset();
if (this.count % 500 === 0) this.clearRegularly();
return obj;
}
}
if (this.count % 500 === 0) this.clearRegularly();
return null;
}
public clear():void {
let obj:IFish = null;
for (let key in this._IdlePool) {
obj = this._IdlePool[key] as IFish;
// 释放与Distributor的链接
obj.del();
}
this._IdlePool = null;
this._IdlePool = {};
}
private clearRegularly() {
const cur = Date.now();
const keys = Object.keys(this._IdlePool);
for (let i = 0,len = keys.length; i < len; i += 1) {
const key = keys[i];
const obj = this._IdlePool[key] as IFish;
// 复用hash中存在10分钟内未复用过的对象进行销毁动作
if (cur - obj.lastUsed > 600 * 1000) {
obj.del();
delete this._IdlePool[key];
}
}
}
...
//more code
...
}
最终效果
游戏运行期间,鱼的总数量维持在80-114之间,内存稳定在27-31mb之间,游戏效果总体流畅度得到相应的提升,用户的游戏体验得到改善。目前游戏整体效果得到一定的提升,但仍存在性能优化的空间,后续会继续提升游戏的性能,给用户更好的体验。
欢迎大家留言讨论,祝工作顺利、生活愉快!
我是bigo前端,下期见。