一、对象池的概念
(摘自CocosCreator手册介绍)
对象池就是一组可回收的节点对象,我们通过创建 cc.NodePool 的实例来初始化一种节点的对象池。通常当我们有多个 prefab 需要实例化时,应该为每个 prefab 创建一个 cc.NodePool 实例。当我们需要创建节点时,向对象池申请一个节点,如果对象池里有空闲的可用节点,就会把节点返回给用户,用户通过 node.addChild 将这个新节点加入到场景节点树中。
当我们需要销毁节点时,调用对象池实例的 put(node) 方法,传入需要销毁的节点实例,对象池会自动完成把节点从场景节点树中移除的操作,然后返回给对象池。这样就实现了少数节点的循环利用。假如玩家在一关中要杀死 100 个敌人,但同时出现的敌人不超过 5 个,那我们就只需要生成 5 个节点大小的对象池,然后循环使用就可以了。
关于 cc.NodePool 的详细 API 说明,请参考 cc.NodePool API 文档。
二、对象池的创建
每次创建都需要写一个新的对象池和脚本对象,不方便使用,于是自己封装了一个对象池基类,如果有新的对象池直接继承这个基类使用即可。
export default class Singleton {
private static _instance: any = null;
static getInstance<T>(): T {
if (this._instance == null) {
this._instance = new this();
}
return this._instance;
}
}
单例基类有了,我们在封装一个对象池基类,该池基类继承单例:
/**
* 对象池基类
* 只要继承本类,并重写前4个变量即可
*/
import Singleton from "../../base/Singleton";
export class BasePool extends Singleton {
//预制体url 子类实现赋值
prefabUrl: string;
//(可选)对象脚本组件 子类实现赋值
script: {
prototype: cc.Component } | string;
//(可选)初始化池量 子类实现赋值
poolSize: number = 10;
//(可选)组件名字
nodeName: string;
//(可选)VIP标记 回收判定是否本池实例
MemberFlag: string;
private _pool: cc.NodePool;
private _prefab: cc.Prefab;
private _nodeName: string;
/**
* 初始化对象池入口
* fixme 注意:由于使用了动态资源加载,会有出现资源加载延迟现象,使用是不能直接链式编程获取对象,等实例化完成在获取对象池
* @param size 初始池量
*/
initPool(size?: number) {
this.clean();
cc.resources.load(this.prefabUrl, cc.Prefab, (err, prefab: cc.Prefab) => {
if (err) {
cc.error(err.message);
return;
}
prefab.addRef();
this._prefab = prefab;
this._nodeName = prefab.name;
this.initPoolSize(size);
});
}
/**
* 获取成员对象
*/
getNode() {
let obj: cc.Node;
if (this._pool.size() > 0) {
obj = this._pool.get();
cc.log(`BasePool Tip: 获取${
this.MemberFlag}Pool 成员成功,当前池容量剩余:${
this._pool.size()}`);
} else {
obj = this.buildNode();
cc.log(`BasePool Tip: 获取${
this.MemberFlag}Pool 成员成功,该池容量不足,产生新成员`);
}
if (!obj) {
throw new Error(`BasePool Tip: 获取${
this.MemberFlag}Pool 成员失败`);
}
return obj;
}
/**
* 回收对象接口
* @param obj 回收对象
*/
putNode(obj: cc.Node) {
if (this.isPoolMember(obj)) {
//回收对象
this._pool.put(obj);
cc.log(`BasePool Tip: ${
this.MemberFlag}Pool回收对象成功,当前池容量提升至:${
this._pool.size()}`);
} else {
if (obj && obj.name) {
cc.warn(`BasePool Tip: ${
this.MemberFlag}Pool回收对象错误,${
obj.name}并该对象池成员,回收失败`);
} else {
throw new Error(`BasePool Tip: 获取${
this.MemberFlag}Pool回收对象错误,对象不存在`);
}
}
}
/**
* 清空对象池
*/
clean() {
if (this._pool) {
this._pool.clear();
cc.log(`BasePool Tip: 清空${
this.MemberFlag}Pool成功,当前池容量:${
this._pool.size()}`);
}
//释放预制体组件资源
if (this._prefab) {
this._prefab.decRef();
this._prefab = null;
cc.log(`BasePool Tip: 释放${
this.MemberFlag}Pool 资源数据`);
}
}
/**
* 初始化对象池
* @param size 数量
*/
private initPoolSize(size?: number) {
if (size > 0) {
this.poolSize = size;
}
//对象池
if (this._prefab) {
if (!this.MemberFlag) {
this.MemberFlag = this.constructor.name;
}
if (this.script) {
this._pool = new cc.NodePool(this.script);
} else {
this._pool = new cc.NodePool();
}
for (let i = 0; i < this.poolSize; i++) {
let obj = this.buildNode();
this._pool.put(obj);
}
cc.log(`BasePool Tip: 初始化对象池(${
this.MemberFlag}Pool)完成,初始容量为:${
this._pool.size()}`);
} else {
cc.error(`BasePool Tip: ${
this.MemberFlag}pool 初始化失败`)
}
}
/**
* 实例化对象并标记为VIP
*/
private buildNode(): cc.Node {
let node: cc.Node = cc.instantiate(this._prefab);
node["memberFlag"] = this.MemberFlag;
if (this.nodeName) {
node.name = this.nodeName;
}
return node;
}
/**
* 判定是否VIP
* @param node 回收对象
*/
private isPoolMember(node: cc.Node): boolean {
return node instanceof cc.Node && this.MemberFlag == node["memberFlag"];
}
/**
* 获取组件名字
*/
getNodeName(): string {
return this.nodeName ? this.nodeName : this._nodeName;
}
}
核心代码很简单,我也在没个函数和参数上都加满了注释,这里我就过多介绍,接下来我们看实战代码:
import ResourcesConfig from "../../common/ResourcesConfig";
import {
BasePool} from "./BasePool";
import PKInjury from "../model/PKInjury";
/**
* 战斗扣血对象池
*/
export default class PKInjuryPool extends BasePool {
private constructor() {
super();
//必须赋值(预制体路径)
this.prefabUrl = ResourcesConfig.Prefab_PKInjury_Common;
//可选
this.MemberFlag = "PKInjury";
this.script = PKInjury;
this.nodeName = "PKInjury";
// this.poolSize = 15;//默认为10
}
//重写抽象类的方法
static get Instance(): PKInjuryPool {
return super.getInstance<PKInjuryPool>();
}
}
实现复用对象看起来也非常简单,只需要重写构造函数的属性,和单例的抽象方法即可,实用性还是非常强的,非常好用。
使用时有一点需要注意:
PKInjuryPool.Instance.initPool();//初始化扣血对象池
------对象池初始化时,必须要提前,这里我在代码有说明-----
let injuryNode: cc.Node = PKInjuryPool.Instance.getNode();//复用
PKInjuryPool.Instance.putNode(injuryNode);//回收
PKInjuryPool.Instance.clean();//清空扣血对象池
关于 Unity 对象池的使用在下一章。