cocosCreator 3.x uses NodePool object pool and encapsulation

Version: cocosCreator 3.4.0

Language: TypeScript

Environment: Mac

NodePool

Frequent use of instantiate and node.destroy in the project has a great cost on performance, such as the use and destruction of bullets in aircraft shooting.

Therefore, the official provides NodePool, which is used as a cache pool for managing node objects. The definition is as follows:

export class NodePool {<!-- -->
  // Buffer pool processing component, used for node recycling and reuse logic. This attribute can be the component class name or the component's constructor
  poolHandlerComp?: Constructor<IPoolHandlerComponent> | string;
  // Constructor, which can pass components or names, is used to handle the reuse and recycling of nodes.
  constructor(poolHandlerComp?: Constructor<IPoolHandlerComponent> | string);
  // Get the number of available objects in the current buffer pool
  size(): number;
  // Destroy all nodes cached in the object pool
  clear(): void;
/*
@func: Store a node object that is no longer needed in the buffer pool
@param: target node for recycling
\tNotice:
1. This function will automatically remove the target node from the parent node, but will not perform a cleanup operation.
2. If the poolHandlerComp component and function exist, the unuse function will be automatically called.
*/
  put(obj: Node): void;
  /*
  @func: Get the object in the object pool, if there is no available object in the object pool, return empty
  @param: If the component and function exist, the parameters passed to the 'reuse' function in poolHandlerComp
  */
  get(...args: any[]): Node | null;
}

Interface summary:

Interface Description
new() Create an object pool
put() Put the node into the object pool
get() Get nodes from the object pool
size() Get the number of objects in the object pool
clear() Destroy all objects in the object pool

The general idea of using NodePool:

  • Create an object pool through NodePool
  • When obtaining a node, you can first detect the number of object pools; if =0, clone the node and put it in the object pool; if 0, obtain it from the object pool
  • When the node is not in use, if there is no object pool, call node.destory, otherwise the node will be placed in the object pool.

In the object pool, there is a get(...args: any[]) method. The use of method parameters is mainly aimed at: Optional parameters are added when the object pool is created.

For the purpose of constructing a neutron bomb for aircraft shooting, let’s take a look at the usage examples of object pools:

// GameManager.ts game management class
import {<!-- --> BulletItem } from '../bullet/BulletItem';

@ccclass('GameManager')
export class GameManager extends Component {<!-- -->
@property(Prefab) bullet: Prefab = null; // Bullet prefab
  private _bulletPool: NodePool = null; // Bullet object pool
  
  onLoad() {<!-- -->
    //Create a bullet object pool
    this._bulletPool = new NodePool();
  }
  
  //Create player bullets
  private createPlayerBullet() {<!-- -->
    // Get bullet node
    const bulletNode = this.getBulletNode();
    const bulletItem = bulletNode.getComponent(BulletItem);
    // Here the bullet object pool is passed into the bullet object script
   bulletItem.init(this._bulletPool);
  }
  
  // Get bullet node
  private getBulletNode(): Node {<!-- -->
    const size = this._bulletPool.size();
    if (size <= 0) {<!-- -->
      //Clone bullet node
      const bulletNode = instantiate(this.bullet);
      //Add the bullet node to the object pool
      this._bulletPool.put(bulletNode);
    }
    // Get nodes from the object pool
    return this._bulletPool.get();
  }
  
  onDestroy() {<!-- -->
    // Destroy the object pool
    this._bulletPool.clear();
  }
}

// BulletItem.ts bullet object component script
export class BulletItem extends Component {<!-- -->
private _bulletPool: NodePool = null;
  
  public init(bulletPool: NodePool) {<!-- -->
    this._bulletPool = bulletPool;
  }
  
  private destroyBullet() {<!-- -->
    //Detect whether there is an object pool. If it exists, put the object into the object pool, otherwise destroy it.
    if (this._bulletPool) {<!-- -->
      this._bulletPool.put(this.node);
    }
    else {<!-- -->
      this.node.destroy();
    }
  }
}

The above example briefly demonstrates the use of NodePool object pool, but please note:

  • It is best to store nodes of the same type for easy management
  • Pay attention to detecting the number of objects in the object pool or obtaining objects through get, and make safety judgments to avoid null
  • Pay attention to the release of object pool objects

Optional parameters of constructor

In the above definition file, there is support for optional parameters for the construction of the object pool. The code is as follows:

//Buffer pool processing component, used for node recycling and reuse logic. This attribute can be the component class name or the component's constructor.
poolHandlerComp?: Constructor<IPoolHandlerComponent> | string;
// Constructor, which can pass components or names, is used to handle the reuse and recycling event logic of nodes.
constructor(poolHandlerComp?: Constructor<IPoolHandlerComponent> | string);

Optional parameters are supported in two main forms:

  1. stringString form
  2. IPoolHandlerComponent Cache pool processing component form

For these two forms, the essence is to add custom logical processing of objects in the object pool, with components as parameters. Take a look at its definition:

export interface IPoolHandlerComponent extends Component {<!-- -->
  // Called when the object is put into the object pool
  unuse(): void;
  // Called when getting an object from the object pool
  reuse(args: any): void;
}

To call these two methods, look at the source code implementation:

// From node-pool.ts
export class NodePool {<!-- -->
  // Store unnecessary objects in the object cache pool
  public put (obj: Node) {<!-- -->
    if (obj & amp; & amp; this._pool.indexOf(obj) === -1) {<!-- -->
      //Remove from the parent node, but do not cleanup
      obj.removeFromParent();
      
      // Get the component poolHandlerComp and detect whether the unuse method exists. If it exists, call it
      const handler = this.poolHandlerComp?obj.getComponent(this.poolHandlerComp):null;
      if (handler & amp; & amp; handler.unuse) {<!-- -->
        handler.unuse();
      }
      this._pool.push(obj);
    }
  }

  // Get the object in the object pool
  public get (...args: any[]): Node | null {<!-- -->
    //Detect whether there is an object in the object pool
    const last = this._pool.length - 1;
    if (last < 0) {<!-- -->
      return null;
    } else {<!-- -->
      //Retrieve the object from the cache pool
      const obj = this._pool[last];
      this._pool.length = last;

      // Get the component poolHandlerComp, and detect whether there is a reuse method, and call it if it exists
      const handler=this.poolHandlerComp?obj.getComponent(this.poolHandlerComp):null;
      if (handler & amp; & amp; handler.reuse) {<!-- -->
        handler.reuse(arguments);
      }
      return obj;
    }
  }
}

The above code helps to add some understanding of when the two methods are called.

Next, we still take the bullet construction of the aircraft as an example, and add some extensions to the code to support custom logic processing of the object pool.

// GameManager.ts game management class
import {<!-- --> BulletItem } from '../bullet/BulletItem';

@ccclass('GameManager')
export class GameManager extends Component {<!-- -->
  onLoad() {<!-- -->
    // Create a bullet object pool, and set the parameter to the name of the bullet class
    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);
    }

// When obtaining the bullet node, you can set custom parameters
    return this._bulletPool.get();
  }
}

// BulletItem.ts bullet object component script, added
export class BulletItem extends Component implements IPoolHandlerComponent {<!-- -->
  unuse(): void {<!-- -->
    console.log("------The unuse method of the component was called");
  }

  reuse(args: any): void {<!-- -->
    console.log("------The reuse method of the component was called");
  }
}

The main points of adding custom logic processing to objects are:

  • When building an object pool, you need to add optional parameters. The name or component of the parameter must be related to the script component of the object.
  • The component script class of the object needs to add the implementation of implements IPoolHandlerComponent, that is, the unuse and reuse methods
  • Depending on the situation, customize the parameters of NodePool.get

At this point, the basic introduction to the use of NodePool is completed.

NodePool Manager

In the above example, there are several issues regarding the use of object pools:

  1. The calls to obtain objects from the object pool and put the objects into the object pool are in different script files, which may cause difficult maintenance problems.

  2. The construction of the object pool is not only for bullets, but also for enemy aircraft, props, etc. There may be problems with multiple object pools and code duplication.

Therefore, we can build an object pool management class to uniformly manage multiple different object pools, similar to PoolManager in cocos2d-x.

The general properties and interfaces are:

Attribute or method Description
_nodePoolMap Container that saves all object pools, the structure is Map
getNodePoolByName():NodePool Get the object pool from the container by name, create it if it does not exist, and get it if it exists
< strong>getNodeFromPool():Node Get the node from the object pool by name, clone it if it does not exist, and get it if it exists
putNodeToPool() Put the node into the object pool
clearNodePoolByName() Remove the object pool from the container by name
clearAll() Remove from the container All object pools

This class uses the single case mode. The detailed code is as follows:

//Object pool manager
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 Gets the object pool from the container through the object pool name
    @param name object pool name
    @return object pool
    */
    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 Gets the node from the object pool through the object pool name
    @param name object pool name
    @param prefab optional parameter, object prefab
    @return node in object pool
    */
    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 puts the node into the object pool
    @param name object pool name
    @param node node
    */
    public putNodeToPool(name: string, node: Node) {<!-- -->
        let nodePool = this.getNodePoolByName(name);
        nodePool.put(node);
    }

    // Remove the object pool from the container by name
    public clearNodePoolByName(name: string) {<!-- -->
        // Destroy objects in the object pool
        let nodePool = this.getNodePoolByName(name);
        nodePool.clear();
        //Delete container element
        this._nodePoolMap.delete(name);
    }

    // Remove all object pools
    public clearAll() {<!-- -->
        this._nodePoolMap.forEach((value: NodePool, key: string) => {<!-- -->
            value.clear();
        });
        this._nodePoolMap.clear();
    }

    static destructionInstance() {<!-- -->
        this._instance = null;
    }
}

Test example:

// GameManager.ts
const BULLET_POOL_NAME = "BulletItem" // Bullet memory pool

//Create player bullets
private createPlayerBullet() {<!-- -->
  // Get the bullet node, parameters: node name, bullet prefab
  const poolManager = NodePoolManager.instance;
  const bulletNode = poolManager.getNodeFromPool(BULLET_POOL_NAME, this.bulletPrefab);
  bulletNode.parent = this.bulletRoot;
}

// BulletItem.ts
private destroyBullet() {<!-- -->
  //Detect whether there is an object pool. If it exists, put the object into the object pool, otherwise destroy it.
  if (this._bulletPool) {<!-- -->
    //this._bulletPool.put(this.node);
    const poolManager = NodePoolManager.instance;
    poolManager.putNodeToPool(BULLET_POOL_NAME, this.node);
  }
  else {<!-- -->
    this.node.destroy();
  }
}

There is an interface in the management class called getNodeFromPool(name: string, prefab?: Prefab). The second parameter can also be prefabName, and then passed resource.load Dynamic loading, similar implementation:

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();
}

Because resouces.load is an asynchronous operation, there may be a problem of getting the code before it is loaded. Therefore, asynchronous programming can be used:

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());
    }
  });
}

For some syntax related to TypeScript, please refer to the blog:

Map of TypeScript

TypeScript asynchronous programming

Due to some reasons at work, I may have incorrect understanding of NodePool and writing examples. Please feel free to give me some advice. Thank you very much!

Finally, I wish you all a happy study and life!

syntaxbug.com © 2021 All Rights Reserved.