版本: cocosCreator 3.4.0
语言: TypeScript
环境: Mac
NodePool
在项目中频繁的使用instantiate
和node.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);
可选参数的支持主要有两种形式:
string
字符串形式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
的实现,也就是unuse
和reuse
方法 - 根据情况,自定义设定
NodePool.get
的参数相关
到这里,关于NodePool的基本使用介绍完毕。
NodePool管理器
在上面的例子中,关于对象池的使用存在着几个问题:
-
从对象池获取对象和将对象放入对象池的调用在不同的脚本文件中,可能会出现维护比较困难的问题
-
对象池的构建不仅针对于子弹,而且可能还有敌机,道具等,可能会出现多个对象池且代码重复的问题。
因此,我们可构建一个对象池的管理类,来统一管理多个不同的对象池,类似于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的语法相关,可参考博客:
因工作的某些缘故,可能对NodePool
的理解及编写示例有所不当,请不吝赐教,感激不尽!
最后祝大家学习生活愉快!