[cocos creator] - Cocos creator 使用对象池必须知道的事情

事情起因

今天中午遇到了一件怪事,使用节点池的时候新创建出来的节点无法改变 spriteFrame(也就是精灵的图片),折腾了一下午终于找到原因,对于初次学习对象池的用户来说……这是一个必须要注意的深坑。

问题代码如下,以下实现了 动态更新精灵图片 的方法:

setSpriteFrame(node, spriteFrame) {
    node.getComponent(cc.Sprite).spriteFrame = spriteFrame;
},

这个方法本身并没有任何问题,然而问题出在对象池上……

对象池

所谓对象池其实是一种设计模式,对象池模式的作用是减少因为初始化而浪费的资源。

用一个例子来理解对象池模式的使用场景。

假如制作一个弹幕游戏,场景中需要用到几百上千个子弹,游戏的场景如下图:

东方系列弹幕游戏

(东方系列 - 弹幕游戏)

每一个子弹都是一个预制体对象(Prefab),如果按照传统的方法,当 Boss 发射子弹的时候创建子弹节点,当子弹离开屏幕的时候销毁节点,用于控制 Boss 行为方式的脚本 boss.js

cc.Class({
    extends: cc.Component,

    properties: {
        bulletPrefab: cc.Prefab,
    },

    /**
     * Boss 发起弹幕攻击
     */
    attack() {
        // 生成 1000 颗子弹
        let bulletCount = 1000;
        for (let i = 0; i < bulletCount; i++) {
             // 创建节点
            let bullet = cc.instantiate(this.bulletPrefab);
            // ... 这里略过子弹初始化逻辑(包括飞行方向、速度等初始化动作)
            // 将子弹添加到场景
            this.node.parent.addChild(bullet);
        }
    },
});

子弹的脚本在这里就不写了,大致逻辑是离开屏幕就调用:

// 销毁子弹
this.node.destroy();

以上便完成了弹幕游戏的简单逻辑。

由于创建节点是十分消耗性能的一件事(需要磁盘 IO 读取子弹图片及重复初始化预制体),加上子弹的数量又非常多,严重影响了程序的性能,子弹的数量更多时,甚至会产生卡顿现象。

但实际上我们真的需要重复创建节点吗?

并不需要!

那些飞出去屏幕之外的子弹完全可以回收起来重复利用,在游戏开始时我们可以预先创建出 1000 个节点(这个数量是自己预估的,估少了也没有关系),把这些创建好的节点放在一个数组(或者对象)里,当需要创建子弹节点的时候从这个数组里面取,当子弹飞出屏幕之外的时候把子弹重新放回数组里。

这样实现资源回收利用的模式就叫做对象池模式。

对象池模式通过预先创建好节点然后反复利用的方式大大减少了性能开销。

在 cocos creator 中内置了对象池系统,详情请阅读官方文档:cocos creator 官方文档 - 对象池

使用对象池必须注意的事

当我们回收节点的时候,cocos creator 并不会帮助我们把对象初始化,而是节点当时是什么样的状态,回收的时候就是什么状态。

在初次使用对象池的时候很容易犯这个错误,以为回收了节点就会变成初始状态。

至此已经发现了问题所在,由于我创建的节点中播放了 动画,但是在回收节点的时候却忘记停止动画,结果导致后面从对象池拿到的节点一开始就在播放动画,结果后面再怎么更新 spriteFrame 也看不到效果(被动画覆盖了)。

在回收节点的时候务必要注意,一定要重新进行初始化操作,初始化的时机可以在两个地方。

  • 获取对象池节点时
  • 回收节点时

获取节点时

从对象池获取节点时可以进行初始化动作,获取对象池节点的时候默认调用预制体中的 init 方法,这样就能确保获取到的节点是初始状态:

createEnemy: function (parentNode) {
    let enemy = null;
    if (this.enemyPool.size() > 0) { // 通过 size 接口判断对象池中是否有空闲的对象
        enemy = this.enemyPool.get();
    } else { // 如果没有空闲对象,也就是对象池中备用对象不够时,我们就用 cc.instantiate 重新创建
        enemy = cc.instantiate(this.enemyPrefab);
    }
    enemy.parent = parentNode; // 将生成的敌人加入节点树
    enemy.getComponent('Enemy').init(); //接下来就可以调用 enemy 身上的脚本进行初始化
}

init 方法中重置节点的状态,这里罗列了一些需要注意重置为初始状态的点:

init() {
    // 初始化各种值
    this.speed = cc.v2(100, 100);
    // 停止所有动作
    this.node.stopAllAction();
    // 恢复透明度
    this.node.opacity = 255;
    // 停止某个动画
 this.node.getComponent(cc.Animation).stop("idle");

}

回收节点时

根据官方文档的说明,在回收节点时会自动调用 removeFromParent 方法:

onEnemyKilled: function (enemy) {
    // enemy 应该是一个 cc.Node
    this.enemyPool.put(enemy); // 和初始化时的方法一样,将节点放进对象池,这个方法会同时调用节点的 removeFromParent
}

因此我们只要在预制体上实现 removeFromParent 方法重置节点状态即可。

总结

对象池使用时非常方便,还能帮助我们提升程序的性能,但是在回收节点的时候却有些麻烦,务必要注意重置节点的状态,因为 cocos creator 是不会帮我们把节点重置的。

问题解决了~~~豁然开朗 (。◕‿◕。)ノ゜.:

讨论

还没有人评论~