从用cocos creator开发游戏开始,一直有一个想法,既然cocos客户端使用ecs来组织逻辑,而服务端大多采用oop模式,为什么不把服务端和客户端的设计模式进行统一呢?
首先参考了一下相关资料 《守望先锋》架构设计与网络同步
然后回顾一下ecs几大设计原则后:
- Component是纯数据结构
- Entity是Component的容器
- System不关心具体的Entity,仅通过监听Component类型来实现逻辑
就开始动手写了
###组件的问题
- 在开始写ECS的过程发现,System监听Component时,如果每次Entity挂载一个Component时需要向所有System发送注册事件,而多个不同的System是可能注册了相同或者相近的Component.
- 查阅相关资料后添加了Group类,Group是映射带有一个和多个Component的唯一集合,每次Entity增删Component时向索引的Group进行注册和释放操作,而System向ECS注册时会先查询是否有对应的Group,如果没有就重新创建一个.
- 单例组件比较特殊,全局唯一所以可以直接从ecs实例获取.
- 因为Cocos引擎隐藏Node->Component的具体实现,所以仅采用了Component包裹CCNode的方式来同步原始Component的数据,仅将Cocos作为一个渲染器使用.
###网络同步
- 网络同步是写这个ecs库的目的,经过对帧同步和状态同步的比较后,选择了通用性比较好的状态同步设计.
- 为了减少状态同步的网络流量,同步采用差量同步的设计:在每次ECS系统产生变动时,对当前逻辑帧编号自增,如果当前帧Entity产生了脏数据,就把Entity的step置为当前的逻辑帧号,每次客户端会发送实体的id和step到服务器,服务器检测客户端的实体是否需要删减或者更新,就可以实现Entity的增量更新.
- 这里还未完成的工作是快照机制和在服务端保存一段时间内的Entity挂载的Component缓存,从而实现对Entity内部Component进行逻辑更新,进一步减少同步流量.
- 还有一个遇到的问题是不同客户端或者用户角色Component中的数据可见性,解决方式是在Component增加一个onSync方法,服务器根据客户端的uid来选择性修改或隐藏敏感信息.
###碰撞检测
- ECS完成后碰撞检测实际上是一个具体实现的System,这里通过对比四叉树和层次包围二叉树的性能效率选择了后者.
###总结
- 最后完成的ECS在可以做到将逻辑和渲染完全分离,客户端的System只关注于Component的UI实现,而服务器专注于逻辑,这里一个意外的好处就是可以将服务端游戏逻辑的开发完全放到客户端上来,完成后再部署到服务器.
- ECS最大的优点是因为系统之间松耦合,模块的复用性能做到很高,单个逻辑编写也非常简单.
- 目前发现的缺点是System之间的执行顺序关系十分考验逻辑编写,而System之间的调用会使复杂性成倍上升,所以放弃System间事件分发的机制,所有System间的事件通过Singleton单例来进行传递.