• cocosCreator 之 3.x使用NodePool对象池和封装


    版本: 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    接口汇总:

    接口说明
    new()创建对象池
    put()将节点放到对象池中
    get()从对象池中获取节点
    size()获取对象池中对象的数目
    clear()销毁对象池中的所有对象

    使用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();
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

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

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

    构造函数的可选参数

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

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

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

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

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

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

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

    // 来源于 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;
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

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

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

    // 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 方法");
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

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

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

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


    NodePool管理器


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

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

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

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

    大致的属性和接口是:

    属性或方法说明
    _nodePoolMap保存所有对象池的容器,结构是Map<对象池名, NodePool池>
    getNodePoolByName():NodePool通过名字从容器中获取对象池,如果没有则创建,如果存在则获取
    getNodeFromPool():Node通过名字从对象池中获取节点,如果没有则克隆,如果存在则获取
    putNodeToPool()将节点放入对象池中
    clearNodePoolByName()通过名字将对象池从容器中移除
    clearAll()移除容器中的所有对象池

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

    // 对象池管理器
    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    测试示例:

    // 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();
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    管理类中有个接口叫做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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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());
        }
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

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

    TypeScript 之 Map

    TypeScript 之 异步编程

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

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

  • 相关阅读:
    Git——关于Git的一些补充(1)
    为什么HashMap不使用B、B+树
    [附源码]计算机毕业设计springboot中小学课后延时服务管理系统
    52.seata分布式事务
    【CKA考试笔记】十四、helm
    【题目讲解】ascii码值(空格、回车、0、1、2、3、a、b)
    TMD,JVM类加载原来是这样的!!!!
    javaScript爬虫程序抓取评论
    py.test --pep8 vsearch.py报错解决办法
    公开课|“技术+法律”隐私计算如何助力数据合规
  • 原文地址:https://blog.csdn.net/qq_24726043/article/details/133910483