帧同步技术(一)

关于帧同步技术原理及实现方案网上已经有非常多的文章介绍了,但做《月夜狂想曲》的帧同步战斗时还是有遇到很多具体的细节问题,也做了很多相关处理,在这打算分两篇文章记录一下。第一篇主要讲客户端如何保持一致性,会遇到哪些问题以及怎么处理。第二篇主要讲通过哪些改进可以让帧同步有更好的游戏体验。

帧同步的原理是,服务器只负责收集指令,然后以恒定帧率(例如30帧1秒)转发指令,真正的游戏逻辑由各个客户端各自计算,客户端要等到服务器派发的指令才能推进逻辑,没有收到指令时逻辑是暂停的(LockStep)。帧同步最重要的是确保各个客户端在不同硬件不同平台下各自计算游戏逻辑结果都是一致的。在Unity中为了达到这个目标,我们需要解决以下问题:

随机数:Unity自带的随机数因为有可能会被物理系统,粒子系统,UI系统,第三方组件等各种模块使用,我们是无法保证他在不同客户端下每次都能取到统一的随机数的。这就需要我们自己实现一套自己控制的随机数发生器,关于如何实现随机数的算法,之前写过一篇文章介绍《随机数生成算法》。在有可控的随机数发生器后,我们需要查找项目中所有用到随机数的地方,判断该流程是逻辑相关需要被统一接管的,还是表现相关可以由各个客户端自由决定的,从而使用不同的随机数发生器。 目标就是确保逻辑相关的流程下在各个客户端下都能取到同样的随机数。

时间: Unity自带的时间因为各个客户端硬件,运行帧率的差异而无法保证结果相同,我们需要维护一个统一的逻辑时间。假设服务器以1秒30帧的速度广播操作,那逻辑的DeltaTime就是固定的0.0334,而逻辑的Time就是当前帧数*DeltaTime。与随机数接管一样,时间的接管也要排查项目中所有用到UnityEngine.Time的地方,看看是否需要改为我们自己维护的逻辑时间。

延迟回调:与Time相关的另外一个是延迟回调,因为系统时间是不可靠,自己维护的逻辑时间才能保证各个客户端的统一,所以我们需要自己实现一套基于逻辑时间的延迟回调机制。直接用Unity的Invoke或其他带延迟功能的接口写的逻辑都是无法保证在各个客户端下一致的。

Update等相关接口:MonoBehaviour相关的一些接口,例如Update、LateUpdate,Start等都是跟着渲染帧调用的,并且同一帧内不同客户端的不同对象调用顺序也会不同。我们必须自己接管这些调用,把Character,Bullet等游戏对象管理起来,在逻辑帧推进时按照所有客户端统一的顺序去调各种对象的各个接口,从而保证一致性。

同步事件:在客户端战斗的过程中会触发各种同步事件,例如出怪、刷Buff、刷掉落,传送等。这些事件需要各个客户端在同一逻辑帧内针对服务段下发的一批数据做一些相应的操作,这就是同步事件。同步事件相关的数据会由服务端先通过TCP下发到各个客户端,并在延后的第5帧的逻辑广播中带上同步事件ID。客户端判断如果已经收到该ID的同步事件具体内容,就可以推进这一帧并处理同步事件,如果没收到就需要等收到具体内容才能推进这一帧。

主角/镜头等判断:在场景控制,机关,怪物AI等游戏逻辑中,经常会有读取主角\镜头位置,判断是否碰到主角等逻辑。针对不同的客户端,如果判断是否是自己的话这些逻辑的结果肯定是不一样。针对这些接口我们需要改造成兼容所有玩家的逻辑,例如取最近的玩家。保证各个客户端得到的结果是相同的。

浮点数处理:在我们的逻辑计算中难免会用到浮点数,浮点数为什么会有精度问题在《浮点数二进制表示》有讨论,这里讲一下如何避免精度问题。在涉及浮点数计算时,我们将浮点数按照小数点后两位的精度来转换成整数后再进行计算,基于整数计算完的结果再除100变回浮点数,从而避免精度问题。浮点数保留小数点后两位转整数的具体做法是,先利用Mathf.FloorToInt(f*100)得到nValue1,然后比较f*100与nValue1以及nValue1 + 1的差值绝对值,取更小的那个。这样做就可以让2.9999999,3,3.0000001这样的浮点数都转换成整数300来计算,避免精度问题。

物理引擎:Unity自带的物理引擎因为底层使用DOTS技术做了并行计算优化,所以会有各种不同步的问题。最好的做法是换一个基于定点数,能提供各个客户端一致性保证的物理引擎来使用。但在我们项目中因为所有的移动逻辑都是我们自己实现的,物理引擎承担的作用仅仅是碰撞检测,所以用原生的物理引擎也能保证一致,但需要处理以下问题。第一是调用顺序的接管,物理引擎如果autoSimulate为true时,是跟着fixedUpdate的间隔调度的,但在帧同步中我们需要关闭autoSimulate,然后在逻辑帧推进中自己调用Simulate,传入的deltaTime就是固定的0.0334。第二是上面有提到过的浮点数问题,不同客户端在利用物理引擎碰撞检测返回的浮点数时,需要作刚刚提到的精度处理。第三是不同客户端的碰撞检测返回结果顺序有可能是不一样的,一个客户端发射射线检测碰撞对象结果可能是A,B,C,而另一个客户端的结果可能是B,C,A,这就需要我们针对碰撞返回的数据进行排序,从而保证处理的顺序在不同客户端上都是相同的。第四个还是时序问题,如果我们实现了OnTriggerEnter这些物理引擎的回调函数,在不同客户端上的执行顺序也是无法保证的。针对这钟情况我们不能依赖物理引擎的碰撞回调,而是在我们自己保证一致性的逻辑中每帧进行所需要的碰撞检测,如果满足条件再调用相应的逻辑。最后一点是控制的对象都要勾选isKinematic,避免物理引擎对他们的修改。Unity原生的物理引擎并不适合用来做帧同步,游戏对象的位置旋转等信息不能让物理引擎去修改,一定要自己维护。物理引擎仅用来做碰撞检测,同时需要注意上面所说的时序问题及浮点数问题。

第三方组件:项目中用到的第三方组件,也需要看看是否有用到系统的时间、随机数,还有是否有浮点数精度问题。检查第三方组件的代码,如果有上面提到的各种有不一致的问题就要针对这些问题进行处理。

以上就是之前项目为了确保帧同步战斗在不同客户端有相同结而处理的各种具体问题,核心就是保障逻辑的一致性。下一篇文章会分享优化帧同步做的其他处理,例如表现与逻辑分离,UDP传输,快进处理,不同步问题定位排查以及验证服务器等。

Tagged , . Bookmark the permalink.

Comments are closed.