e c s的一些思考,以及思想整合

与传统的“类-继承”奉行的“我是什么”不同,基于组件化的ECS架构更强调的是“我有什么”,是一种组合优先的编程模式。使用组合而非继承,会使你的代码更具灵活性。还是上面的例子,针对游戏的玩法,我们会构建出一个英雄的Entity实体类,它更像一个空盒子,可以在创建英雄Entity实例的时候赋予它一个ID作为唯一标识。当我们将这个实体放到world下,也许什么也看不见,什么也做不了,这是因为它现在还什么数据都没有。此时就需要根据游戏的需求,来设计出不同的组件填充到这个实体当中。注意,应尽可能地保证组件设计上的扁平化,会让你的模块结构更加清晰,也大大增加了CPU缓存命中的概率。

举个例子,常见的组件包括而不仅限于

  • 渲染组件 :英雄的顶点、材质等数据,保证我们能正确地渲染到world中
  • 位置组件 :记录着实体在这个world的真实位置
  • 特效组件 :不同的时机,可能会需要播放不同的粒子特效以增强视觉感受

此外,根据策划的各种奇葩需求,还可以衍生出不同的功能性组件,本质上都是数据的集合,之后会交由 System 来进行各种状态修改与逻辑计算。比如,想要一个英雄既能变成汽车又能变成飞机,我们可以设计出 Wheel 和 Wing 两个组件,存储数据的同时也表明不同实体的对应功能或身份。当然对应着的是处理该组件的System,一个 FlightSystem 可以去关注那些持有 Wing 的实体。确切点说,FlightSystem 其实只需要关注 Wing 组件就足够了,它不应该关心是哪个实体持有这个组件,只要能修改 Wing 的状态就足矣。

这样,就实现了我们经常说的解耦。

将复杂的游戏拆解成不同的逻辑处理单元 (System) ,而每个逻辑处理单元只关心那些向它注册监听的数据,其他数据一概不管。并且最主要的是,System 是不保存状态的,Component 才是状态的真正持有者。刚开始的时候,也许会很不适应,总想着在 System 里加点什么标识,好方便地进行状态回溯或者复用。这时应该警惕起来,你所设计的 System 职责是否单一,组件持有的数据是否过于复杂。将一个复杂的模块拆解成若干个相对简单的单元,不失为明智的选择。

下面就是我们基于ECS而设计的新的游戏架构。

然而,现实总是残酷的。如果一个游戏真要这么简单,或许ECS也就没什么存在的价值了。仅就《守望先锋》分享所知,它们游戏中光 System 就上百个,并且为了保证 System 不保存状态,在游戏帧更新时,System 执行的时序就有了限制。并且很多情况下,很多System关心的组件只有一个(如输入事件),于是就有了Singleton Component。个人觉得,由于不同游戏的不同特质,某些情况下都很难去严格遵循ECS的架构约束,但毕竟架构是为人服务的,而不仅仅是束缚。在我们深刻理解了ECS的思想后,针对实际需要来做一些变通也是未尝不可的。

就拿我参与开发的一款轻MOBA类的多人对战游戏为例,采用的网络通讯方式是protobuf加状态同步,伤害、状态等判定结果几乎都是放在服务器端来处理,客户端主要就是根据每一帧接收的网络消息来处理相应的数据、更新状态。由于客户端使用的语言是Lua,因此会使用一个table数据结构来保存游戏中注册的 System 实例,在帧循环遍历这个注册表,按照顺序依次执行每个 System 的Update函数。System 会处理自己内部维护的组件池,里面放的是注册进来希望被处理的组件,从而根据这些组件的数据来进行一些逻辑上的操作。

解析protobuf数据后,每条协议消息发来的数据其实就是组件所要更新的,但由于服务器端并没有采用ECS这种设计模式,数据设计上也肯定会有些出入的地方,于是需要客户端来解析转换下。为此,引入了Driver的概念。首先,会有若干个表格来存放我们创建了的不同Entity 实例,每个Entity都会持有一个ID。服务器端下发的消息中总会包含某些实体ID,这样处理不同逻辑的 Driver 就会根据这些ID来找到它所需修改数据的Entity,再从Entity找出相关的 Component 组件,将proto消息里的数据更新给这个组件即可,剩下的工作就交给 System 了。

至此,我们的ECS设计变种成了这个样子:

上述设计中,由于多了一层Driver,并且游戏使用的是状态同步机制,因此帮助 System 分担了很多工作。System 仅仅是批量处理 Component 状态的管理者,每一帧遍历系统组件池里的所有组件 (此时的组件已经是 Driver 更新好数据了的) ,我们也可以根据自己的需要来设定刷新间隔,对于一些不需要在每个帧刷新都执行 Update 函数的 System ,可以降低它们的更新频率从而节约一些性能开销。而 Driver 层面,只是数据的一道“搬运工”,负责更新给 System 能够识别的组件的持有数据。

当所在团队改用ECS后,起初都很不习惯这种新的编程模式,总会不知不觉中切换回“类-继承”的编程思路。但随着项目的推进,ECS所带来的一些优势变得愈发明显。首先,就是降低了团队协作成本。往昔的项目经历中后期时,总会出现某些又臭又长的 God Class ,甚至会出现一些功能重复的模块,同一个功能的函数被实现了两次!这种情境下,代码的维护成本可想而知。而在ECS编程模式下,每名开发人员,只需要关心自己负责的模块即可,System 很好地隔离模块之间的耦合。

posted @ 2021-11-03 11:43  专杀小三  阅读(156)  评论(0)    收藏  举报