最近打算回顾一下在自己在slg向的沙盘大地图游戏开发中遇到的一些问题和解决方案,本篇(大概会分上中下3节)主要讲述大地图同步的相关问题
游戏玩法概述
- 支持数千人在同一张大地图上建造、采集、争夺地图资源的网络游戏
- 玩家可以自由的浏览整个沙盘地图
- 玩家可以自由的控制多只部队在沙盘上移动、战斗
游戏数据的抽象分类
- 静态的沙盘地图
- 地形地貌
- 固定的战略要地(如关卡、要塞)
- 沙盘上的动态对象
- 玩家的建筑、部队
- 周期刷新的资源点
- 地图的占领状态
- 玩家的养成数据
- 玩家之间的关系
游戏数据同步方式
- 静态数据无需同步,由服务器客户端各自按需从配置表或资源文件中读取
- 沙盘动态实体通过ECS框架进行同步
- 其他数据如养成数据、关系数据,由相关的游戏子系统通过传统的C/S消息同步
沙盘同步思路
如果强行要对沙盘同步方案分个类的话,应该属于状态同步(另一大类是帧/锁同步)。
对于大地图游戏来说,游戏世界内存在着大量的游戏对象,全部同步对服务器和客户端都是无法接受的。
所以首先我们要确定一种规则筛选出有限的对象同步给玩家。受游戏玩法的影响,不同于传统的MMORPG类游戏,slg向的游戏一般是上帝视角,可以不受约束的将视窗移动到地图的任何位置,所以MMORPG的以控制角色为中心的视野对+9宫格的同步方案就不适用了。我们采用地图块+视野窗口的模式来决定需要同步的对象。
接下来我们要设计一种结构来组织游戏世界中的各种对象,让它们的创建、交互、变化更新、销毁等等行为变得可控、易于同步。
传统游戏设计中大多采用面向对象的方式来设计,将游戏中的每一种事物(玩家、npc、资源点、帮会等等)封装成一类对象,每个对象有一个update方法,在主循环中不断遍历这些对象,调用它们的update方法实现游戏逻辑。
这是一套非常经典的模型,非常符合人的思维方式,并且也在各类游戏中得到了广泛的应用。不过它在某些方面有不是完美的。比如:
随着游戏功能的不断扩展,可能导致一个类中的内容会不断膨胀,或者会产生复杂的继承关系,再或者类多到爆炸,并且类之间的界限难以区分。
游戏系统以类对象为单位进行遍历,但往往这些对象中并不是所有的对象都符合系统本次遍历的需要,这时候实际上会产生很多的无效update
归根结底是因为OOP是以对象为核心,将所有的数据、方法全部集成到一个类对象的内部,由这个对象拥有全部的数据,并且负责处理全部的事务。
ECS框架
假设我们以功能逻辑为核心,将原来集成到一个对象内部的数据按功能进行切片,拆分成若干独立的功能组件(这里的组件可以理解为数据的载体,它提供基础的读写方法,但自己不包含引起变化的逻辑)。
系统负责驱动逻辑,在一次update中,会取出这个功能逻辑所关心的组件列表(功能操作涉及到的数据),遍历组件列表执行对应的功能函数。
举一个具体的例子:假设游戏中有一个对象叫做兵营,它存在于地图上,可以进行建造升级、移动、生产士兵,也可以被攻击摧毁
如果是传统OOP的写法,可能会写这样一个类
class CBarrack
{
//建造进度相关数据和函数
//移动相关数据和函数
//生产进度相关数据和函数
//耐久、燃烧状态等数据个函数
};
然后会用一个容器存放所有的兵营,比如List<CBarrack>
在主循环中,我们会遍历这个list,取出每一个兵营对象,并依次调用它的建造、移动、生产、耐久相关的方法,完成状态的更新。
现在我们把兵营的数据拆分开来,由这4个组件组合而成:
- 建造队列组件BuildComponent
- 移动组件MoveComponent
- 生产队列组件ProductComponent
- 战斗属性组件BattleAttrComponent
每一种组件有一个对应的容器统一管理,比如主城、矿场、医院的建造队列组件都放在同一个容器中
在建造系统中,我们遍历所有的建造队列,处理建造相关的逻辑(此时不光是兵营的建造组件,所有含有建造组件的实体的建造组件都会被一起处理)
在移动组件中,处理所有的移动逻辑(同样,这里的移动组件的所有者可能是部队、野怪、建筑)
更进一步,当建造完成时,可以将建造组件从兵营实体上移除,那么在下一此建造系统的遍历中,就不再需要遍历兵营了。
而假设后续游戏功能里又加入了一个部队可以驻扎到兵营中的功能,我们可以单独写一个驻扎组件,然后挂接到兵营实体上,而不需关心也不会影响到兵营已有的逻辑。
再假设这个驻扎的功能又改成兵营不能驻扎,而是箭塔可以驻扎,那么只需要把驻扎组件从兵营上移到箭塔上即可,外部系统完全无感。
这种以功能逻辑为核心,对对象的数据进行切片和组合的设计框架,被称作ECS(Entity+Component+System)框架
- Component(组件):对象中可以被独立处理的若干数据(或者说属性)的载体,提供对数据的最基础的读写功能,但不包含其他逻辑
- Entity(实体):对应OOP中的对象,只不过再ECS框架中,Entity只是一个“壳子”,一个可以按需求动态添加和移除组件的容器
- System(系统):游戏逻辑的驱动者,它提供实现各种功能的函数,关心的是实现功能所需要和会影响到的数据(组件),而不关心这些组件具体是属于哪个实体
ECS与状态同步的契合之处在于,状态同步关心的是游戏对象的数据(状态值)而非操作指令,而ECS本身就是由数据驱动的框架,服务器可以最大程度上只告诉客户端游戏对象的当前的数据是什么,而不是游戏内发生了什么。
至于ECS如何与具体的同步实现相结合,请听下回分解。