实体与对象池是游戏开发中的常见概念。例如,我们可以为子弹设计实体,为玩家设计实体,甚至为爆炸效果设计实体。而对象池就是方便我们管理多个重复实体,而不必频繁创建和销毁的一种设计。
这里的实体不是指看得见摸得着的意思。任何经过实例化的对象都可以称为实体。
每一个实体,如果要播放动画效果,我们常常给它加上一个状态机。虽然有些实体(例如爆炸效果),只有一种状态,但是利用模块化的状态机可以很方便的制作动画效果。关于状态机的概念我在上节已经讲过了。
实体简单来说就是对象,但有一定的区别。我们的DataManager.Instance是DataManager类的单例对象,但它不能称为实体。实体可以动态创建,也就是说,可以通过prefab在游戏中动态生成。
上节我们讲了动态创建DynamicCreation。基本思路为,先把我们需要的资源名称存在一个枚举中,然后用一张map来维护资源名称和资源路径的映射关系。然后在游戏初始化的时候通过资源路径来把对应的prefab加载到另一张map中,和资源名称建立另一个映射关系。
那么我们如何管理创建好的实体呢?每一个实体都应该具有自己唯一的编码Id,而且也应该建立一个映射关系,让我们可以通过这个编码id找到实体。
比如我们希望给每一个子弹编码,然后建立一个map来维护所有子弹,可以编写如下代码
//DataManager.ts
bulletMap: Map<number, BulletManager> = new Map();//建立映射关系,每个子弹都由它对应的BulletManager管理
//DataManager.ts的applyInput方法中的一个分支
case InputTypeEnum.WeaponShoot: {
const { owner, position, direction } = input;
const bullet: IBullet = {
id: this.state.nextBulletId++,
owner,
position,
direction,
type: this.actorMap.get(owner).bulletType,
};
EventManager.Instance.emit(EventEnum.BulletBorn, owner);
this.state.bullets.push(bullet);
break;
}
在这个示例中,我们首先建立了子弹id和子弹管理类之间的一个映射关系。每个子弹都由一个子弹管理类来进行管理。然后,在DataManager的applyInput的一个分支中(applyInput用来处理输入系统提供的Input,第一章有介绍),我们创建了一个新的bullet数据字段,里面含有五个参数。第一个是id,第二个是owner,代表是谁发射的子弹,第三个是position发射位置,第四个是direction发射方向,最后通过之前建立的玩家id与actorManager之间的映射actorMap,获取到玩家的子弹类型。
这里的子弹id是不断自增的,上一次的数据保存在全局状态字段state中。
子弹数据bullet创建完成后,将它推入state中的bullets数组,然后,我们的渲染系统下一次进行渲染时,会获取bullets数组中的所有元素,挨个进行渲染。
这就是数据驱动的含义。数据系统从输入系统获取输入信息,进行处理后修改底层数据,然后渲染层不断从数据系统获取新数据进行渲染,达成游戏运行的效果。
对象池是传统实体的频繁创建和销毁,导致计算机性能消耗的问题的解决办法。对象池本质上是实体的容器。
主要思路为,一次性创建所有实体放入对象池,或者动态扩充对象池。需要使用实体的时候,从对象池中取出该实体,使用结束后将实体放回对象池。这样每一个实体创建之后都可以进行多次复用,而不必频繁地创建和销毁。
比如游戏场景里最多存在50颗子弹,我就在加载游戏的时候一次性创建50个子弹实体。当我需要调用子弹实体的时候,我取出一个节点node,挂载上BulletManager,然后通知DataManager进行数据赋值,再通知渲染系统进行渲染。
要实现ObjectPool也很简单。之前我们产生子弹实体的方式是通过
const bullet = cc.instantiate(bulletPrefab)
然后设置bullet的父节点,并添加一个BulletManager来管理子弹
bullet.setParent(DataManager.Instance.stage)
bullet.addComponent(BulletManager)
现在我们从对象池中获取实体
const bullet = ObjectPoolManager.Instance.get(type);
bm =
bullet.getComponent(BulletManager) ||
bullet.addComponent(BulletManager);
DataManager.Instance.bulletMap.set(id, bm);
因为我们从对象池中取出的节点可能是之前使用过的节点,所以先调用获取组件方法getComponent,如果没有返回则说明这个节点是第一次使用,就调用addComponent方法。
最后在DataManager的bulletMap中注册该子弹。
DataManager.Instance.bulletMap.delete(this.id);
ObjectPoolManager.Instance.ret(this.node);
触发子弹销毁逻辑的时候,我们在DataManager的bulletMap中取消子弹的注册,然后归还这个节点。
如此,就实现了单机游戏通讯的基础架构。输入系统输入Input给数据系统,从对象池管理器请求获得Node。数据系统注册id和对应管理类的映射关系,管理所有游戏数据。实体管理类获得数据系统的数据进行业务的处理和实体的渲染。
下附对象池的具体实现(单例模式)
import { _decorator, resources, Asset, Node, instantiate } from "cc";
import Singleton from "../Base/Singleton";
import { EntityTypeEnum } from "../Common";
import DataManager from "./DataManager";
export class ObjectPoolManager extends Singleton {
static get Instance() {
return super.GetInstance<ObjectPoolManager>();
}
private objectPool: Node;
private map: Map<EntityTypeEnum, Node[]> = new Map();
get(type: EntityTypeEnum) {
if (!this.objectPool) {
this.objectPool = new Node("ObjectPool");
this.objectPool.setParent(DataManager.Instance.stage);
}
if (!this.map.has(type)) {
this.map.set(type, []);
const container = new Node(type + "Pool");
container.setParent(this.objectPool);
}
const nodes = this.map.get(type);
if (!nodes.length) {
const prefab = DataManager.Instance.prefabMap.get(type);
const node = instantiate(prefab);
node.name = type;
node.setParent(this.objectPool.getChildByName(type + "Pool"));
return node;
} else {
const node = nodes.pop()
node.active=true
return node
}
}
ret(node: Node) {
node.active=false
this.map.get(node.name as EntityTypeEnum).push(node)
}
}
这个对象池管理类是可扩充型。自动根据对象池的实体最大上限来扩充对应对象池的容量。首先建立对象池根节点(‘objectPool’),然后再次使用map来映射不同类型实体(用枚举量表示)和对应对象池的关系。