在所有的游戏类型中,联机游戏一定是非常重要的一个类型,即使是很多主机游戏,也会增加联机玩法。而说到联机,就一定离不开网络同步技术。今天,我们就聊聊游戏中的同步技术。希望以后我们不仅能玩游戏,也能知道我们为什么被游戏玩,哦不对,是游戏为什么这么好玩。

一、网络同步技术

游戏机/街机时代

很明显,小霸王游戏机时代,是没有网络同步的,它也有同步,但是只需要处理好两个手柄输入的同步就够了。只要我们在同一个机器游玩,我们的手柄输入一定会传入到同一个游戏机,同一个CPU,同一个处理逻辑,等游戏逻辑计算好了之后,再把结果输出到那个带后脑勺的凸面显示器即可(不会有人没见过那个时代的显示器吧?)。所有的一切都在同一个处理器上执行完成。

小霸王游戏机

同理也有当时流行游戏厅里街机,对,就是那个每次经过,里面都是一群抽烟喝酒烫头的社会青年(于谦老师:???)在里面噼里啪啦一顿操作的游戏厅。这种方式最简单直接,就是通过物理连线把两个玩家接入到同一个处理器,这种不会存在任何同步问题(因为它就没有同步)。当然了,现在的PS,XBOX,Switch等主机玩家,仍然可以通过这种方式和朋友面对面的一起分享游戏的快乐。不过这一部分不在今天想要聊的话题里。(如果感兴趣,可以单独写一篇聊聊来主机游戏)

在个人电脑逐渐进入百姓平民家中,并且互联网也开始普及的时候,游戏开发者就开始考虑如何让两个主机玩同一场游戏了。

或许可以说,从这里开始,才是本篇文章最想聊的部分——游戏的网络同步


要聊到网络同步,不得不先聊聊两种同步模式:

  • P2P
    即Peer To Peer,端对端的同步模式,也就是游戏客户端和客户端之间来进行网络同步。这种同步模式是不需要服务器来作为同步中心的,或者说,客户端之间选择某一端来作为服务器,其他端继续作为客户端来进行同步。这种方式的好处就是不需要服务器的维护,也省去了服务器的成本;坏处也很明显,就是当作为“同步服务器“的客户端如果掉线了,其他客户端也只能被迫中止这场游戏。

  • C/S
    即Client/Server,也就是以服务器为同步中心,多个客户端来进行网络同步。这种同步模式相对于P2P模型要安全很多,一方面,有服务器的存在,就意味着我们一定程度上是可以掌控游戏的安全性的,一些作弊手段就可以很容易被服务器捕获到(为什么要说“一些”呢,我们后面再讲);另一方面,对于P2P中的“服务器”掉线的问题,C/S模式不仅可以使用性能更好的机器来作为服务器,同时我们也可以通过负载均衡,容灾备份,集群部署等方式来避免因为某一台机器的宕机而是整个游戏服务不可用。

1. 帧同步

早期的游戏,大部分游戏都会采用P2P模式来进行同步,即其中一个客户端作为主机,开一个房间,其他客户端在同一个网络内进入房间进行游戏。大家可能会好奇,上面说了C/S模式的那么多好处,为什么还要用P2P呢?其实很大一部分原因,是因为当时的硬件限制,而这种无服务器的架构在当时也是比较主流的联机游戏的做法。
大家应该很容易就能想到,帝国时代,星际争霸,CS等游戏,基本都是基于这种模式。这种模式下,最先使用的就是帧同步方案

这里终于引出了我们今天要聊的第一个游戏同步方案——帧同步

帧率

要讲帧同步,我们先聊聊什么是“帧”?
大家应该都知道动画原理吧:每秒连续播放24张静态图片,就能形成一个视觉上连贯的动画。
游戏也是类似的,不同的是,每一张图片是什么内容,需要通过我们游戏的逻辑的运算来决定:

假如我们的游戏以30的帧率运行,即每秒30帧,那么每一帧,就需要播放33ms(1s/30),也就是说,只要我们的计算机在33ms内计算出下一帧要播放的画面,我们就完全可以通过代码来实现播放一段流畅的画面。而想在33ms内运算下一帧的画面,对于计算机来说简单太容易了。
游戏原理

注意看左边虚线的部分,那其实就是游戏运行原理的核心,我们通过各种输入设备,把玩家的操作指令输入到逻辑单元,逻辑单元通过玩家的指令来运算出游戏的各种状态,再通过渲染单元将画面输出到电视机或者显示器上。这样,我们就从感官上感受到了通过我们自己意识来决定了游戏输出的画面内容,也就真正和游戏产生了互动,这就是游戏最直接,最基础的原理。

同步原理

既然游戏逻辑运算的都是帧,那么我们就可以把每一帧的计算,都同步到所有客户端,客户端都能根据这一帧的计算,来播放相应的游戏画面,这样就能完成游戏的同步。
帧同步原理

在P2P模式中,任何客户端,都可以承担起这个“服务器”这个角色

帧同步的原理其实就这么简单,一句话概括就是,客户端通过网络转发自己的帧操作到所有的客户端,客户端根据收到的广播的帧序列进行逻辑运算,最后渲染游戏画面。

我们拿释放技能举例
释放技能
可以看到在这个时间序列中,服务器所承担的,仅仅是一个转发中心的功能,它只负责收集指令和广播指令,而真正的游戏逻辑运算,都在各个游戏客户端中。

逻辑核表现分离

上面图中把客户端分成了逻辑层和表现层,这也是帧同步中很重要的一个概念,即逻辑和表现的分离。

为什么要做分离呢,其实很大的原因是各个设备之间性能参差不齐,表现是否一致远没有逻辑保持一致重要,逻辑保持一致才能确保计算准确。性能好的设备,我们可以让它跑到30帧,60帧,但是我们的逻辑运算,只需要15帧就够。

我们可以举一个简单的例子,比如我们移动,当玩家发送一个向前移动的指令后,逻辑层只需要运算好玩家每一帧的位置即可,而不同设备,可能表现层的画面就不太一样了,设备性能比较好的,它的帧率相对较高,看到的可能就是一个完整平滑的移动过程,理论上来讲,帧率越高,你看到的移动过程就越顺滑;相反,机能不太好的设备,帧率相对较低,可能看到的是一卡一卡的画面,甚至于玩家能看到自己几乎就是瞬移过去的,不过就算设备再卡,玩家的最终位置也一定是它要移动的目标点。
移动

逻辑层和表现层的分离,其实也是编程思想中很重要的一个概念——解耦。这样一来,逻辑只做逻辑的事,表现只做表现的事。在帧同步的客户端开发中,必须非常注意把逻辑层和表现层进行分离,逻辑层就是去壳后的纯粹的逻辑运算,甚至是把逻辑层拿到一个单独的环境,它也是能跑起来的。

网络通信

从上面的帧同步的原理图中,可以看到游戏服务器承担着一个非常重要的角色——帧序列转发中心。这就对服务器的网络通信效率要求非常高。
如果逻辑帧按照每秒15帧来计算,每秒钟,一个人要转发15个消息包,如果按照王者荣耀这类的MOBA游戏来说,一场战斗10个人,就要转发150个包,如果一个战斗服务器有100场战斗,就要转发15000个包。这还只是一秒的消息转发量,可想而知,这对于服务器的网络通信的压力有多大。

在网络通信中,我们通常使用的就是TCP/IP协议,即使是我们平时访问网页所用到的HTTP协议,它的传输层协议也是TCP/IP。TCP协议以网络拥塞控制为主要目的,因为它的握手,全双工通信,重传,有序包序列等机制,它最大的特点就是安全;相反这样的特点所带来的负面影响就是传输效率和网络延迟,这些问题在内网环境下并不明显,但是一旦到了外网,丢包率会直线上升,而TCP的这些优势,反而便成了劣势。

UDP协议则和TCP相反,它无需握手,没有重传,包乱序,所以也非常不安全;但是也是因为这样,它的传输效率很高,并且有被利用起来的无限可能。汲取TCP的经验教训,基于UDP协议之上,出现了很多既克服了UDP缺点,又实现了TCP的安全可靠的特点的协议。比如KCP、QUIC、UTD、ENET等。
HTTP1和HTTP2都是基于TCP协议,而HTTP3已经抛弃了TCP协议,使用了基于UDP的QUIC协议

这里不想像教科书似的列举TCP和UDP的区别,以及各自的优势劣势,这些内容在网上也有大把的文章(如果大家感兴趣,我也可以单独写一篇此类文章详细了解网络通信协议)。这里只想说明一个问题,在帧同步的同步模型中,TCP的传输效率是远远满足不了服务器每秒的转发频率的。而这个时候,UDP就显得非常合适了,我们可以自己实现基于UDP的安全的通信协议,也可以使用上面提到的几种协议,总而言之,我们的最终目的,都是需要提高服务器的消息转发频率。

不难看出,在帧同步的同步方式下,服务器仅仅做指令收集和消息转发,并不会涉及到任何的逻辑运算,真正的游戏逻辑依然运行在游戏客户端。由于服务器带宽始终是有上限的,所以这样的高频消息转发的同步方式只能适合玩家数量比较少的游戏类型。

2. 状态同步

帧同步是通过服务器转发客户端的操作帧来进行网络同步,所以整个运算逻辑都是在客户端进行的。而状态同步则是走完全相反的路子,它把运算逻辑放在了服务器。

状态

要了解状态同步,首先我们需要知道什么是状态:玩家,怪物,NPC的属性,位置,动作等都可以叫状态。一切通过输入指令产生的结果,通过AI运算的结果,通过单位与单位之间互相影响的结果,都可以叫做状态。

同步原理

所谓的状态同步,其实就是把这些状态同步到各个客户端。游戏服务器作为权威服务器,负责全部的游戏逻辑运算,并下发运算结果,而客户端则更像是一个播放器,只需要播放服务器通知你的当前状态即可。
状态同步
在状态同步的过程中,客户端依然需要把逻辑和表现进行分离,而服务器这边在网络层需要收集操作指令之外,还需要一个逻辑运算的线程进行逻辑处理。大家可以看到客户端的逻辑层这里有三个虚线的方块,其实最基本的状态同步是可以没有这三个步骤的,所谓的预演算、预表现和本地修正其实是对网络响应延迟的一个平滑优化处理(后面详细讲)。

我们依然以释放技能举例
释放技能

在这个序列图中,我依然是考虑到了客户端的预计算和预表现,以及本地数据的修正。可以看到的是,这张图比帧同步的时间序列要复杂很多,而实际情况下,状态同步的实现也确实比帧同步更加复杂。

客户端预表现

由于状态同步是不需要等待服务器的帧数据的返回的,所以理论上,状态同步比帧同步能更好的做操作的预表现优化,这样给玩家的操作体验也会更好。客户端在发出操作指令之后,就应该开始做预计算和预表现,而这个操作在服务器的逻辑中真正产生的表现效果是不是和我们预表现一致呢?如果客户端收到同步消息之后,发现是一致的,那么预表现就非常成功,玩家也会感受到无比顺滑,但如果预表现并不成功,有一点偏差,甚至与真实结果完全相反,那么客户端也必须对状态做好修正。

二、孰强孰弱

大致说完了帧同步和状态同步的概念,那么这两模式各有什么优势,又各有什么劣势呢?
我根据常见的几点不同列了表格

帧同步 状态同步
一致性 同步原理上天然支持,也要求必须的一致性 在状态上能保持一致性,但实际表现中则可能出现拉扯现象(本地修正回滚)
响应 帧操作指令需要等待服务广播下发才能响应,预表现实现比较受限,在网络状况不好时响应比较受限 状态同步可以更好的进行预表现,响应较好
带宽 因为只需要同步帧序列(数据包较小),因此人数较少时,使用带宽较低,但随着人数增长带宽会几何程度增加。因此帧同步也不太可能承受MMO那种类型的需要上千上万人同场战斗的游戏 虽然数据包较大,但同步频率不如帧同步高,并且数据包和发送频率还可以一定程度进行优化,所以状态同步也是MMO类型游戏的首要选择
延迟体验感 因需要保持逻辑帧率的一致性,在较差的网络环境下,延迟较高,体验较差 可以通过延迟补偿、预测演算等方式优化高延迟下的体验感
开发效率 开发效率高,只需要开发一次客户端逻辑即可,但很难排查引起同步问题的相关bug 客户端和服务端要分别写一遍游戏逻辑,也可使用相同代码,但因为两者机制上有着差异,所以必不可少的会额外增加一些开发成本
玩家数量 少量。客户端的性能和服务器的带宽都不一定能支撑更多的玩家 没有限制,玩家数量多可以使用AOI算法、分场景、分区域、负载均衡等支撑更多玩家
跨平台 因不同设备的硬件不同、编译器不同等原因,可能导致运算结果的不一致,一般浮点数都会改为通过定点数计算,并且允许损失一部分精度 因为游戏逻辑在服务器端运算,客户端只是做表现,因此跨平台就十分容易
反外挂 P2P模式基本很难反作弊,C/S模式下如果在服务端再单独跑一份战斗逻辑则可以通过服务器的运算结果来进行作弊验证(可以实时反作弊,也可以事后校验反作弊) 状态同步中,越多的逻辑跑在服务器,就越容易反作弊(游戏逻辑都在服务器,你在本地的任何修改都只能自己骗自己)
断线重连 由于帧同步需要从断线前到重连上的每一帧的数据,因此断线重连必须要求服务器下发这期间丢失的所有帧数据 客户端只是状态表现,因此断线重连也非常容易,主要同步一下最新的状态即可
战斗回放 只要保留了所有的帧数据,想让客户端重新演算一遍所有的帧十分容易 如果非要做的话,也不是不行,但是实现难度极大
服务器承载 服务器CPU开销极低,主要压力在网络通信层 CPU压力大,但能通过很多常规的服务器优化手段进行优化以达到更高的承载

实际上,在现在很多游戏中,往往会结合帧同步和状态同步两种方式,取长补短,来进行网络同步。

到这里,我应该把帧同步和状态同步的概念说清楚了,但实际上这里面还有很多技术难点和需要攻克的问题,这里就不再展开了,这篇文章的目的就是能让你们了解到游戏的网络同步技术的概念就可以了,如果感兴趣的话,我可以挑其中的重点单独聊聊。

我们再回头具体看一下,有哪些游戏是帧同步,又有哪些游戏是状态同步呢?

  • 帧同步:DOOM/魔兽争霸/DOTA/帝国时代/星际争霸/街霸/王者荣耀
  • 状态同步:DOTA2/魔兽世界/怪物猎人:世界/CSGO/LOL/全民超神/大部分MMORPG
  • 其他:守望先锋(混合方案:状态帧同步)

三、参考资料

写文章的过程,也是自我提升的过程,这期间也看了不少大佬的文章,以便于让我更深的理解其中的概念,这里感谢大佬们分享

Q.E.D.