Difficulties and frame synchronization optimization solutions

This part of the frame synchronization more complex, there are many optimization minutiae point, there are some different optimization directions, depending on the type of program, the operation feel of the requirements, the number of online players, etc., have different difficulties and pain points. Optimization of different directions, optimizes the difference between practices that may lead to some controversy. And frame synchronization, itself, there are many variants to respond to different needs. Therefore, this article is based on all of the types of projects (ACT) program and optimized to do, not necessarily suitable for other games also need to frame synchronization, so in advance to say about this in order to avoid unnecessary misunderstanding.

Frame synchronization of several difficult

Fundamentals frame synchronization, and synchronization status and distinction, many articles have been introduced, I will not go into details, you can own google. The following just say a few difficulties.

Ensure that the client's own calculation properly, namely consistency

Synchronization-frame basis, different clients, based on the same sequence of operational instructions, each execution logic, the same effect can be obtained. As we all know, in the Unity engine, different calling sequence, timing, floating-point computations deviation uncertainty sorting container, Coroutine write logic within the uncertainties, the physical floating point number, with a random number value to uncertainty and so on.

Some are easy to solve, such as random values, you can only do random seed.

Note that some code specifications, such as battle in the frame synchronization logic does not use Coroutine portion, the container does not depend on the cycle sequence similar uncertainty Dictionary the like.

As well as the most basic, through a unified logical Tick Entrance to update the entire battle logic, rather than themselves to each logical Update. Tick ​​to ensure that each are from top to bottom, the same order each execution.

Physical aspects, because we do not need physical battle logic, logic collision crash all her own, so do not skip to say, this can refer to other articles.

Finally, floating-point calculation can not guarantee consistency, we need to convert to fixed-point. On the realization of fixed points, relatively simple way, on the basis of superior original 1000 or 10000 floating-point numbers, divided by the corresponding local 1000 or 10000, this approach is the most simple, supplemented by trigonometric look-up table, can solve some problems, reduce the probability calculation inconsistent. However, this approach is a temporary solution way, there are some risks (for example, such as an int and a float multiply, if the original value will * 1000, that it calculated the value may be very large, cross-border risks.)

The best solution is: use to achieve a more precise and rigorous, and proven fixed-point math library, in C #, there is the realization of a fixed point, and an earlier version of Photon network, Truesync have a very good fixed-point implementation.

Enter an image description
Realize fixed points

 

Which FP, you can completely replace Float, we just want our own logic part, Float and other transformation of FP, can be easily solved. And, can be a good and serialization our Protobuf integrated (note code Attribute, below), we ensure that the profile is fixed points.

Enter an image description

 

TSVector corresponds Vector3, as long as the FP based, can extend their own data structures. (Of course, if you use a complex plug-in, and do not open, then for the transformation of fixed-point, will be much difficulty)

Enter an image description
Trigonometric functions implemented by table look-up approach to ensure accurate fixed-point numbers

 

I personally think that this set of implementation is better than simply multiply 10000, in addition to 10,000 way. Bring harm, it may compute performance slightly worse point, but we have a lot of test down, very small impact on computing performance, should be capable of performing the needs of most projects.

For the calculation of the uncertainty, we also have some small risk that we use to detect the Physics.Raycast floor and walls, so that people can be on the downhill, stairs and other uneven road, may have irregularly shaped wall. Here'll get a float position, which may lead to uncertainty, here we use the cut-off value, etc., to avoid as far as possible, after repeated testing, there have been inconsistencies. But this way, after all, logically, there is a risk, a better way is to achieve a fixed-point raycast based mechanism, we have limited manpower, there is no time and energy to do it. This article has put some of the more detailed, we can see:
a frame synchronization: floating-point precision test
https://zhuanlan.zhihu.com/p/30422277

Achieve frame synchronization network protocol

在处理好了基础的计算一致性问题后,我们就要考虑网络如何通信。这里,我不谈p2p方式了,我们以下谈的,都是多client,一个server的模式,server负责统一tick,并转发client的指令,通知其他client,可以参看文章:
网游流畅基础:帧同步游戏开发
http://www.10tiao.com/html/255/201609/2650586281/4.html

首先,是网络协议的选择。TCP和UDP的选择,我就不多说了,帧同步肯定要基于UDP才能保证更低的延迟。在UDP的选择上,我看网上有些文章,容易导入一个误区,即,我们是要用可靠传输的UDP,还是冗余信息的UDP。

基于可靠传输的UDP,是指在UDP上加一层封装,自己去实现丢包处理,消息序列,重传等类似TCP的消息处理方式,保证上层逻辑在处理数据包的时候,不需要考虑包的顺序,丢包等。类似的实现有Enet,KCP等。

冗余信息的UDP,是指需要上层逻辑自己处理丢包,乱序,重传等问题,底层直接用原始的UDP,或者用类似Enet的Unsequenced模式。常见的处理方式,就是两端的消息里面,带有确认帧信息,比如客户端(C)通知服务器(S)第100帧的数据,S收到后通知C,已收到C的第100帧,如果C一直没收到S的通知(丢包,乱序等原因),就会继续发送第100帧的数据给S,直到收到S的确认信息。

有些文章介绍的时候,没有明确这两者的区别,但是这两种方式,区别是巨大的。可靠传输的UDP,在帧同步中,个人认为是不合适的,因为他为了保证包的顺序和处理丢包重传等,在网络不佳的情况下,Delay很大,将导致收发包处理都会变成类似TCP的效果,只是比TCP会好一些。必须要用冗余信息的UDP的方式,才能获得好的效果。并且实现并不复杂,只要和服务器商议好确认帧和如何重传即可,自己实现,有很大的优化空间。例如,我们的协议定义类似如下:

Enter an image description
双方都要通知对方,已经接受哪一帧的通知了,并通过cmd list重发没有收到的指令

 

这里简单说一下,对于这种收发频繁的消息,如果使用Protobuf,会造成每个逻辑帧的GC,这是非常不好的,解决方案,要么对Protobuf做无GC改造,要么就自己实现一个简单的byte[]读写。无GC改造工程太大,感觉无必要,我们只是在战斗的几个频繁发送的消息,需要自己处理一下byte[]读写即可。

在这部分需要补充一下,KCP作者韦易笑提到KCP+FEC的模式,可以比冗余方式,有更好的效果,我之前并没有仔细研究过这个模式,不过可以推荐大家看一下。

因为我们项目早期,服务器定下了使用Enet,我评估了一下,反正使用冗余包的方式,所以没有纠结Enet或KCP,后续其实想改成KCP,服务器不想再动,也就放下了。

Enet麻烦的地方是,Enet的ipv6版本,是一个不成熟的Pull Request,Enet作者没有Merge(并且存在好几个ipv6的Pull Request),我不确定稳定性,还好看了下Commit,加上测试下来,没有太大问题。KCP我没有评估过ipv6的问题,不过Github上有C#版本,改一下ipv6支持应该很简单。

逻辑和显示的分离

这部分在很多讲帧同步的文章中都提过了。配置的数据和显示要分离,在战斗中,战斗的逻辑,也要和显示做到分离。

例如,我们动作切换的逻辑,是基于自己抽象的逻辑帧,而不是基于Animator中一个Clip的播放。比如一个攻击动作,当第10帧的时候,开始出现攻击框,并开始检测和敌人受击框的碰撞,这个时候的第10帧,必须是独立的逻辑,不能依赖于Animator播放的时间,或者AnimatorStateInfo的NormalizedTime等。甚至,当我们不加载角色的模型,一样可以跑战斗的逻辑。如果抽离得好,还可以放到服务器跑,做为战斗的验证程序,王者荣耀就是这样做的。

联机如何做到流畅战斗

前面所有的准备,最终的目的,都是为了战斗的流畅。特别是我们这种ACT游戏,或者格斗类游戏,对按键以后操作反馈的即时性,要求非常高,一点点延迟,都会影响玩家的手感,导致玩家的连招操作打断,非常影响体验。我们对延迟的敏感性,甚至比MOBA类游戏还要高,我们要做到好的操作手感,还要联机战斗(PVP,组队PVE),都需要把帧同步做到极致,不能因为延迟卡住或者操作反馈出现变化。

因为这个原因,我们不能用Lockstep的方式,Lockstep更适合网络环境良好的内网,或者对操作延迟不敏感的类型(例如我听过还有项目用来做卡牌类的帧同步)。

我们也不能用缓存服务器确认操作的方式,也就是一些游戏做的指令Buffer。具体描述,王者荣耀的分析文章,讲得很具体了。这也是他们说的模式,这个模式能解决一些小的网络波动,对一些操作反馈不需要太高的游戏,例如有些游戏攻击前会有一个比较长的前摇动作,这类游戏,用这种方式,应该就能解决大部分问题。但是这种方式还是存在隐患,即使通过策略能很好地动态调整Buffer,也还是难以解决高延迟下的卡顿和不流畅。王者荣耀优化得很好,他们说能让Buffer长度为0,文章只提到通过平滑插值和逻辑表现分离来优化,更细节的没有提到,我不确定他们是否只是基于这个方式来优化的。目前也没有看到更具体的分析。

指令Buffer的方式,也不能满足我们的需求,或者说,我没有找到基于此方式,能优化到王者荣耀的效果的办法。我也测试过其他MOBA和ACT,ARPG类游戏的联机,在高延迟,网络波动情况下,没有比王者表现更好的了。

最后,在仔细研究了我们的需求后,找到一篇指导性的文章,非常适合我们:
Understanding Fighting Game Networking
http://mauve.mizuumi.net/2012/07/05/understanding-fighting-game-networking/

这篇文章非常详细地介绍了各种方式,最终回滚逻辑(Rollback)是终极的解决方案,国内也有文章提到过,即:
Skywind Inside 再谈网游同步技术
http://www.skywind.me/blog/archives/1343#more-1343

文章里面提到的Time Warp方式,我理解回滚逻辑,和Time Warp是一个概念。

游戏逻辑的回滚

回滚逻辑,就是我们解决问题的方案。可以这样理解,客户端的时间,领先服务器,客户端不需要服务器确认帧返回才执行指令,而是玩家输入,立刻执行(其他玩家的输入,按照其最近一个输入做预测,或者其他更优化的预测方案),然后将指令发送给服务器,服务器收到后给客户端确认,客户端收到确认后,如果服务确认的操作,和之前执行的一样(自己和其他玩家预测的操作),将不做任何改变,如果不一样(预测错误),就会将游戏整体逻辑回滚到最后一次服务器确认的正确帧,然后再追上当前客户端的帧。

此处逻辑较为复杂,我尝试举个例子说明下。

当前客户端(A,B)执行到100帧,服务器执行到97帧。在100帧的时候,A执行了移动,B执行了攻击,A和B都通知服务器:我已经执行到100帧,我的操作是移动(A),攻击(B)。服务器在自己的98帧或99帧收到了A,B的消息,存在对应帧的操作数据中,等服务器执行到100帧的时候(或提前),将这个数据广播给AB。

然后A和B立刻开始执行100帧,A执行移动,预测B不执行操作。而B执行攻击,预测A执行攻击(可能A的99帧也是攻击),A和B各自预测对方的操作。

在A和B执行完100帧后,他们会各自保存100帧的状态快照,以及100帧各自的操作(包括预测的操作),以备万一预测错误,做逻辑回滚。

执行几帧后,A和B来到了103帧,服务器到了100帧,他开始广播数据给AB,在一定延迟后,AB收到了服务器确认的100帧的数据,这时候,AB可能已经执行到104了。A和B各自去核对服务器的数据和自己预测的数据是否相同。例如A核对后,100帧的操作,和自己预测的一样,A不做任何处理,继续往前。而B核对后,发现在100帧,B对A的预测,和服务器确认的A的操作,是不一样的(B预测的是攻击,而实际A的操作是移动),B就回滚到上一个确认一样的帧,即99帧,然后根据确认的100帧操作去执行100帧,然后快速执行101~103的帧逻辑,之后继续执行104帧,其中(101~104)还是预测的逻辑帧。

因为客户端对当前操作的立刻执行,这个操作手感,是完全和PVE(不联网状态)是一样的,不存在任何Delay。所以,能做到绝佳的操作手感。当预测不一样的时候,做逻辑回滚,快速追回当前操作。

这样,对于网络好的玩家,和网络不好的玩家,都不会互相影响,不会像Lockstep一样,网络好的玩家,会被网络不好的玩家Lock住。也不会被网络延迟Lock住,客户端可以一直往前预测。

对于网络好的玩家(A),可以动态调整(根据动态的Latency),让客户端领先服务器少一些,尽量减少预测量,就会尽量减少回滚,例如网络好的,可能客户端只领先2~3帧。

对于网络不好的玩家(B),动态调整,领先服务器多一些,根据Latency调整,例如领先5帧。

那么,A可能预测错的情况,只有2~3帧,而网络不好的B,可能预测错误的帧有5帧。通过优化的预测技术,和消息通知的优化,可以进一步减少A和B的预测错误率。对于A而言,战斗是顺畅的,手感很好,少数情况的回滚,优化好了,并不会带来卡顿和延迟感。

重点优化的是B,即网络不好的玩家,他的操作体验。因为客户端不等待服务器确认,就执行操作,所以B的操作手感,和A是一致的,区别只在于,B因为延迟,预测了比较多的帧,可能导致预测错,回滚会多一些。比如按照B的预测,应该在100帧击中A,但是因为预测错误A的操作,回滚重新执行后,B可能在100帧不会击中A。这对于B来说,通过插值和一些平滑方式,B的感受是不会有太大区别的,因为B看自己,操作自己都是及时反馈的,他感觉自己是平滑的。

这种方式,保证了网络不好的B的操作手感,和A一致。回滚导致的一些轻微的抖动,都是B看A的抖动,通过优化(插值,平滑等),进一步减少这些后,B的感受是很好的。我们测试在200~300毫秒随机延迟的情况下,B的操作手感良好。

这里,客户端提前服务器的方式,并且在延迟增大的情况下,客户端将加速,和
守望先锋的处理方式是一样的。当然,他们肯定比我做得好很多:
https://mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg

希望我已经大致讲清楚了这个逻辑,大家参看几篇链接的文章,能体会更深。

这里,我要强调的一点是,我们这里的预测执行,是真实逻辑的预测,和很多介绍帧同步文章提到的预测是不同的。有些文章介绍的预测执行,只是View层面的预测,例如前摇动作和位移,但是逻辑是不会提前执行的,还是要等服务器的返回。这两种预测执行(View的预测执行,和真实逻辑的预测执行)是完全不是一个概念的,这里需要仔细地区分。

这里有很多的可以优化的点,我就不一一介绍了,以后可能零散地再谈。

游戏逻辑的快照(snapshot)

我们的逻辑之所以能回滚,都是基于对每一帧状态可以处理快照,存储下每一帧的状态,并可以回滚到任何一帧的状态。
Understanding Fighting Game Networking文章:
http://mauve.mizuumi.net/2012/07/05/understanding-fighting-game-networking/

守望先锋网络文章:
https://mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg

以上两篇文章都一笔带过了快照的说明。他们说的快照,可能略有不同,但是思路,都是能保存下每一帧的状态。如果去处理快照(Understanding那篇文章做的是模拟器游戏,可以方便地以内存快照的方式来做),是一个难点。
这也是我前面文章,提到ECS在这个方式下的应用:
https://zhuanlan.zhihu.com/p/38280972

云风的解释:

Enter an image description
云风博客截图,地址 https://blog.codingnow.com/2017/06/overwatch_ecs.html

 

ECS是一个好的处理方式,并且我找到一篇文章,也这样做了(我看过他开源的demo,做得还不够好,应该还是demo阶段,不太像是一个成型的项目)。
https://www.kisence.com/2017/11/12/guan-yu-zheng-tong-bu-de-xie-xin-de/

这篇文章的思路是很清晰的,并且也点到了一些实实在在的痛点,解决思路也基本是正确的,可以参看。

这块我做得比较早了,当时守望先锋的文章还没出,我的战斗也没有基于ECS,所以,在处理快照上,只有自己理顺逻辑来做了。

我的思路是,通过一个回滚接口,需要数据回滚的部分,实现接口,各自处理自己的保存快照和回滚。就像我们序列化一个复杂的配置,每个配置各自序列化自己的部分,最终合并成一个序列化好的文件。

首先,定义接口,和快照数据的Reader和Writer

Enter an image description

 

Enter an image description

 

Enter an image description

 

然后,就是每个模块,自己去处理自己的TakeSnapshot和Rollback,例如:

Enter an image description
简单的数值回滚

 

Enter an image description
复制的列表回滚和调用子模块回滚

 

思路理顺以后,就可以很方便地处理了,注意Write和Read的顺序,注意处理好List,就解决了大部分问题。当然,在实现逻辑的过程中,时刻要注意,一个模块如何回滚(例如获取随机数也需要回滚)。

有一个更简单的方式,就是给属性打Attribute,然后写通用的方法。例如,我早期的实现方案:

Enter an image description
给属性打标签

 

根据标签,通用的读写方法,通过反射来读写,就不需要每个模块自己去实现自己的方法了:

Enter an image description
部分代码

 

这种方法,能很好地解决大部分问题,甚至前面提到的Truesync,也是用的这种方式来做。

但是这种方法有个难以回避的问题,就是GC,因为基于反射,当我们调用field的GetValue和SetValue的时候,GC难以避免。并且,因为全自动,不方便处理一些特殊逻辑,调试优化也不方便,最后改成了现有的方式,虽然看起来笨重一些,但是可控性更强,我后续做的很多优化,都方便很多。

关于快照,也有很多可以优化的点,无论是GC内存上的,还是运行效率上的,都需要优化好,否则,可能带来性能问题。这块优化,有空另辟文章再细谈吧。

当我们有了快照,就可以支持回滚,甚至跳转。例如我们要看战斗录像,如果没有快照,我们要跳到1000帧,就需要从第一帧,根据保存的操作指令,一直快速执行到1000帧,而有了快照,可以直接跳到1000帧,不需要执行中间的过程,如果需要在不同的帧之间切换,只需要跳转即可,这将带来巨大的帮助。

自动测试

由于帧同步需要测试一致性的问题,对我们来说,回滚也是需要大量测试的问题。自动测试是必须要做的一步,这块没有什么特别的点,主要就是保存好操作,快照,log,然后对不同客户端的数据做比对,找到不同的地方,查错改正。

我们现在做到,一步操作,自动循环战斗,将每一盘战斗数据上传内网log服务器。

当有很多盘战斗的数据后,通过工具自动解析比对数据,找到不同步的点。也是还可以优化得更好,只是现在感觉已经够用了。经过大量的内部自动测试,目前战斗的一致性,是很好的。

总结

我们现在的帧同步方案,总结下来,就是预测,快照,回滚。当把这些有机地结合起来,优化好,就有了非常不错的帧同步联网效果,无论网络速度如何,只要不是延迟大到变态,都保证了非常好的操作手感。

快照回滚的方式,也不是所有游戏都适用,例如:
Skywind Inside 再谈网游同步技术文章中对此模式(Time warp或Rollback)的缺点,也说明了。
http://www.skywind.me/blog/archives/1343#more-1343

Enter an image description

 

如图所述,这种模式不适合太多人的联网玩法,例如MOBA,可能就不太适用。我们最多三人联机,目前优化测试下来,效果也没有太大问题。但是联机人数越多,预测操作的错误可能性越大,导致的回滚也会越多。

一篇文章,难以讲得面面俱到,很多地方可能描述也不一定明确,并且,个人能力有限,团队人员有限(3个客户端)的情况下,必定有很多设计实现不够好的地方,大家见谅。

一些有帮助的文章再列一下:
1、Understanding Fighting Game Networking
http://mauve.mizuumi.net/2012/07/05/understanding-fighting-game-networking/

2, Skywind Inside "talk online synchronization technology
http://www.skywind.me/blog/archives/1343#more-1343

3, "Watch pioneer" playback technology: killed lens, the overall best and brightest performance
https://mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg

4, "the king of glory" technical director fupan refresher course
http://youxiputao.com/articles/11842

5, the frame synchronization: floating-point precision test
https://zhuanlan.zhihu.com/p/30422277

6、A guide to understanding netcode
https://www.gamereplays.org/overwatch/portals.php?show=page&name=overwatch-a-guide-to-understanding-netcode

7, online games smooth foundation: a frame synchronization game development
http://www.10tiao.com/html/255/201609/2650586281/4.html

Finally, a long-winded, as the beginning, there are many variants of frame synchronization, implementations and optimization direction. Sometimes, it may be a different article mentions the term time frame synchronization, which means, may have differences, we need to carefully sort out and distinguish.

Guess you like

Origin www.cnblogs.com/yptianma/p/11781083.html