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

最近在想开发一个多人的 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 的场景。这种算法不再追求实时,而追求准确。换句话说,它不再在乎玩家看到的是“实时”的画面——既然我们做不到让你看到其他人的实时位置,那么让你看到他们在两秒前的位置如何?

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

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

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

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

总结

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

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

9 Replies to “多人游戏的网络实现:帧同步和状态同步”

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

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

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code