从今年下半年开始制作一款实时对战游戏以来,我就在着手写一个帧同步的游戏框架,其中包含了服务器框架和客户端框架,该框架目前已经开源。 首先,我希望写一个前后端能统一语言的框架,以至于在前端写好的游戏逻辑,拿到后端就可以直接使用。 目前看来,这两个目标都得到了比较好的完成。 首先要解决的是前后端语言一致的问题 这里我使用了一个c#服务器框架 SupperScoket 1.导出这个框架到在Mono上运行时报一个找不到window API的错误,解决方法是使用.Net 4.0以上版本的SupperSocket 2.框架在使用TCP模式时,有时会报出一个Send byte Time out 的异常,解决方法是使用TrySend方法,并在返回false时关闭连接。 3.框架在解析消息时,遇到不完整的消息没能正确解析,这里他的文档不是很详细,其实要把未解析的数据数量缓存起来,详情看这篇博客,源码在此。 第二个要解决的是同步框架的问题 这个问题比较复杂,如何在书写游戏逻辑的时候感受不到同步问题的存在?如果每个事件都要等服务器的回包,还要体验流畅,只能从预处理和追赶两个角度入手。 预处理就是,这个事件还没有发生,但是考虑到网络延迟的存在,提前先把结果发送给每个客户端,然后客户端到了这个时刻再把这个事件表现出来,典型的例子就是皇室战争。 如果说没有办法做到预处理呢,比如说玩家的操作需要立即响应,那么其他玩家收到这个事件的时候必然已经迟了,所以就要做追赶,比较典型的就是影子跟随算法。 但是这两种做法必然要在游戏逻辑中做对应的处理,开发者要时刻清醒此时是预测还是追赶,增加逻辑的复杂性,而且游戏的表现可能也参差不齐,有些地方也许同步的好,有些地方可能不好,要调优需要在每个地方都下功夫,增加开发时间。 那么应该怎么办呢,最理想的方法当然是全部当成本地计算,这样就无需考虑是追赶还是预测的问题了,那么网络游戏怎么全当成本地计算呢?当然就是帧同步了。 关于帧同步网上已经有很多资料,在此不再赘述,但是关于帧同步有一个核心的问题,那就是它在网络差的时候表现很差,这一点我们可以从星际争霸和魔兽争霸这些游戏中看出来,一旦有人卡顿,所有人都要停下等这个人的消息,但是我们知道手游《王者荣耀》这款游戏就是帧同步做的,他是怎么解决这一问题的呢?在《王者荣耀》负责人在unite 2017大会分享中我们没有看到这一解决方案,我感觉有可能是乐观帧同步,但是在看了暴雪分享的守望先锋同步机制之后,我得到了一个我自己的解决方案。 那就是预测回滚和解。 原理很简单,游戏开始时,每个客户端按照帧同步的方案推进着游戏,但是如果遇到服务器没能及时返回其他玩家操作的时候,给对应的玩家预测一个操作(复制该玩家最后一次操作),并继续推进游戏,如果在其后收到了服务器玩家关于这个人的操作,则把游戏回滚到预测开始的那一帧重新计算一遍,然后和现在游戏世界的表现和解。 如果服务器迟迟没有收到某个玩家的消息,则会给这个玩家预测一个消息(复制该玩家的最后一次操作)然后推送给所有玩家,包括那个掉线的玩家。其他玩家会以这个预测操作为准计算接下来的游戏世界,而这个掉线玩家也会收到这个预测操作,并且替换掉玩家实际进行的操作,重新计算一遍游戏世界。保证每个客户端的输入一致。 原理说起来简单,但是其实有几个难点。 第一个难点就在于回滚,如何回滚到预测开始的那一帧呢,要记录下每一帧的变化,然后逐帧退回吗?还是把每一帧的数据做一个快照保存下来? 其实这个问题实现起来不难,关键是从性能考虑,如果把每一帧的数据都快照下来,内存可能会吃紧,如果做逐帧退回的方式,实现起来相对复杂,并且在性能上也可能有问题。 这里就引入了ECS架构帮助我简化了这一问题,在ECS架构中,C 也就是component(组件),它是纯数据的集合,并且 E 也就是 Entity(实体) 集中存放在一起,这方便了我对它们的集中操作, 在ECS架构的帮助下,我实现了对组件进行快照式的存储,对实体进行了增量式的存储,实现了对数据的回滚。 第二个难点在于和解,由于预测操作和玩家真实操作的不同,重计算出来的世界必然预测的世界有差异,那么怎样以尽量不引人注意的形式,把预测世界过渡到真实世界呢,这一点守望先锋的分享中提到了一部分,但是没有完全解答这个问题。 实际上解决这个问题的思路是,先确定哪些是可以和解的,哪些是不可以和解的,然后分头处理。怎么分头处理呢,就是可以和解的在预测计算中就表现,不可以和解的,要等到真正的数据来了才进行表现。 那么哪些是可以和解的呢?就是在玩家不知不觉间就可以过渡到的,比如说物体的位置,动画。这里有很多的技术可以做这种和解,比如说影子跟随算法。 不可以和解的比如说冒出的血条数字,你不能说伤害数字都冒出来了,然后又塞回去。 但其实有一个难点是,飞弹能不能和解? 显然,飞弹的位置是可以和解的,但是飞弹的创建与销毁呢?这里涉及到一个游戏表现的问题,如果飞弹的创建要等到服务器回包才出现,那么这个表现在网络差的时候就太糟糕了。 下面是解决方案 其实一部分解决方法在难点1已经提到了,首先要建立一个对实体的回滚系统,保证飞弹能回滚。 很自然的想到可以延迟派发创建的事件,在数据层面这个实体已经被重计算的很多次了,但只要这个实体仍然存在我就不再派发这个实体的创建事件。销毁也是一样。 但是我如何确定我两次创建的实体的是一个呢?要知道我们框架的设计目标是开发时尽量避免对同步系统的感知,也就是我们游戏逻辑并不知道现在的数据是真实的数据还是预测的数据,要在创建这个体的的时候判断这个实体是否已经在预测时创建过了显然不应该是我们游戏逻辑应该做的,可我们的框架又如何确定两个实体是否一致呢。 直接比较它们两个是否相等肯定不行,把他们的数据取出来一一比对又太耗时。 第三个难点是重计算的性能,在我开发的早期版本时,游戏逻辑执行一帧要耗时5ms,如果此时客户端预测了5帧,那么收到服务器消息再重计算需要25ms才能计算的完,在网络延迟更大的时候,游戏性能是不可接受的。 解决这个问题要从优化游戏性能和限制预测帧数入手,我优化了ECS的几个最基本API的执行性能,再用四叉树优化了碰撞系统,把游戏逻辑的执行时间缩短到1ms左右,然后又通过服务器控制客户端的预测帧数,使其不至于过大导致沉重的重计算负担。 再说一点其他的技术细节 1.实体的集中创建与销毁 2.断线重连 3.常见的不同步情况 参考资料: 云风:浅谈《守望先锋》中的 ECS 构架 |
|