在编写网络游戏的过程中,我们经常会遇到同步(Synchronization)的概念。刚刚接触到这个概念时或许很难理解,都是一样的服务器,相同内容的客户端程序,为什么需要进行同步?同步的原理是怎样的,不同步又会产生怎样的后果呢?
同步,简单地说就是设法确保不同客户端上的游戏表现一致。要想了解为什么需要同步,我们首先来探究一下,一个没有同步机制的游戏,会产生什么样的表现。
假设在游戏中,玩家A和玩家B用手枪决斗。他们都只剩最后1点生命值,谁先开枪命中对方,即可击败对方并得分。
最基础的,不带任何同步措施的网络游戏机制,我们可以称之为“纯转发机制”:每个玩家的客户端将玩家操作上传到服务器,服务器收到操作后向所有客户端广播,令它们执行这一操作。由于网络存在延迟,玩家的操作上传到服务器,以及服务器将操作下发到各客户端都需要时间。
在一个较为理想的网络环境下,每个客户端与服务器间的通讯延迟都是较低的固定值(例如20ms),在这种条件下,纯转发机制通常不会产生问题。但实际上,网络环境通常是不那么稳定,带有很大随机性的。客户端与服务器间的通讯延迟会不停地改变,且每一个数据包的传递用时都会有所不同;此外,客户端和服务器在经历瞬时性能压力时可能会出现对数据包处理的滞后,这也会对数据包传递的延迟时间带来不确定性。
假设,在一个不稳定的网络环境下,时间T=0时玩家A和玩家B同时按下自己键盘上的开枪按钮。
在这之后,游戏中共有四份操作数据需要完成传递。
A的开枪信息上传到服务器再下发到A,使得客户端A执行玩家A的开枪事件,用时t1;
B的开枪信息上传到服务器再下发到A,使得客户端A执行玩家B的开枪事件,用时t2;
A的开枪信息上传到服务器再下发到B,使得客户端B执行玩家A的开枪事件,用时t3;
B的开枪信息上传到服务器再下发到B,使得客户端B执行玩家B的开枪事件,用时t4。
由于网络环境和设备性能的不确定性,可以认为t1/t2/t3/t4都是随机值。为了暴露纯转发机制的问题,我们设t1 = 50ms, t2 = 60ms, t3 = 100ms, t4 = 80ms.
那么,游戏接下来的运行状况会是这样。
在玩家A的电脑上,T = 50ms时玩家A开枪,T = 60ms时玩家B开枪,由于A先开枪,判定为玩家A得分;
在玩家B的电脑上,T = 100ms时玩家A开枪,T = 80ms时玩家B开枪,由于B先开枪,判定为玩家B得分。
电光石火,天壤之差。由于传输延迟的存在,两个客户端上出现了致命性的意见分歧,游戏没有办法进行下去了。
到这里我们看到,对于没有同步机制的纯转发机制,各个客户端上的游戏表现,与它们各自收到游戏数据包的时间和先后顺序紧密相关。由于传输延迟的不确定性,各个客户端在游戏逻辑上有可能会“各执一词”。所以,游戏必须找到一个方法,使不同客户端遵循相同的游戏逻辑来运行。
既然有游戏,就可以有裁判。一个非常容易想到的解决思路是,能不能让服务器充当“裁判”,对游戏中的所有逻辑事件进行计算和确认,然后再统一公告给每个客户端呢?当然可以,这就是网络游戏中的状态同步机制。
状态同步的原理,是由服务器运算游戏的逻辑内容,然后将游戏的【当前逻辑状态】周期性地下发给客户端。(当前逻辑状态指的是游戏内的各种事物的状态。例如每名玩家的位置坐标、面朝方向、生命值、魔法值等)客户端上传玩家操作请求,并按照服务器下发过来的游戏整体状态信息来周期性地更新游戏表现。(类似一个视频播放器!)
这种机制可以消除纯转发机制中所出现的客户端时间不一致的问题。因为,游戏事件的先后顺序是由服务器确认的,服务器下发到各个客户端的游戏逻辑信息都唯一确定。
假设还是上面那个“开枪决斗”的情境,在状态同步下,将会是这样的情况;
(1)时间T=0时,玩家A和玩家B同时按下自己键盘上的开枪按钮;
(2)A和B的玩家操作信息,都会在某一时间点被上传到服务器。假设A的网络上行速率更快一些,T=50ms时上传到服务器,B的操作信息则在T=60ms时上传到服务器;
(3)服务器收集到上述操作消息后进行运算。在服务器的眼里,A的开枪操作时间靠前,故运算结果为A先开枪击倒了B。这个结果将被下发到客户端A和B;
(4)客户端A和B按照服务器下发的内容更新游戏状态,它们都会得出玩家A获胜的结果。
到这里,我们能够看出:
在状态同步机制下,游戏逻辑是由服务器运算并确定的,客户端仅在收到服务器消息后对游戏内容进行表现;因此可以防止各客户端游戏逻辑不一致的问题。
网络同步没有完美的解决方案,状态同步当然也远非完美。这个方案存在一个明显的问题。
在状态同步机制下,游戏的逻辑内容由服务器运算并决定。由于逻辑状态是由服务器周期性向客户端发送,这些状态会表现为一个个间断的“状态关键帧”,而不是连续的游戏表现。
例如,当玩家A操控角色作曲线移动时,假设状态同步的服务器消息下发周期为1s,那么:
时间t=0时,玩家A位于出发点;
t=1s时,服务器下发消息,声明玩家A位于点a;
t=2s时,服务器下发消息,声明玩家A位于点b;
t=3s时,服务器下发消息,声明玩家A位于点c;
......
如果客户端仅按照服务器下发的这些内容来运行游戏,那么玩家A的角色无法平滑移动,而是一秒一跳,在出发点-a-b-c之间进行瞬移。这显然是不可接受的游戏体验。
为了解决这个问题,当游戏使用状态同步时,通常需要为客户端加入预测性渲染和平滑渲染的功能。
例如,如果根据服务器消息,玩家的坐标从点a变更为点b,客户端应当具有平滑的渲染表现,使玩家角色在屏幕上流畅地从点a移动到点b;而当屏幕上的玩家到达b点后,如果服务器的下个状态同步数据包还未到达,那么屏幕上的玩家应当“预测性”地按原有移动趋势继续移动。这样,当下一个数据包到达,如果玩家的新位置是c点,依据两个状态关键帧中间的移动预测,从b点到达c点的过程能够显得平滑一些。
不过,状态同步是由服务器决定游戏的关键逻辑,绝对不会由客户端说了算;因此,一旦网络状况不佳,出现丢包或数据到达延迟的情况,那么客户端所作出的“预测”和“平滑处理”就会和实际状况有较大的偏差。这样的偏差一旦出现,必须得到坚决的纠正。
现在研究一个新的情境,如下图。
假设玩家正在控制角色进行一个缓慢右转的移动,轨迹为A-B-C-D-E(黑色线条)。服务器进行运算,并依次下发记录玩家位于点A->B->C->D->E的状态数据包。
由于网络状况不佳,客户端没有能够及时收到包含点C和点D的状态数据包;为了避免游戏卡住,客户端需要按照原有的移动趋势进行预测,将屏幕上的角色依次移动到粉色线条上的【预测版】点C和点D。当服务器下发到最后一个数据包时,网络状况终于改善,客户端收到了记录玩家位于点E的数据包。这时客户端就会知道,前面的移动预测错了。
这个时候,由于客户端绝对听从服务器逻辑,客户端必须对屏幕上的游戏表现进行强制纠正,将玩家从点D?强制移动到远处的点E。在玩家的眼中,自己的角色会在这一刻发生瞬移(即图中的橙色线条)。但是没有办法,这是状态同步必须付出的代价,也是状态同步模式下,网络出现暂时卡顿后的标志性特征。
用更概括的方式来说,状态同步会使客户端随时将游戏更新到服务器所传来的最新逻辑状态;这意味着当游戏从网络卡顿中恢复后,客户端的表现可以进行突变,来保证游戏表现跟上服务器逻辑。大多数MMORPG游戏采用的都是状态同步机制;而在该类型网游中,我们经常会看到人物的瞬移,生命值、魔法值在卡顿后的突然变化,或者在任务执行短暂卡顿后,背包里突然增加了或减少了什么道具。这都是在网络卡顿恢复后,客户端强制适应服务器逻辑所产生的表现。
状态同步在技术上有许多先天的优势。
主要逻辑跑在服务器上,使得游戏不易产生外挂,作弊行为也容易被发现;
每个客户端仅承担上传玩家操作和播放游戏表现的任务,通常不参与游戏逻辑运算。因此个别客户端出现性能问题或者发生掉线时,通常只表现为对应的玩家呆住不动,不会影响其它客户端的游戏体验;
状态同步机制下,服务器不停地下发游戏的全局状态,每一次下发的全局状态都包含了当前游戏的全局完整信息;因此,掉线的客户端容易实现断线重连。它们只需要在重连后接收一个状态关键帧,就可以将游戏表现恢复到最新的逻辑状态。
一般来说,状态同步多用于对严谨性要求不高的MMORPG游戏,或者规则简单而确定的回合制游戏。对于严谨性很高、竞技性很强的RTS/MOBA类游戏来说,使用状态同步也是可行的,但需要克服一些固有问题。
对于偏休闲性质的游戏来说,由服务器下发间隔较长的状态关键帧,然后由客户端进行间隙平滑处理的机制是可以接受的;但是对于竞技性游戏来说是难以接受的。在多人对战游戏中,不同客户端上必须看到严格一致,基本没有偏差的运行全程。客户端的预测和平滑处理显然从逻辑上是不可信的,不同客户端“脑补”的内容也不一定完全一致。如果服务器真的像前面的情境中一样,每1秒钟才下发一次数据,中间的间隙全都由客户端“脑补”,那么游戏运行的严谨性就太差了。实际上,对于使用状态同步的竞技类游戏,状态同步的速率一般会达到每秒30次以上,只有这样才能保证游戏的严谨性,使得客户端需要“脑补”的预测性内容减少到最少。
(每秒30次以上的状态同步,需要良好的网络带宽的支持;上世纪90年代的竞技类游戏——星际争霸、帝国时代等都没有使用状态同步,而是帧同步,就是因为当时的网络条件达不到状态同步的要求。)
此外,状态同步不利于实现游戏的录像和回放。一局游戏由许许多多个“状态关键帧”组成,这些关键帧之间不是动态连续的(或者说连续的逻辑过程只在服务器上被运算过),如果要将这些关键帧进行回放,则还是难以避免“脑补”所产生的潜在误差。此外,存储游戏全局状态需要的数据量是很大的,一个状态同步的录像文件中必然包含许多帧的全局状态,因而录像文件的体积肯定不会小。
帧同步是一种更加古老的经典同步机制,在网络带宽极其有限的“上古时代”,它也几乎是唯一的同步方案。由于历史原因,它几乎成为了一种“为竞技代言”的同步方式;非常多的电子竞技游戏(例如星际争霸1/2代、魔兽争霸3、帝国时代系列、以及最新的王者荣耀)采用的都是帧同步方案。
什么是帧同步呢?回到本文最开始时举的“纯转发机制”的例子。帧同步可以看成是一种严谨的转发机制,基本工作原理如下。
(1)服务器(或者局域网游戏中的主机,下同)会将时间拆分成固定时长、且时长极短的一个个【逻辑帧】来运行。
(2)各个玩家在客户端进行操作后,客户端会将玩家的操作信息上传到服务器。在一个逻辑帧内,服务器收集各客户端上传的所有操作信息,并将这些操作整合为一个逻辑帧数据包。一个逻辑帧数据包记录了在该段时间内,各个玩家所进行的所有操作,这些操作指令会被看成是同一时刻下达的。
(3)服务器将逻辑帧数据包下发到所有客户端。客户端收到序号正确的下一个逻辑帧数据包后,在本地游戏内执行该数据包内记载的各玩家的所有操作,并根据这些操作运算动态的游戏表现。
帧同步的优势显而易见,它将游戏的主要逻辑放在客户端内进行计算,从而极大地减少网络带宽需求。例如,玩家在游戏中圈选100名士兵,命令它们移动到某一地点;假设这个移动过程需要10s,在状态同步机制下,服务器需要计算并下发这100名士兵在这10s内的每一个”状态关键帧“中的坐标信息,从而产生非常可观的高流量;而在帧同步机制下,服务器总共只转发了一次玩家的操作信息(玩家圈选了哪些单位,然后对哪个坐标下达了移动指令),流量消耗几近于零;此后的整个移动过程由客户端自行计算并表现,不需要执行任何网络传输。
帧同步是一种实时演算机制,客户端从收到服务器下发的第一个逻辑帧数据包开始,一直自行演算到游戏结束;因此,使用帧同步的游戏会表现出一些非常鲜明的特征。由于客户端代码(或者说游戏的逻辑演算规则)是确定的,因此帧同步的游戏天生具有录像功能,且不需要专门开发。
帧同步的游戏逻辑,由各个客户端根据服务器统一下发的,记录着玩家操作的数据包进行独立演算。各个客户端之间的游戏运行表现无法相互校验,因而任何偏差都会产生蝴蝶效应,使得不同客户端的游戏表现变得完全不同步。
(例如一个士兵生命值为100点,承受一次攻击后,A客户端认为其承受了100点伤害而死亡,B客户端由于浮点数误差,认为其承受了99.9999点伤害所以未死。这种情况一旦出现,游戏显然没有继续运行下去的必要了。)
为了使不同客户端的运行结果一致,帧同步游戏必须满足一些特定的技术要求,简单地说,就是消除游戏逻辑中可能出现的任何随机性。
·客户端程序中不能使用浮点数,避免出现浮点数运算导致的先天误差;
·一般来说,不能在游戏逻辑中使用物理引擎、导航系统等模块,因为这些模块都使用浮点数,因而存在不确定性;
·游戏中的所有随机过程,必须采用各个客户端都相同的伪随机算法,使得每个客户端开出的随机结果完全一样。
·设法克服不同设备硬件型号、处理器架构等差异因素对游戏运算的影响。
在前面讲解的状态同步中,玩家网络卡顿的问题天生比较容易解决;因为玩家网络不畅仅会导致玩家的操作不能及时上传到服务器,不会影响到服务器端游戏逻辑的计算。玩家网络恢复后,只需要将自己的游戏表现强制校正到服务器下发的最新状态即可。
但是帧同步则有所不同,游戏逻辑由玩家客户端自行计算。如何处理玩家的网络问题呢?
设想,当一个客户端出现短时断线,则会突然与服务器失去连接。当服务器试图收集一个逻辑帧内各客户端的操作时,这个卡顿的客户端不会向服务器提交任何操作。此时,有一个最简单的解决方法——等。
在《星际争霸》中,一旦游戏里有一个玩家断线,服务器(主机)会要求所有玩家的客户端立即暂停,并弹出”等待玩家“倒计时提示框。直到该玩家恢复连接,主机可以正常收集逻辑帧后,游戏才会继续。
这种方案不妨称为“耐心帧同步”——所有玩家都会等待掉线的玩家,直到该玩家连上为止。
《星际争霸》是一款近乎纯竞技的比赛型游戏;一旦参与对战的一名玩家掉线,其它玩家继续游戏是没有什么意义的,因此使用上述“耐心等待”的方案完全没有问题。然而,如果是在娱乐性较强的网游中,一个玩家暂时掉线,其它玩家很可能并不愿意一起进入漫长的等待,而是希望游戏能够正常进行下去。
此时,则需要采取“乐观帧同步”方案。服务器收集每个逻辑帧时,如果一个客户端未能提交操作,则认为该名玩家什么操作都没有做。等到该客户端与服务器恢复联系时,服务器会将将失去联系这段时间内的所有逻辑帧数据包一并发送给它。然后,这个重新连上的客户端则可以利用这些数据包来进行快进演算,以此进行追帧,并最终追上其它玩家的游戏进度。在某些采用此类方案的网游中,我们会在短暂的网络不畅过后发现客户端中的游戏场景“快进”来追赶进度,就是基于这个原理。
·帧同步的游戏逻辑过程是唯一确定且严格的,每个逻辑帧中的操作必然导向唯一确定的游戏运行表现。不同客户端不会产生表现差异,有利于满足竞技游戏的公平性;
·游戏可以方便地进行录像和回放。录像文件只需要保存玩家的操作输入而不需要保存游戏状态,因此体积较小;
·在大多数情况下服务器只对玩家操作进行收集和转发,因此可以实现非常小的服务器压力;
(很多游戏会让服务器也具有跑游戏逻辑的能力,因此这一点不是确定的)
·开发较为简单,游戏逻辑只需要写在客户端。没有预置联网功能的单机游戏可以被方便地改造为帧同步网游,但很难被改造为状态同步网游。