基于ECS的游戏同步设计
前言
早期为了实现对服务器的快速实现,忽略了系统上的设计。使用传统的面向对象的方式对业务需求进行实现,导致了项目在研发的中期遇到了网络数据上的冗余数据和数据压缩的瓶颈。同时因为消息结构的频繁修改也导致客户端和服务器消息处理模块的代码变得越发冗余絮乱,维护性极差。在这样的情况下,我们决定对网络消息的同步模式进行重构。
ECS(实体组件系统)
因为我们团队一直在使用Unity这款引擎,所以对 ECS 这个架构也是非常认可的,并且认为在游戏开发中,面向数据编程的设计是优于面向对象的。经过讨论我们决定使用ECS来设计这套系统。
那么什么是ECS呢?
首先ECS得全称是:Entity Component System 实体组件系统
E:Entity - 实体 - 游戏世界中的单位
C:Component - 组件 - 赋予实体行动能力
S:System - 系统 - 处理组件行为的中枢
根据以上几点我们能很明确,用ECS架构来驱动我们的游戏开发。
设计目标
从设计上来说用任何架构都不能过度设计,任何一个好的设计都需要一个合理的底层支撑。所以在确定用什么方式来设计后,我们开始确定目标。即我们要用该架构来实现那些功能以解决什么问题,并且为什么这么做。
得到以下几点是我们需要达到的目标:
1.双端只需面向本地数据编程。服务器完成每一个帧的处理后,提交即可。
2.重复性高的代码(Components)通过预制结构生成双端代码。
3.同步数据要实现增量更新。
4.组件结构成员可以设置选项。(如:是否增量更新)
以上即为我们需要实现的几个目标。
为什么如此做:
1.双端只面向本地数据进行编程,不用再需要在去处理网络数据。网络数据在核心层已经处理。每一帧核心层同步数据后会通知GamePlay开始处理。我们只需要把注意力放在System即可。
2.在ECS的设计上Component是只存在状态的。所以这块我们可以通过配置文件直接生成代码,节省人力投入的成本。在实际中还有一些相关模块是生成的,思想大致都是为了让正确性更高,并且提升开发的效率。
3.服务器每一帧的下发都是在同步数据,实现增量更新就是为了压缩网络传输。
4.有的组件内的相关数据是不需要同步或者增量更新的,所以加此选项。(比如临时属性等字段)
设计
首先我们通过 Protobuf 的结构化方式预制了一套同步消息的结构。
以上就是每一个帧会下发的组件消息。 打包组件帧消息。(这块代码由生成器生成) 对象的目录结构,所有以"_gen"结尾的文件均为生成。csharp目录下的为对应客户端的通用代码。通过以上这样设计后,我们只需要在 proto 文件中添加新的 component 结构便可以生成一个新的组件代码和更新数据同步代码。从某种意义上来讲我们就把从前的增加消息变成了增加组件。也把一系列繁琐的流程简化下来。服务器客户端开发者只需要添加一个 system 然后实现一个消息下发接口。去处理自己感兴趣的控件信息就可以,就像如此:
系统目录结构 移动系统处理逻辑这样就大致实现了我们面向本地数据的编程。客户端就像一个播放器,它的每个系统都是安全的。只要每次给它的帧数据相同,它甚至能回放。当然我们也结合了逻辑帧 ( 20/s ) 的概念在其中,否则不可能会实现回放的效果。
总结
通过 ECS 这种设计模式且配合上逻辑帧与生成器重构了底层传输和游戏上的系统实现后,使我们现在的系统内聚性更强,耦合性更低。Entity就像一个通用媒介一样,每个系统都自动关联了感兴趣的Entity。系统处理上得针对性也更精准。而 Component 则提供了 System 处理时所需要的一切数据。在这套架构下,客户端只需要面向 Entity 和 它的Components 进行编程,而不用再去存一堆数据,写一堆管理器了。
以上就是我们本次的分享。喜欢的小哥哥、小姐姐给个赞~ 谢谢~