对象池使用

一、概述

在项目开发过程中,有许多对象会不停的创建与移除,比如角色攻击子弹、特效的创建与移除,NPC(非玩家角色)的被消灭与刷新等,在创建过程中非常消耗性能,特别是数量多的情况下,此时会造成卡顿的现象。

因此使用对象池就可以避免大量对象的创建。如果我们每次对象使用完了都放到池子里,比如怪物,子弹等等,怪物被杀死了,不需要用了,就可以放到池子里,下次使用的时候可以直接从池子里拿,对象池没有才需要创建。

对象池的优点是减少了实例化对象时的开销,且能让对象反复使用,减少了新内存分配与垃圾回收器运行的机会。另外对象移除时并不是立即从内存中抹去,只有认为内存不足时,才会使用垃圾回收机制清空,清空时很耗内存,很可能就会造成卡顿现象。用了对象池后将减少程序的垃圾对象,有效的提高程序的运行速度和稳定性。

接下来,我们来看看在LayaAir中是如何使用对象池(Pool)的。

二、对象池 Pool

Pool 是对象池类,用于对象的存贮、重复使用。合理使用对象池,可以有效减少对象创建的开销,避免频繁的垃圾回收,从而优化游戏流畅度。

2.1 获得一个对象池

    /**
     * 根据对象类型标识字符,获取对象池。
     * @param sign 对象类型标识字符。
     * @return 对象池。
     */
    static getPoolBySign(sign: string): any[] {
        return Pool._poolDic[sign] || (Pool._poolDic[sign] = []);
    }

Laya.Pool 是通过对象类型标识符,也就是一个字符串名字来标识和管理对象池的。如果对象池系统中没有这个标识,那么会创建一个标识的对象池。因此我们也可以通过标识来定义多个对象池,分别处理不同类型的对象。比如攻击的子弹是一个对象池,NPC玩家是一个对象池。

所以,当我们想使用一个对象池的话,需要这样在代码中调用:

let bulletPool = Laya.Pool.getPoolBySign("Bullet");

有了对象池,我们可以查看对象池当前的情况,比如查看对象池内对象的数量,继续添加对象等等。比如代码:

let bulletPool = Laya.Pool.getPoolBySign("Bullet");
// 查看当前对象池内对象数量
console.log( bulletPool.length );

if( bulletPool.length == 0 )
{
    // 把子弹放入对象池
    pool.push( new Bullet() );
}

2.2 清理一个对象池

    /**
     * 清除对象池的对象。
     * @param sign 对象类型标识字符。
     */
    static clearBySign(sign: string): void {
        if (Pool._poolDic[sign]) Pool._poolDic[sign].length = 0;
    }

比如在游戏中,当一场战斗结束时,当没有需要子弹的对象池的需求了,我们可以通过代码来清理对象池:

Laya.Pool.clearBySign("Bullet");

2.3 从池中获得对象

2.3.1 通过标识获得

    /**
     * 根据传入的对象类型标识字符,获取对象池中已存储的此类型的一个对象,如果对象池中无此类型的对象,则返回 null 。
     * @param sign 对象类型标识字符。
     * @return 对象池中此类型的一个对象,如果对象池中无此类型的对象,则返回 null 。
     */
    static getItem(sign: string): any {
        var pool: any[] = Pool.getPoolBySign(sign);
        var rst: any = pool.length ? pool.pop() : null;
        if (rst) {
            rst[Pool.POOLSIGN] = false;
        }
        return rst;
    }

这是最基本的操作,从对象池中拿到一个对象的示例,如果对象池里已经没有可以拿的对象时,返回 null,使用代码如下:

let bullet = Pool.getItem("Bullet");

此时如果拿到的对象是 null,那么我们应该考虑下,有两种情况:

1,对于特别频繁需要创建的某个对象,或者创建这个对象的过程比较消耗性能,我们可以在进入这个场景的加载过程中,预先创建好一组对象,并把这组对象放入对象池中

// 第一次创建子弹的对象池
let bulletPool = Laya.Pool.getPoolBySign("Bullet");
// 创建10个子弹对象,并放入对象池中
for( var i = 0 ; i < 10 ; i++ )
{
    // 创建一个子弹
    let bullet = new Bullet();
    bulletPool.push( bullet );
}
// 当需要的时候,可以从对象池中拿子弹对象
let bullet = Pool.getItem("Bullet");

2,对于创建对象性能要求不高,我们可以通过下面的方法来创建对象,并随时把对象放入对象池中

2.3.2 通过标识获得,没有则创建

    /**
     * <p>根据传入的对象类型标识字符,获取对象池中此类型标识的一个对象实例。</p>
     * <p>当对象池中无此类型标识的对象时,则使用传入的创建此类型对象的函数,新建一个对象返回。</p>
     * @param sign 对象类型标识字符。
     * @param createFun 用于创建该类型对象的方法。
     * @param caller this对象
     * @return 此类型标识的一个对象。
     */
    static getItemByCreateFun(sign: string, createFun: Function, caller: any = null): any {
        var pool: any[] = Pool.getPoolBySign(sign);
        var rst: any = pool.length ? pool.pop() : createFun.call(caller);
        rst[Pool.POOLSIGN] = false;
        return rst;
    }

基于上述的情况,如果对象池中没有对象了,可以随时利用这个方法创建对象:

let bullet = Laya.Pool.getItemByCreateFun("Bullet", function()
{
    // 创建一个子弹
    let bullet = new Bullet();
    // 拿到子弹的对象池
    var pool = Laya.Pool.getPoolBySign("Bullet");
    // 把子弹放入对象池,也可以不放入对象池,根据开发者需求
    pool.push( bullet );
    // 返回子弹对象
    return bullet;
});

2.4 回收对象到池中

2.4.1 通过对象进行回收

    /**
     * 将对象放到对应类型标识的对象池中。
     * @param sign 对象类型标识字符。
     * @param item 对象。
     */
    static recover(sign: string, item: any): void {
        if (item[Pool.POOLSIGN]) return;
        item[Pool.POOLSIGN] = true;
        Pool.getPoolBySign(sign).push(item);
    }

比如在游戏的一场战斗过程中,从对象池中拿出的子弹已经结束了它的生命周期时,我们可以通过代码来回收这个子弹对象到对象池中:

Laya.Pool.recover("Bullet", bullet);

2.5 通过类名,获得和回收对象

往往我们在做复杂的系统架构过程中,通过使用类名来获得和回收对象是一种很好的处理方式。Laya的Pool对象已经为我们考虑了这种情况,我们先来看看

2.5.1 通过类名获得对象

    /**
     * <p>根据传入的对象类型标识字符,获取对象池中此类型标识的一个对象实例。</p>
     * <p>当对象池中无此类型标识的对象时,则根据传入的类型,创建一个新的对象返回。</p>
     * @param sign 对象类型标识字符。
     * @param cls 用于创建该类型对象的类。
     * @return 此类型标识的一个对象。
     */
    static getItemByClass<T>(sign: string, cls: new () => T): T {
        if (!Pool._poolDic[sign]) return new cls();

        var pool = Pool.getPoolBySign(sign);
        if (pool.length) {
            var rst = pool.pop();
            rst[Pool.POOLSIGN] = false;
        } else {
            rst = new cls();
        }
        return rst;
    }

2.5.2 根据类名进行回收

    /**
     * 根据类名进行回收,如果类有类名才进行回收,没有则不回收
     * @param    instance 类的具体实例
     */
    static recoverByClass(instance: any): void {
        if (instance) {
            var className: string = instance["__className"] || instance.constructor._$gid;
            if (className) Pool.recover(className, instance);
        }
    }

有了这两种对应的方式,我们可以不用在代码中去关心每个对象的创建和回收,只关心对象的内部逻辑就好了。比如在战斗过程中有很多的技能特效,我们可以对每个特效用统一的方式进行管理对象池:

export class EffectA {    

    constructor() {
        super();
    }

    static create(): EffectA {
        Pool.getItemByClass(EffectA);
    }

    recover(): void {
        Pool.recoverByClass(this);
    }

}

export class EffectB {    

    constructor() {
        super();
    }

    static create(): EffectB {
        Pool.getItemByClass(EffectB);
    }

    recover(): void {
        Pool.recoverByClass(this);
    }

}

总结,Laya提供的Pool是一个比较基本的对象池,开发者可以根据自己的需求来扩展对象池的使用,从而更方便的实现更复杂的对象管理。

Copyright ©Layabox 2022 all right reserved,powered by LayaAir Engine更新时间: 2023-03-03 17:34:16

results matching ""

    No results matching ""