版本: cocosCreator 3.4.0

语言: TypeScript

环境: Mac


NodePool


在项目中频繁的使用instantiatenode.destory对性能有很大的耗费,比如飞机射击中的子弹使用和销毁。

因此官方提供了NodePool,它被作为管理节点对象的缓存池使用。定义如下:

export class NodePool {
  // 缓冲池处理组件,用于节点的回收和复用逻辑,这个属性可以是组件类名或组件的构造函数
  poolHandlerComp?: Constructor<IPoolHandlerComponent> | string;
  // 构造函数,可传递组件或名称,用于处理节点的复用和回收
  constructor(poolHandlerComp?: Constructor<IPoolHandlerComponent> | string);
  // 获取当前缓冲池的可用对象数量
  size(): number;
  // 销毁对象池中缓存的所有节点
  clear(): void;
	/*
	@func: 向缓冲池中存入一个不再需要的节点对象
	@param: 回收的目标节点
	注意:
	1. 该函数会自动将目标节点从父节点上移除,但是不会进行 cleanup 操作
	2. 如果存在poolHandlerComp组件和函数,会自动调用 unuse函数
	*/
  put(obj: Node): void;
  /*
  @func: 获取对象池中的对象,如果对象池没有可用对象,则返回空
  @param: 如果组件和函数存在,会向 poolHandlerComp 中的 'reuse' 函数传递的参数
  */
  get(...args: any[]): Node | null;
}

接口汇总:

使用NodePool的大概思路:

  • 通过NodePool创建对象池
  • 获取节点时,可先检测对象池的数目;如果 =0 则克隆节点并放到对象池中,如果 >0 则从对象池中获取
  • 节点不使用的时候,如果没有对象池,则调用node.destory,否则将节点放到对象池中

在对象池中,有个get(...args: any[])的方法,方法参数的使用主要针对于:对象池创建时添加了可选参数。

以飞机射击中子弹的构建为目的,看下关于对象池的使用示例相关:

// GameManager.ts 游戏管理类
import { BulletItem } from '../bullet/BulletItem';

@ccclass('GameManager')
export class GameManager extends Component {
	@property(Prefab) bullet: Prefab = null;		// 子弹预制体
  private _bulletPool: NodePool = null;				// 子弹对象池
  
  onLoad() {
    // 创建子弹对象池
    this._bulletPool = new NodePool();
  }
  
  // 创建玩家子弹
  private createPlayerBullet() {
    // 获取子弹节点
    const bulletNode = this.getBulletNode();
    const bulletItem = bulletNode.getComponent(BulletItem);
    // 此处将子弹对象池传入子弹对象脚本
   	bulletItem.init(this._bulletPool);
  }
  
  // 获取子弹节点
  private getBulletNode(): Node {
    const size = this._bulletPool.size();
    if (size <= 0) {
      // 克隆子弹节点
      const bulletNode = instantiate(this.bullet);
      // 将子弹节点添加到对象池中
      this._bulletPool.put(bulletNode);
    }
    // 从对象池中获取节点
    return this._bulletPool.get();
  }
  
  onDestroy() {
    // 销毁对象池
    this._bulletPool.clear();
  }
}

// BulletItem.ts 子弹对象组件脚本
export class BulletItem extends Component {
	private _bulletPool: NodePool = null;
  
  public init(bulletPool: NodePool) {
    this._bulletPool = bulletPool;
  }
  
  private destroyBullet() {
    // 检测是否存在对象池,如果存在,则将对象放到对象池中,否则销毁
    if (this._bulletPool) {
      this._bulletPool.put(this.node);
    }
    else {
      this.node.destory();
    }
  }
}

如上例子,简单演示了下NodePool对象池的使用,但需要注意:

  • 最好存储同类型的节点,方便管理
  • 注意检测对象池内的对象数目或通过get获取对象后,进行安全判定,避免null
  • 注意对象池对象的释放

构造函数的可选参数

在上面的定义文件中,针对于对象池的构建,有着可选参数的支持,代码如下:

// 缓冲池处理组件,用于节点的回收和复用逻辑,这个属性可以是组件类名或组件的构造函数
poolHandlerComp?: Constructor<IPoolHandlerComponent> | string;
// 构造函数,可传递组件或名称,用于处理节点的复用和回收事件逻辑
constructor(poolHandlerComp?: Constructor<IPoolHandlerComponent> | string);

可选参数的支持主要有两种形式:

  1. string字符串形式
  2. IPoolHandlerComponent 缓存池处理组件形式

对于这两种形式,其本质就是增加了对对象池中对象的自定义逻辑处理,以组件为参数,看下它的定义:

export interface IPoolHandlerComponent extends Component {
  // 在对象被放入对象池的时候进行调用
  unuse(): void;
  // 从对象池中获取对象的时候被调用
  reuse(args: any): void;
}

这两个方法的调用,看下源码的实现:

// 来源于 node-pool.ts
export class NodePool {
  // 向对象缓存池存入不需要的对象
  public put (obj: Node) {
    if (obj && this._pool.indexOf(obj) === -1) {
      // 从父节点移除,但并不cleanup
      obj.removeFromParent();
      
      // 获取组件poolHandlerComp,并检测是否存在 unuse方法,如果存在则调用
      const handler = this.poolHandlerComp?obj.getComponent(this.poolHandlerComp):null;
      if (handler && handler.unuse) {
        handler.unuse();
      }
      this._pool.push(obj);
    }
  }

  // 获取对象池中的对象
  public get (...args: any[]): Node | null {
    // 检测对象池中是否有对象
    const last = this._pool.length - 1;
    if (last < 0) {
      return null;
    } else {
      // 将对象从缓存池中取出
      const obj = this._pool[last];
      this._pool.length = last;

      // 获取组件poolHandlerComp,并检测是否存在reuse方法,如果存在则调用
      const handler=this.poolHandlerComp?obj.getComponent(this.poolHandlerComp):null;
      if (handler && handler.reuse) {
        handler.reuse(arguments);
      }
      return obj;
    }
  }
}

上面的代码有助于对两个方法的调用时机增加一些了解。

下面我们依然以飞机的子弹构建为例,代码增加一些拓展,用于支持对象池的自定义逻辑处理。

// GameManager.ts 游戏管理类
import { BulletItem } from '../bullet/BulletItem';

@ccclass('GameManager')
export class GameManager extends Component {  
  onLoad() {
    // 创建子弹对象池, 参数设定为子弹类的名字
    this._bulletPool = new NodePool("BulletItem");
  }
  
  private getBulletNodePool() {
    const size = this._bulletPool.size();
    if (size <= 0) {
      const bulletNode = instantiate(this.bullet_1);
      this._bulletPool.put(bulletNode);
    }

		// 获取子弹节点时,可以设置自定义的参数相关
    return this._bulletPool.get();
  }
}

// BulletItem.ts 子弹对象组件脚本,增加
export class BulletItem extends Component implements IPoolHandlerComponent {
  unuse(): void {
    console.log("------ 调用了组件的 unuse 方法");
  }

  reuse(args: any): void {
    console.log("------ 调用了组件的 reuse 方法");
  }
}

增加对对象的自定义逻辑处理,其要点就是:

  • 构建对象池时,需要添加可选参数,参数的名字或组件一定要是对象的脚本组件相关
  • 对象的组件脚本类,需要增加implements IPoolHandlerComponent 的实现,也就是unusereuse方法
  • 根据情况,自定义设定NodePool.get的参数相关

到这里,关于NodePool的基本使用介绍完毕。


NodePool管理器


在上面的例子中,关于对象池的使用存在着几个问题:

  1. 从对象池获取对象和将对象放入对象池的调用在不同的脚本文件中,可能会出现维护比较困难的问题

  2. 对象池的构建不仅针对于子弹,而且可能还有敌机,道具等,可能会出现多个对象池且代码重复的问题。

因此,我们可构建一个对象池的管理类,来统一管理多个不同的对象池,类似于cocos2d-x中的PoolManager

大致的属性和接口是:

该类使用的是单例模式,详细的代码如下:

// 对象池管理器
import { _decorator, Component, instantiate, NodePool, Prefab} from 'cc';
const { ccclass } = _decorator;

export class NodePoolManager {
    private static _instance: NodePoolManager = null;
    private _nodePoolMap: Map<string, NodePool> = null;

    static get instance() {
        if (this._instance) {
            return this._instance;
        }
        this._instance = new NodePoolManager();
        return this._instance;
    }

    constructor() {
        this._nodePoolMap = new Map<string, NodePool>();
    }

    /*
    @func 通过对象池名字从容器中获取对象池
    @param name 对象池名字
    @return 对象池
    */
    private getNodePoolByName(name: string): NodePool {
        if (!this._nodePoolMap.has(name)) {
            let nodePool = new NodePool(name);
            this._nodePoolMap.set(name, nodePool);
        }
        let nodePool = this._nodePoolMap.get(name);
        return nodePool;
    }

    /*
    @func 通过对象池名字从对象池中获取节点
    @param name 对象池名字
    @param prefab 可选参数,对象预制体
    @return 对象池中节点 
    */
    public getNodeFromPool(name: string, prefab?: Prefab): Node | null {
        let nodePool = this.getNodePoolByName(name);
        const poolSize = nodePool.size();
        if (poolSize <= 0) {
            let node = instantiate(prefab);
            nodePool.put(node);
        }
        return nodePool.get();
    }

    /*
    @func 将节点放入对象池中
    @param name 对象池名字
    @param node 节点
    */
    public putNodeToPool(name: string, node: Node) {
        let nodePool = this.getNodePoolByName(name);
        nodePool.put(node);
    }

    // 通过名字将对象池从容器中移除
    public clearNodePoolByName(name: string) {
        // 销毁对象池中对象
        let nodePool = this.getNodePoolByName(name);
        nodePool.clear();
        // 删除容器元素
        this._nodePoolMap.delete(name);
    }

    // 移除所有对象池
    public clearAll() {
        this._nodePoolMap.forEach((value: NodePool, key: string) => {
            value.clear();
        });
        this._nodePoolMap.clear();
    }

    static destoryInstance() {
        this._instance = null;
    }
}

测试示例:

// GameManager.ts 
const BULLET_POOL_NAME = "BulletItem"       // 子弹内存池

// 创建玩家子弹
private createPlayerBullet() {
  // 获取子弹节点,参数:节点名,子弹预制体
  const poolManager = NodePoolManager.instance;
  const bulletNode = poolManager.getNodeFromPool(BULLET_POOL_NAME, this.bulletPrefab);
  bulletNode.parent = this.bulletRoot;
}

// BulletItem.ts
private destroyBullet() {
  // 检测是否存在对象池,如果存在,则将对象放到对象池中,否则销毁
  if (this._bulletPool) {
    //this._bulletPool.put(this.node);
    const poolManager = NodePoolManager.instance;
    poolManager.putNodeToPool(BULLET_POOL_NAME, this.node);
  }
  else {
    this.node.destory();
  }
}

管理类中有个接口叫做getNodeFromPool(name: string, prefab?: Prefab),第二个参数也可以为prefabName,然后通过resource.load进行动态加载,类似实现:

public getNodeFromPool(name: string, prefabName?: string): Node | null {
  let nodePool = this.getNodePoolByName(name);
  const poolSize = nodePool.size();
  if (poolSize <= 0) {
    const url = "prefab/" + prefabName;
    resources.load(url, (err, prefab) => {
      if (err) {
        return console.err("getNodeFromPool resourceload failed:" + err.message);
      }
      let node = instantiate(prefab);
      nodePool.put(node);
    });
  }
  return nodePool.get();
}

resouces.load属于异步操作,可能会出现代码未加载完成就获取的问题,因此可使用异步编程

public getNodeFromPool(name: string, prefabName?: string): Promise<Node | null> {
  return new Promise<Node | null>((resolve, reject) => {
    let nodePool = this.getNodePoolByName(name);
    const poolSize = nodePool.size();
    if (poolSize <= 0) {
      const url = "prefab/" + prefabName;
      resources.load(url, (err, prefab) => {
        if (err) {
          console.error("getNodeFromPool resourceload failed:" + err.message);
          reject(err);
        } else {
          let node = instantiate(prefab);
          nodePool.put(node);
          resolve(nodePool.get());
        }
      });
    } else {
      resolve(nodePool.get());
    }
  });
}

关于一些TypeScript的语法相关,可参考博客:

TypeScript 之 Map

TypeScript 之 异步编程

因工作的某些缘故,可能对NodePool的理解及编写示例有所不当,请不吝赐教,感激不尽!

最后祝大家学习生活愉快!

10-18 20:00