联机游戏的前后端交互逻辑(Cocos)【第三章 单机 Entity and ObjectPool 】

实体与对象池是游戏开发中的常见概念。例如,我们可以为子弹设计实体,为玩家设计实体,甚至为爆炸效果设计实体。而对象池就是方便我们管理多个重复实体,而不必频繁创建和销毁的一种设计。

一、Entity实体

这里的实体不是指看得见摸得着的意思。任何经过实例化的对象都可以称为实体。

每一个实体,如果要播放动画效果,我们常常给它加上一个状态机。虽然有些实体(例如爆炸效果),只有一种状态,但是利用模块化的状态机可以很方便的制作动画效果。关于状态机的概念我在上节已经讲过了。

实体简单来说就是对象,但有一定的区别。我们的DataManager.Instance是DataManager类的单例对象,但它不能称为实体。实体可以动态创建,也就是说,可以通过prefab在游戏中动态生成。

上节我们讲了动态创建DynamicCreation。基本思路为,先把我们需要的资源名称存在一个枚举中,然后用一张map来维护资源名称和资源路径的映射关系。然后在游戏初始化的时候通过资源路径来把对应的prefab加载到另一张map中,和资源名称建立另一个映射关系。

那么我们如何管理创建好的实体呢?每一个实体都应该具有自己唯一的编码Id,而且也应该建立一个映射关系,让我们可以通过这个编码id找到实体。

比如我们希望给每一个子弹编码,然后建立一个map来维护所有子弹,可以编写如下代码

//DataManager.ts  
bulletMap: Map = 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数组中的所有元素,挨个进行渲染。

这就是数据驱动的含义。数据系统从输入系统获取输入信息,进行处理后修改底层数据,然后渲染层不断从数据系统获取新数据进行渲染,达成游戏运行的效果。

二、ObjectPool对象池

对象池是传统实体的频繁创建和销毁,导致计算机性能消耗的问题的解决办法。对象池本质上是实体的容器。

主要思路为,一次性创建所有实体放入对象池,或者动态扩充对象池。需要使用实体的时候,从对象池中取出该实体,使用结束后将实体放回对象池。这样每一个实体创建之后都可以进行多次复用,而不必频繁地创建和销毁。

比如游戏场景里最多存在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();
  }

  private objectPool: Node;
  private map: Map = 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来映射不同类型实体(用枚举量表示)和对应对象池的关系。

你可能感兴趣的:(游戏)