网络同步在游戏历史中的发展变化(四)—— 状态同步的发展历程与基本原理(下)...
前言:
网络同步属于游戏开发中比较重要且复杂的一部分,但是由于网上的资料内容参差不齐,很多人直接拿别人的结论写文章,导致很多人对这一块的很多概念和理解都是错误的。本文参考了大量的相关论文和资料(花了半年的时间看了不下70篇论文和博客),从网络同步的基本概念讲起,进一步深入到服务器架构与同步算法的实现细节,可以帮你系统的梳理网络同步技术的发展与应用。该系列估计有6篇,本篇核心内容为“状态同步的发展历程与基本原理”,首发在网易雷火官方的知乎账号上。
另外,最近业内的知名的开发者,KCP作者——韦易笑老师对国内“帧同步”和“状态同步”两个概念的发展历史做了说明和解释,描述了为什么国内游戏圈会滥用这两个词以及国内网络同步技术的发展简史。
具体请信息参考:https://zhuanlan.zhihu.com/p/165293116
注:在公众号后台回复“网络同步论文”可获取文中所引用的论文与相关链接
网络同步在游戏历史中的发展变化(一)—— 网络同步与网络架构
网络同步在游戏历史中的发展变化(二)—— Lockstep与帧同步
网络同步在游戏历史中的发展变化(三)—— 状态同步的发展历程与原理
目录(第四篇):
四.State Synchronization 状态同步
6.延迟补偿(Lag Compensation)
7.跟随状态同步(自译)(Trailing state synchronization)
8.状态同步框架的演变
9.守望先锋与ECS架构
10.状态同步小结
四.状态同步State Synchronization
6.延迟补偿(Lag Compensation)
2001年,Valve的新作《半条命》发布,打破了传统FPS游戏玩法。不久之后,其Mod《反恐精英》更是火遍了全球并作为独立游戏发布出去。
由于半条命是基于“QuakeII引擎修改的GoldSrc引擎”开发,所以游戏同样采用了CS架构以及状态同步。不过,为了能达到他们心中理想的效果,半条命在网络同步上做出了不小的改动。首先,半条命也采用了客户端预测逻辑来保证本地玩家能够有流畅的手感,同时为了让客户端提高预测准确率(保证客户端与服务器上的代码逻辑一致),所以半条命里面他们让客户端与服务器执行的是同一套代码。其次,考虑到本地玩家的时间总是领先服务器,玩家开枪的时间到服务器执行时就一定会被延迟,所以为了尽量减小延迟所带来的问题,他们提出了一种名为延迟补偿的技术。
所谓延迟补偿[19],就是弥补客户端到服务器同步延迟的一项技术,该技术的核心是服务器在指定时刻对玩家角色进行位置的回滚与计算处理。假如客户端到服务器的延迟为Xms。当客户端玩家开枪时,这个操作同步会在Xms后到达服务器,服务器这时候计算命中就已经出现了延迟。为了得到更准确的结果,服务器会在定时记录所有玩家的位置,当收到一个客户端开枪事件后,他会立刻把所有玩家回退到Xms前的位置并计算是否命中(注意:计算后服务器立刻还原其位置),从而抵消延迟带来的问题。
红色是当前端的具体位置,黄色是回滚预测的位置
不过,延迟补偿并不是一个万能的优化方式,采用与否应该由游戏的类型与设计决定。考虑一个ACT类型的网游,玩家A延迟比较低、玩家B延迟比较高。在A的客户端上,玩家A在T1时间靠近B,而后立刻执行了一个后滚操作,发送到服务器。在B的客户端上,同样在T1时间发起进攻,然后发送命令到服务器。由于A的延迟低,服务器先收到了A的指令,A开始后滚操作,这时候A已经脱离了B的攻击范围。然后当B的指令到达服务器的时候,如果采用延迟补偿,就需要把A回滚到之前的位置结果就是A收到了B的攻击,这对A来说显然是不公平的。如果该情况发生在FPS里面,就不会有很大的问题,因为A根本不知道B什么时候瞄准的A。
ACT中采用延迟补偿会影响玩家体验
7.Trailing state synchronization
2004年,Eric Cronin等人在传统的Timewrap的回滚方式上提出了Trailing state synchronization算法[20](TSS)。在他们看来,TimeWarp需要频繁的生成游戏快照进而占用大量内存(每次发送命令前都要生成一份),而且每次遇到过期信息就立刻回滚并可能产生大量的对冲事件(anti-message)。这种同步方式是不适合Quake这种类型的FPS游戏的。
在TSS算法中,游戏的快照不是随每个命令产生,而是以某种延迟(比如100ms)间隔为单位对游戏做快照。他事先保存了N个完整的游戏状态(快照)以及命令链表,让这N个状态以不同的延迟去模拟推进。游戏中延迟最低且被采用的状态称为Leading State,其他的称为Trailing State,每个状态都记录着一个命令链表(执行的以及未执行的),各个状态的延迟间隔由开发者设定。
Leading State向前推进的时候会不断的收到其他端的指令并添加到PendingCommands里面,如果某个命令的执行时间小于当前已经推进到的时间(比如图A CommandB指令在时间225ms才被Leading State执行),就会放在表的最前面立刻执行,这时候其实我们已经知道这个命令已经由于延迟错过正常执行时间,可能要进行回滚操作了。但是对于后续的Trailing State,这些过期Commands是可以被放到正确的位置的。当Trailing State执行到某个命令且发现Leading State在对应的位置没有这个命令的话,他就会触发回滚(如果该命令对当前游戏无影响,其实也可以不回滚),将当前Trailing State的状态信息拷贝到Leading State里面,然后设置错误命令时间至当前本地执行时间的所有命令为pending状态,触发这些状态的重新执行。
图A
图B Trailing State S1检测冲突并触发回滚
TSS相比TimeWarp,最大的优势就是大大降低了快照的记录频率(由原来的按事件记录改为按延迟时间分开记录),同时他可以避免由于网络延迟造成的连续多次指令错误而不断回滚的问题(Leading State不负责触发回滚,Trailing State检测并触发)。
不过TSS同时维护了多个游戏世界的快照,也无形中增加了逻辑的复杂度,在最近几年的网络游戏中也并没有看到哪个游戏使用了这种同步算法。在我看来,其实我们不必将整个世界的快照都记录,只要处理好移动的快照同时使用服务器状态同步就可以满足大部分情况了。
8.状态同步框架的演变
在2011年的GDC上,光环(Halo)项目的网络技术负责人David Aldridge就其网络同步框架发表了一次演讲。通过视频[21],可以看到David同样借鉴了TribeEngine的网络架构并在此基础上进行更多细节的调整。
光环项目的网络架构同样被分层,但相比Tribe却更加简洁和精炼。上图的Replication层是Gameplay开发中比较重视的,他决定了我们逻辑上层可用的同步手段。Halo里面有三种基本协议,State Data、Event、ControlData,分别是指“基于对象的属性同步”、“通过调用产生的事件同步”以及“玩家的输入信息同步”,其中移动同步归类于ControlData协议。
2015年,游戏业内著名的商业引擎——Unreal Engine正式开源,其中内置了一套非常完善的网络同步架构[22]。
虚幻引擎的前身是FPS游戏——“虚幻竞技场”。该游戏早在1998年就发布,当时与Quake属于同类型的竞品。虚幻本身也是基于CS架构的状态同步,不过由于无法查找到当时的资料,笔者认为一开始可能也是与Quake非常相似的同步架构。后来在参考Tribe引擎的基础上,进行调整和优化,形成了如今的Netdriver /Connection /Channel /Uobject的模型,以及RPC和属性同步两种同步方式,这已经是网络同步发展至今非常典型且完善的状态同步方案了(后面要提到的OverWatch与其有很多相似之处)。作为一款游戏引擎,虚幻并没有将所有常见的同步手段都集成到引擎里面,只是将移动相关的优化方案(包括预测回滚、插值等)集成到了移动组件里面。其他的诸如延迟补偿,客户端预测等,他们放到了特定的Demo以及插件(GameplayAbility)当中。有兴趣的朋友可以去阅读一些Unreal的源码,看看最近几年其网络架构的发展变化。更多的细节也可以参考我的文章:"使用虚幻引擎4年,我想再谈谈他的网络架构"[23]
9.守望先锋与ECS架构
守望先锋可以说是近年来将网络同步优化到极致的FPS游戏,其中涵盖了我们可以用到的大部分同步优化技术。在2018年的GDC上,来自守望先锋的Gameplay程序TimFord分享了整个游戏的架构以及网络同步的实现方式[24]。
虽然OverWatch基于CS架构,但是却同时用到了帧同步(逻辑帧概念)以及状态同步包含的多种技术手段。为了实现确定性,他们固定了更新周期为16毫秒(电竞比赛时7毫秒),每个周期称为一个“命令帧”(等同于Lockstep中的“Turn”、“Bucket”)。在所有与客户端预表现和玩家行为有关的操作不会放在Update而是放在固定周期的UpdateFixed里更新,方便客户端预测与回滚。不过,整个游戏同步的核心还是状态同步,玩家也并不需要等待其他客户端的行为。
先用一句话来简单概括,守望先锋采用的是基于ECS架构的带有预测回滚的增量状态同步。
我们先从Gameplay层面去分析一下。在守望里面,网络同步要解决的问题被分为三部分,分别是玩家移动,技能行为以及命中检测。
移动模块,客户端本地会不断读取输入并立刻进行角色移动的模拟,他会在客户端记录一个缓冲区来保存历史的运动轨迹(即运动快照),用于后续与服务器纠正数据进行对比以及回滚。
技能行为模块,客户端添加了一个buffer来存储玩家的输入操作(带有命令帧的序号),同时保留历史的技能快照。一旦服务器发现客户端预测执行失败,就会让客户端先通过快照回滚到错误时刻(包括移动和技能),然后把错误时刻到当前时间的所有输入都重执行一遍。
左边是服务器通知客户端被眩晕,右边是客户端收到后进行回滚
命中模块,伤害计算在服务器,但是命中判定是在客户端处理(所以可能存在一些误差)。延迟补偿技术也被采用,但是不是在服务器回滚所有玩家的位置,而是检测当前玩家的准星与附近敌人的逻辑边界(bounding volumes)是否有交集,没有的话不需要回滚。
为了增强玩家的游戏体验,游戏还对不同ping的玩家进行了逻辑的调整。ping值会影响本地的预测行为,一旦PING值超过220毫秒,我们就会延后一些命中效果,也不会再去预测了,直接等服务器回包确认。PING为0的时候,对弹道碰撞做了预测,而击中点和血条没有预测,要等服务器回包才渲染。
当PING达到300毫秒的时候,碰撞都不预测了,因为射击目标正在做快读的外插,他实际上根本没在这里,这里也用到了前面提到的DR(Dead Reckoning)外推算法。
谈完Gameplay,我们可以再考虑一下他的状态同步是如何实现的。同样在2018年的GDC上,来自Overwatch服务器团队的开发工程师Phil Orwig分享了有关回放与同步的相关技术细节[25]。
客户端玩家操作后,这些指令会立刻发给服务器,同时本地开始执行预测。随后,服务器会将这一帧收到的所有玩家的输入进行处理和计算。在服务器上,每个对象产生的变化都会被记做一个Delta,并且会持续累积所有对象状态的变化并保存到一个临时的“每帧脏数据集合”(per frame dirty set)里。同时,服务器会对给每个客户端(每个Connection)也会维护一个对应的“脏数据集合”,这个集合可能保存一些之前没有发送出去的信息(如下图的C1是到客户端1的,C2是到客户端2的)。每帧结束时,所有客户端对应的“脏数据集合”会与当前脏集合F合并,随后当前脏集合F会被清空。
同一个Tick的后期,这些对应不同客户端连接的脏集合(C1、C2等)会被序列化并发送给对应的客户端,同时从脏集合中移除。这里的序列化并不是完全使用原生的状态数据,而是维护了一个经客户端确认收到的状态数据的历史记录(比如我们服务器上已经记录了玩家的大部分信息,每次位置变化只序列化位置信息就可以了),这样我们就可以使用“增量编码”来改善带宽模型,即减少带宽的占用。
通过前面的分析,我们可以了解到整个网络同步的逻辑是很复杂的,细节也非常多。所以,我们也需要考虑是否能从底层和框架上做一些调整和优化。在守望先锋里面,他们并没有采用常见的面向对象模型(OOP),而是使用了数据与操作行为分离的ECS架构[26]。Entity代表一个空的实体、Component代表一个只包含数据的组件、System代表一个处理数据的系统。在这个架构下,我们将面向对象编程转为面向数据编程,游戏的不同模块可以划分成不同的系统,每个模块只关心自己需要的数据(Component),这种模式下可以方便我们处理快照与回滚的逻辑。ECS系统看起来有着缓存友好、逻辑解耦等优点,但是操作起来问题也不少,其中最难处理的一个问题就是如何控制System 运作的次序。
最后,简单说一下底层的一些优化。为了提高通信效率,守望也采用定制的可靠UDP,因此会有不可避免的丢包情况。为了对抗丢包,每一帧的数据包包含的是最近N帧的数据,即使某一个数据包丢了也没什么影响。除此之外,他们还在服务器添加了一个缓冲区,记录玩家的输入信息。缓冲区越大,就能容忍越多的丢包,但是也意味着同步延迟越大。所以,在网络条件良好的情况下,他们会尽力减小这个缓冲区的大小,而一旦客户端丢包,那么就可以提高客户端发送数据频率,进而服务器收到更多的包,缓存更多的数据用于抵消丢包。
10.状态同步小结
状态同步大概在上世纪末就已经诞生(相比帧同步要晚一些),然而至今却没有一个完整的定义。不过单从名字上看,我们也能猜到“状态同步”同步的是对象的状态信息,如角色的位置、生命值等。
在Quake诞生前,其实也存在直接传输游戏对象状态的游戏,但是那时候游戏都比较简单,相关的概念也并不清晰。当时的架构模型以P2P为主,考虑搭配带宽限制等原因,军事模拟、FPS等游戏都采用了“Lockstep”的方式进行同步。
不过由于作弊问题日益严重、确定性实现困难重重等因素,CS架构逐渐代替P2P走向主流。我们也发现似乎所有的游戏状态信息都可以保存在服务器上,客户端只需要接受服务器同步过来的状态并渲染就可以了。按照这种思路,Quake诞生了,他抛弃了Doom的架构并带着状态同步的方式进入我们的视野。这时候的状态同步还只是简单的快照同步,每次同步前服务器都需要把整个游戏世界的状态信息打包发送给客户端。
然而,快照同步太浪费带宽了,不同的玩家在一段时间内只能在很小的范围内活动,根本没有必要知道整个世界的状态。同时,每次发送的快照都与之前的快照有相当多重复的内容,确实过于奢侈。因此,星际围城:部落的开发团队构建出了一个比较完善的状态同步系统,用于对同步信息进行分类和过滤。
后来,光环、虚幻竞技场、守望先锋、Doom等游戏都在Tribe Engine的基础上不断完善状态同步,形成了如今的架构模型。
如今距离状态同步的诞生已经20余年,当我们现在再讨论状态同步时,到底是指什么呢?
我认为,如今的状态同步是指包含增量状态同步、RPC(事件同步)两种同步手段,并且可以在各个端传递任何游戏信息(包括输入)的一种同步方式。
目前的状态同步多用于CS架构,客户端通过RPC向服务器发送指令信息,服务器通过属性同步(增量状态同步)向客户端发送各个对象的状态信息。我们可以采用预测回滚、延迟补偿、插值等优化方式,甚至也可以采用“命令帧”的方式对同步做限制。不过在这个过程中,传递的内容以状态信息(即计算后的结果)为主,收到信息的另一端只需要和解同步过来的状态即可,不需要在本地通过处理其他端的Input信息来进行持续的模拟。
最后,再次拿出虚幻引擎的网络同步模型来展示当今的状态同步。
到此帧同步和状态同步的发展历史讲述基本完结,下篇会继续和大家谈谈物理同步、常见同步优化技术等内容。
— 未完待续 —
游戏开发那些事
回复"gamebook",获取游戏开发书籍
回复"C++面试",获取C++/游戏面试经验
回复"操作系统",获取操作系统经典书籍
游戏开发交流群(875867499)
往期热门文章:
参考文献:
[19]Yahn W. Bernier,"Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization" 2001.Available: https://developer.valvesoftware.com/wiki/Latency_Compensating_Methods_in_Client/Server_In-game_Protocol_Design_and_Optimization[Accessed:2020-07-17]
[20]Eric Cronin, Burton Filstrup Anthony R. Kurc, Sugih Jamin,"An Efficient Synchronization Mechanism for Mirrored Game Architectures", 2004. Available: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.87.6043&rep=rep1&type=pdf[Accessed:2020-07-17]
[21]David Aldridge, "I Shot You First: Networking the Gameplay of HALO: REACH", GDC, 2011. Available: https://www.bilibili.com/video/BV1Vt4y127op[Accessed:2020-07-17]
[22]Epic Games, " UnrealEngine: Networking and Multiplayer". Available: https://docs.unrealengine.com/en-US/Gameplay/Networking/Overview/index.html[Accessed:2020-07-17]
[23]Jerish, "使用虚幻引擎4年,我想再谈谈他的网络架构".Available: https://zhuanlan.zhihu.com/p/105040792[Accessed:2020-07-17]
[24]Timothy Ford," 'Overwatch' Gameplay Architecture and Netcode", GDC, 2018. Available: https://www.bilibili.com/video/av44410490[Accessed:2020-07-17](翻译链接:https://gameinstitute.qq.com/community/detail/114516)
[25]Philip Orwig," Replay Technology in 'Overwatch': Kill Cam, Gameplay, and Highlights", GDC, 2018. Available: https://www.bilibili.com/video/BV1aA41147bY[Accessed:2020-07-17](翻译链接:https://gameinstitute.qq.com/community/detail/115186)
[26]WIKI,“Entity component system”.Available: https://en.wikipedia.org/wiki/Entity_component_system[Accessed:2020-07-17]
— 觉得不错请帮忙转发、点个在看 —
本文地址:https://blog.csdn.net/u012999985/article/details/107873400
上一篇: Linux下用uniq命令后丢失某些行
下一篇: tr命令在统计英文单词出现频率中的妙用