由于实际投放中的种种问题,去除了回滚机制,整体的同步方案也做了修改:
1)客户端在收到服务器下发的驱动帧之前,逻辑帧不能再向前演算;
2)客户端在每个逻辑帧内都会将最新收集的指令打包上传到服务器(不包含客户端帧号),服务器在下一个驱动帧中会广播所有上传来的客户端指令集;
3)客户端在收到服务器的驱动帧后,解析出指令并缓存在本地下一将要执行的逻辑帧用户指令中;
4)客户端本地timer发现可以继续执行逻辑帧时,会计算当前的时延,调整自身的间隔,并向后演算逻辑帧;
5)演算逻辑帧时会更新各Entity的速度等属性,渲染层根据新的速度对各Entity插值。
上述方案存在一些问题。
对于点2),当前假设当前刚执行完逻辑帧N-1,而网络帧N尚未到来,逻辑帧N迟迟不能执行,那么在逻辑帧N-1~逻辑帧N间隔内收集的玩家指令必须在网络帧N到来之后,也就是执行逻辑帧N时才能发出,最早也要在网络帧N+1广播出来,那么玩家指令最早也要在逻辑帧N+1才能执行,无故增加了延迟时间。后来针对此点做了改进,每个渲染帧内都会搜集玩家指令并上传(部分重复的指令可以剔除),这样在网络帧N到来之前,依然有部分玩家指令有机会在网络帧N被收集,并广播执行。
非主玩家以及AI在渲染层该如何预测。因为逻辑帧总是落后于渲染帧,因此渲染帧会以逻辑帧的数据作为起点并向前计算一段时间,以得到的终点作为当前目标点插值处理。
但是对于主玩家则情形不同。为了提升玩家的手感体验,期望玩家的操作指令能立即得到执行。在状态同步方案或者支持回滚的帧同步方案下这点都很容易实现,但是帧同步去除回滚后,由于要等待自身指令广播回来,造成了玩家操作上的延迟,手感降低了很多。因此考虑玩家操作时立即在渲染层执行,但这样带来的问题是,稍后网络帧回来后,渲染层的位置和网络帧的位置的偏差如何解决。此时可以考虑对渲染层的主玩家的移动插值,尽可能接近逻辑层的演算目标位置,但这样一是会影响玩家的操作,二是如何插值接近也很复杂。比如一种办法是在逻辑层没有玩家指令执行时对渲染层进行插值处理,但这样就可能会出现每个网络帧之间都有玩家指令时,一直没有机会做足够的插值,导致渲染帧与逻辑帧的误差越来越大的情况。
因此我又考虑了另一种方案。玩家不仅同步自身的方向,还要同步自己的位置。即主玩家在每个动作收集帧中都会将当前的运动方向和它下一帧运动后的位置提交到服务器,并在下一个网络帧中广播给包括自己在内的所有客户端,改变逻辑层对应的位置信息,这样就是逻辑层“追随”渲染层了,如何减少二者之间的误差是重点。它有几个点可以优化。一是在服务器的网络帧N之前,玩家的所有指令都会被收集到帧N中,而不管客户端自己走了几个渲染帧或动作帧。玩家可以在自己的动作帧(与网络帧间隔相同)预计算出下一动作帧的位置并提交给服务器,而在下一动作帧到来之前,玩家始终按照之前提交的指令运行,这样就可以保证位置信息尽可能早地同步出去,到达各客户端时可以抹去一部分网络延时;二是同时提交方向信息,如果某一网络帧内没有收集到任何动作指令,则有两种情况,一种是由于网络延迟,导致这个网络帧内的指令没有同步下来;二是玩家在这个网络帧间隔内确实没有发出新的指令,比如改变运动方向或者停止再启动等。可以由其它客户端继续模拟,从而减少信息同步量。三是预测各端的位置时根据当前的本地与服务器帧差动态改变各端的速度。
在实现中又发现一个问题,虽然设定的是每隔25MS执行一帧(Laya.Timer.delay),但是测定实际时间间隔至少在30MS以上。每一逻辑帧是两个渲染帧,按照50MS的固定数值计算的,而实际的时间间隔则至少是60MS,每个渲染帧使用实际的时间间隔计算渲染位移。这就导致渲染帧与逻辑帧并未拉开差距,但是渲染位移上的差距却越来越大。为了解决这个问题,只能在固定渲染帧中以固定的时间间隔(__BattleSimulatorLoopInterval__/2)计算渲染位移。而如果渲染帧与逻辑帧之间拉开差距,则计算倍率factor,代入渲染位移的计算。一种做法是发现帧拉开差距时就动态调整timer的更新间隔,但是这样带来的问题是当渲染更新间隔很大时,比如50MS一渲染帧,会造成渲染帧数下降,表现上有跳跃,即飞机位移跳动过大,导致卡顿效果。因此需要以固定的间隔渲染。