多人游戏的网络实现:帧同步和状态同步

最近在想开发一个多人的 3D 游戏。在多人游戏中,网络同步是一个重要且不可忽视的模块。由于我之前没有做过真正意义的游戏开发,在搞懂游戏的网络编程的路上走了一些奇奇怪怪的弯路,谨以此文记录我的理解与总结,辅助自己消化,也请各位读者斧正。

在多人游戏里,网络同步要实现的目的主要有:

1. 看见:看见视野内其他玩家的位置、面向、动作等
2. 互动:视野内其他玩家进行互动,如攻击、辅助、治疗等
3. 实时:在网络延迟客观存在的前提下,保证各玩家看到的状态尽量一致,对玩家的操作做出尽量快的反应

基于以上目的,我们来讨论多人游戏的网络实现。目前而言,多人游戏的网络同步,主流实现可以分为两大类:帧同步和状态同步。

帧同步,一般而言是 P2P 架构。它的核心思路是:对同样的输入,每个客户端做出同样的操作,即可模拟视野内其他玩家的动作。这样的同步架构,一般而言只需要把少量的玩家输入分发给其他客户端,由其他客户端在本地进行模拟即可。例如一个 RTS 游戏,只需要把玩家的选兵、行动指令分发给其他客户端,由他们进行模拟即可。

状态同步则多采用 C2S 架构。它的核心思路则是:客户端将自己的操作或者局部状态交给服务器,由服务器来运算出游戏内的全局状态,最后由服务器把游戏内的状态分发给其他客户端(可能也包括自己),而客户端只是负责根据这些状态渲染出对应的画面而已。例如一个 MMORPG 游戏,每个玩家将自己所在的位置传递给服务端,服务端收集到各个玩家的位置之后,再分发给其他客户端。

两种网络同步算法各有千秋。总的来说,帧同步算法更适合于做 RTS / FPS / MOBA 类游戏:这类游戏输入简单、对实时性要求非常高,且有“局”和“房间”的概念,一局游戏所能持续的时间有限;而状态同步算法则更适合于 MMORPG 类游戏:这类游戏动作繁多,但对实时性要求可以容忍(甚至可以加入前摇、公 CD 等机制强行增加延迟),且游戏内部状态数据非常庞大、时间轴也会延续很长。

帧同步算法

帧同步算法的实现也有很多不同的样子,这里介绍一种较为简单的方法,即“锁定帧”同步(Lock-step)。

锁定帧同步的核心在于“同步帧”。你需要在游戏中将运算帧分为同步帧和非同步帧。在非同步帧时,你收集本地玩家的所有输入,并将其发送给网络上的其他玩家;同时保存网络上其他玩家发送过来的数据包。到了同步帧,你将网络上的所有操作拿出来,并且在游戏中演算他们。为了保证游戏的实时性和公平性,在同步帧时你可能需要将所有其他玩家的操作数据都收集到,才能进行接下来的游戏过程;但这样可能会导致一个玩家卡顿、所有玩家都无法进行游戏。

关于锁定帧更详细的实现方法,可以参考 Clinton Brennan 的 Lockstep Implementation in Unity3D 一文,也可以参考 bindog 的 游戏中的网络同步机制——Lockstep 一文。

当然,帧同步也有很多不需要锁定帧的方法,这里就不再赘述了。由于传统的帧同步很难解决“开图”之类的反作弊外挂(因为所有玩家的动作都在你本地存着,只是被挡住了而已),所以也有很多和状态同步结合在一起使用的例子。

状态同步算法

状态同步的思路和帧同步相比,客户端的实现要简单很多。你只需要把所有的操作都告诉服务器,让服务器帮你运算,最后将状态同步回本地即可。

说起来非常简单,但事实上很少有游戏是把所有的运算都放到服务器上的。究其根本原因,在于人类的科技实在是太落后了,还无法将网络通信做得比光速更快……扯远了,根本原因在于有网络延迟。如果所有游戏都这么实现,那恐怕是一场灾难:用户往前一步,要等一会儿才会看到自己的角色移动了……

现在很多的状态同步游戏,会在本地做一些运算,并把运算的结果告知服务器。比如移动,可能会定期把当前的坐标信息发给服务器。而服务器收到之后呢,就把这些坐标再告诉其他客户端,其他客户端收到后,再对应的坐标渲染一个人物,这就完事了。

可以看出来,在有网络延迟的情况下,客户端收到的其他玩家的坐标信息必然是有延迟的。在忽略服务器处理的延迟时间的情况下,其延迟时间等于两个玩家到服务器的延迟的总和。在当今的网络条件下,100ms 的延迟都是很常见的,因此玩家看到的其他玩家可能延迟 200ms。如何处理这 200ms ,也许各个游戏都应该有自己的策略:

预测-修正算法

这种方法在云风的BLOG 开发笔记(12) : 位置同步策略 中有提到,在 Gabriel Gambetta 的 Fast-Paced Multiplayer (Part III): Entity Interpolation 一文中也有类似的算法。最初,我也采用这种方法来实现我的游戏。这种方法追求实时性,即,希望玩家看到其他玩家“现在”的位置。

例如,现在的时间 t = 1000;当你收到一个来自 200ms 以前(t=800)、其他玩家的位置数据,你应当根据他的速度,来计算出 400ms 以后(t=1200)它在哪;然后,通过补间动画,将这个玩家平滑的移动到它的位置。这样,如果这个玩家一直在往前移动,当 t=1200 时,你看到的它的位置将是准确无误的。

当然这种方法有很明显的缺点:如果这个玩家在 t=1000 的时候突然掉头往反方向行走,那么你在 t=1200 计算出来的位置是明显错误的;此时,你可能看到这名玩家突然加速向反方向移动。此外,如果玩家在移动过程中进行跳跃,也是很难模拟的。

延迟-回溯算法

在 Gabriel Gambetta 的 Fast-Paced Multiplayer (Part IV): Lag Compensation 一文中提到了一种“延迟补偿”算法,我个人认为更加适用于大多数 MMORPG 的场景。这种算法不再追求实时,而追求准确。换句话说,它不再在乎玩家看到的是“实时”的画面——既然我们做不到让你看到其他人的实时位置,那么让你看到他们在两秒前的位置如何?

通过这种思路,我们可以很轻松的写出网络同步的算法,它对网络的要求也更低——只需要定期采样位置,每隔一小段时间把采样的位置打包回报即可,无需发出大量的网络包来保持同步。同时,客户端也非常好实现,只需确定好延时,然后在过去收到的网络包中,挑出离当前时间减去延迟时间最近的位置,做一个补间动画即可。

当然,它也有它的问题:你看到的是你的“实时”状态,外加其他人的“过去”状态,就像是你超光速旅行一样。这大部分时候没有什么问题,但当你想和其他人交互的时候——比如你瞄准了对方的脑袋开了一枪——此时现在的你对过去的对手开了一枪,可你开枪的时候你的对手已经不在那个位置了。

这个时候,就需要“回溯”了。服务器收到你的“开枪”请求,需要判断你是在哪个时间点开的枪,并且把你开枪的目标的状态回溯到那个时间点,判断你的射击是否命中。如果命中了,再同步到你对手的客户端,告诉他,你中弹了!

显然,你的对手可能对这种情况感到奇怪:我明明已经走远了,为什么他还能打到我?因为同样地,他看到的是你的过去,而你现在已经走远了……嗯,怎么说,只能接受了。

总结

看了很多游戏的同步算法,可以说,在客观延迟存在且无法解决的前提条件下,各种同步算法在尽力保证本地游戏体验的同时,或多或少都牺牲了一些东西。要么牺牲其他玩家的准确性(预测类算法),要么牺牲实时性(延迟类算法),要么牺牲流畅性(锁帧类)……怎么说,鱼与熊掌不可兼得嘛。

对于我而言,我还是期望人类发明什么超光速无延迟通信方案,这样就可以随便怎么做都很开心啦!

评论

JiYouMCC

我想到了我那个抓鬼游戏。。。不存在p2p,全是c2s,还碰上了时间戳不同步。。

三三

感觉抓鬼那个本质上是 p2p,中间服务器转发了一下没有改数据,算是 p2p 的改良,避免直连网络问题。

JiYouMCC

野狗还翘辫子了,幸好他们也不怎么玩了……其实我可以无缝切换到firebase,就是大多数人都没法用。

三三

试试 leancloud 的 play 咯。他们最近搞的,和野狗差不多感觉。

时光

目前大部分竞技游戏应该都是采用的状态预测法,像炉石传说这类就是帧同步。
在实际的体验中可以增加各种东西来削弱奇怪运动的存在感,多增加一些会动的物体就是一个很好的方法。
博主写的很不错,学习了,等啥时候强一点也试着做一个游戏玩(

三三

炉石应该两种兼有。比如指牌、鼠标摸牌这种动作应当是采用帧同步的,而像出牌这类需要服务器参与运算的就更加像状态同步一些。实际游戏体验中确实有很多障眼法可以用。谢谢你的喜欢!

发表评论

发表评论代表你授权本网站存储并在必要情况下使用你输入的邮箱地址、连接本站服务器使用的 IP 地址和用户代理字符串 (User Agent) 用于发送评论回复邮件,以及将上述信息分享给 Libravatar Akismet,用于显示头像和反垃圾。