网络同步 = 数据同步+表现同步
,数据同步是后端操作,而表现同步就是让前端对后端同步过来的数据进行进一步的处理从而达到表现上的一致。
不过一般Web服务器只是单纯的从服务器向客户端进行数据同步,不会把其他客户端的数据都发给你。而在游戏里面,你需要让N个客户端的显示看起来一模一样(由于网络延迟,同一时刻不可能完全一样),所以需要把其他玩家的一些数据也发给你,不能说A玩家跳了一下,B玩家看到A却趴下了,那样游戏就没法玩了。
高及时性
:
然而,前面我们还忽略了一个游戏中非常重要的需求(尤其是在MMO、FPS这种类型的网游中)——那就是实时性。你可以容忍微信点进去一个文章要花2秒钟,但是你不可能接受你的子弹要2秒后才打到敌人。实际上,在各种电子竞技里面,0.1秒的延迟就足以让整个游戏的局势发生逆转。像浏览器这种页面显示都吞吞吐吐的应用,如何用他流畅的玩FPS和MOBA呢?(关于云游戏这里先不谈)。可以认为 网络同步 = 实时的多端数据同步+实时的多端表现同步
。
Packet Server
以某个客户端为Host主机(或叫做ListenServer)的CS架构),这样的架构不需要单独都维护一个服务器,任何一个客户端都可以是Sever,能够比较方便的支持局域网内对战,也能节省服务器的运行与开发成本。不过,虽说也是CS架构,如果Host主机不做任何server端的校验逻辑,那么其本质上还是P2P模型,只不过所有的客户端可以把消息统一发送到一个IP,Host再进行转发,这种方式我们称其为Packet Server。
避免单点崩溃影响到所有玩家的问题
。类似CDN,玩家还可以选择就近的服务器进行通信,降低了通信延迟。不过,这种方式增加了服务器的租用和维护成本,在后续的游戏网络架构中并没有被大量使用,倒是WEB服务器广泛采用这种模型并不断将其发扬光大。游戏存储负载和网络连接负载随后从逻辑服上拆分出来,形成独立的服务
;玩家数量增多后,又将游戏拆分成多个平行世界,出现了分服和跨服;游戏逻辑进一步复杂后,又开始按照功能去划分成网关服务器、场景服务器、非场景服务器等。我们今天讨论的网络同步几乎都是在逻辑服务器(基本上无法拆分)上进行的,所以后续的这些架构方式与网络同步的关系并不是很大,这里就不再赘述停等协议
不过随着游戏的种类和玩法复杂性的提升,面对的问题也接踵而来。
1)在CS架构下逻辑在客户端执行还是在服务器执行?如果逻辑都在服务器执行,那么客户端的操作都会被发送到服务器运算,服务器计算出结果后通知客户端,客户端拿到结果后再做表现,这样的好处是所有的逻辑由服务器处理和验证,客户端无法作弊,但坏处是会造成客户端的资源被浪费,服务器运算压力过大。如果逻辑在各个客户端执行,那么玩家可以在本地计算后再把本地得到的结果告知服务器,服务器只进行简单的转发,这样的好处是玩家的本地表现很流畅,但坏处是很容易在本地进行作弊。而对于P2P架构,反作弊更是一个严重的问题,我连一个权威服务器都没有,根本无法验证其他客户端消息的真伪,怎么知道其他玩家有没有作弊?
2)我们要发送什么数据来进行同步?如果发送每个对象当前的状态,那么如果一个游戏里面有大量的角色,就会大规模的占用网络带宽,造成数据拥塞、丢包等等问题。如果发送玩家指令,那这个指令是要服务器执行还是服务器转发?而且对于大型多人在线游戏又没必要处理所有不相关的玩家信息,同样浪费网络资源
3)面对日益成熟的计算机网络协议,我们选择哪种来进行同步?TCP、UDP还是Http?
(这时,游戏开发者们需要面对“发什么数据”,“在哪计算”,“发给谁”等细节问题,他们开始考虑引入更多的其他相关领域的技术(比如计算机模拟仿真)来解决游戏中的同步问题,网络同步概念初见端倪)
未完待续
[1]WIKI “History of video games”. Available:https://en.wikipedia.org/wiki/History_of_video_games[Accessed: 2020-03-24]
[2]T.A. Funkhouser.”RING: A Client-Server System for Multi-User Virtual Environments“, In Proc. 1995 Available:https://dl.acm.org/doi/pdf/10.1145/199404.199418[Accessed: 2020-03-24]
[3]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-03-24]
保证每帧(逻辑帧)输入一致
1)帧锁定
2)乐观帧锁定
3)lockstep
4)bucket同步等等
优化手段
1)要不要回滚?
2)服务器要不要跑一段完整的逻辑?
3)操作要不要是键盘鼠标还是高阶指令?
4)客户端要不要像视频播放器一样保证平滑缓存1-2帧
5)要不要保证平滑加一层显示对象的坐标插值?等等
用于计算机容错系统,即“使用相同的、冗余的硬件组件在同一时间内处理相同的指令
,保持多个CPU、内存精确的同步
,所以一开始与游戏并没有任何关系
游戏介绍传送门
不过,早在1994年,FPS鼻祖Doom就已经采用了类似Lockstep的方式进行网络同步[5]。Doom采用P2P架构,每个客户端本地运行着一个独立的系统,该系统每0.02秒钟对玩家的动作 (鼠标操作和键盘操作,包括前后移动、使用道具、开火等) 采样一次得到一个 tick command 并发送给其他所有玩家,每个玩家都缓存来自其他所有玩家的 tick commands,当某个玩家收到所有其他玩家的 tick commands 后,他的本地游戏状态会推进到下一帧。在这里, tick command的采集与游戏的推进是相互独立的。
其实当时并没有Lockstep这个说法,doom的整篇论文里面也没有出现过这个词。不过后面概念逐渐清晰后我们会发现,Doom采用的同步方式就是我们常说的原始版本的Lockstep——“确定性锁步同步(Deterministic Lockstep)”
。
MiMaze介绍传送门
1999年,Christophe Diot和Laurent Gautier的团队开发了一款基于互联网的页游——MiMaze,基于传统的Time Bucket Synchronization[6]他们在发布的论文里面提出了改进后的Bucket Synchronization同步方法[7]。Bucket Synchronization把时间按固定时长划分为多个Bucket,所有的指令都在Bucket里面执行。考虑到网络延迟的情况,每个玩家在本地的命令不会立刻执行而是会推迟一个时延(该时延的长度约等于网络延迟),用来等待其他玩家的Bucket的到来。如果超过延迟没有到达,既可以选择放弃处理,也可以保存起来用于外插值(Extrapolation)或者使用前面的指令重新播放。在这种方式下,每个玩家不需要按照Lockstep的方式严格等待其他玩家的命令在处理,可以根据网络情况顺延到后面的bucket再执行。Bucket Synchronization可以认为是我们常说的“乐观帧锁定”
1)优点:简单
2)缺点:浮点数跨平台的同步问题、玩家数量增长带来的带宽问题以及显而易见的作弊问题(在P2P架构下几乎没有任何反作弊能力)
1)lookahead cheats定义:比如客户端A使用了外挂工具,每次都将自己的操作信息推迟发送,等到看到了别人的决策后再决定执行什么(或者假装网络信号不好丢弃第K步的操作,第K+1步再发送)
2)来源:在2001年,Nathaniel Baughman和Brian Neil Levine在IEEE上发表了论文,提出锁步同步协议 Lockstep protocol [8]来对抗lookahead cheat类型的外挂
1)简介:
这里的Lockstep protocol并不是我们前面提到的Deterministic Lockstep ,相比之前的在第K步(第K个Tick Command间隔)就直接发送第K+1步的明文操作信息,Lockstep protocol每一步都分两次发送信息。
2)大概的流程如下:
①先针对要发送的明文信息进行加密,生成“预提交单向哈希(secure one-way commitment hash)”并发送给其他客户端。
②待本地客户端接收到所有其他客户端的第K步预提交哈希值之后,再发送自己第K+1步的明文信息
③等到收到所有其他客户端的第K步明文信息后,本地客户端会为所有明文信息逐个生成明文哈希并和预提交的哈希值对比,如果发现XXX客户端的明文哈希值和预提交哈希值不相等,则可以判定该客户端是外挂。反之,游戏正常向前推进。
缺点:虽然可以对抗外挂,但是很明显带来了带宽以及性能的浪费,而且网络条件好的客户端会时刻受到网络差的客户端的影响。
性能优良但是延迟高
就会拉长每个turn的时间(多出的时间用于正常进行多个帧的渲染以及Gameplay的处理,虽然可能有误差),性能差但是网络正常
就会把大部分的时间用于每个turn的渲染,在这种条件下每个客户端相同的turn执行的本地时间虽然不同,但是执行的内容是完全一致的。同样需要提前发送hash
,这种操作同步、不等待超时玩家的确定性锁步的特性逐渐成为“Lockstep”的标准,被广泛应用于网络同步中。来源
TimeWarp原本是指科幻小说中的时间扭曲,其实早在1982年就被D Jefferson等人引入计算机仿真领域[11],后续又被Jeff S. Steinrnan进行优化和调整[6][12]。
概念
TimeWarp算法基本思路是多个物体同时进行模拟,当一个物体收到了一个过去某个时刻应该执行的事件时,他应该回滚到那个时刻的状态,并且回滚前面所有的行为与状态。
TimeWarp对于PipleLined的优化
前面提到的Pipelined Lockstep protocol可以流畅的处理玩家互相不影响的情况,但是却没有很好的解决状态冲突与突发的高延迟问题。参考TimeWrap这种思路,我们可以将本地执行过的所有操作指令进行保存行成一个快照(Snapshot),本地按照Pipelined Lockstep protocol的规则进行推进,如果后期收到了产生冲突的指令,我们可以回滚到冲突指令的上一个状态,然后把冲突后续执行过的事件全部取消并重新将执行正确的指令。这样如果所有玩家之间没有指令冲突,他们就可以持续且互不影响的向前推进,如果发生冲突则可以按照回退到发生冲突前的状态并重新模拟,保持各个端的状态一致。
关于帧
前面提到了那么多lockstep的算法,但好像没有一个算法使用到“帧”这个概念。其实“帧同步”属于一个翻译上的失误,宽泛一点来讲“帧同步”是指包含各种变形算法的Lockstep,严格来讲就是指最基本的Deterministic Lockstep。我猜测国内在引入这个概念的时候已经是2000年以后(具体时间没有考证),lockstep算法已经有很多变形,时间帧的概念也早已诞生,所以相关译者可能就把“lockstep”翻译成了“帧同步”。当然也可能是引入的时候翻译成了“按帧锁定同步”,后来被大家以简化的方式(帧同步)传递开来。但不管怎么说,“帧”在实际应用中更多的是指画面渲染的频率,lockstep里面的“step”概念要更宽泛一些才是。
逻辑帧和渲染帧
了解游戏的朋友,都知道游戏是有帧率(FPS)的,每一帧都会运行相当复杂的运算(包括逻辑和渲染),如果运算规模达到一定程度就会拉长这一帧的时间,帧率也就会随之下降。所有影视作品的画面都是由一张张图构成的,每一个画面就是一帧,一秒能放多少个画面,就是有多少帧。在游戏里面,渲染器会不停的向渲染目标输出画面,并在帧与帧之间游戏处理各种逻辑,逻辑会改变游戏世界对象的行为,这些行为又会影响游戏画面的变化,这就是游戏的核心框架。早期的lockstep里面渲染和逻辑都是放在一个帧里面去处理的,这样一旦命令受到网络延迟的影响,玩家本地就会卡在一个画面直到消息的到来。
为了解决逻辑帧和渲染帧相互影响的问题
为了解决这个问题,有一些游戏会将逻辑和渲染分开处理(分为逻辑帧和渲染帧),
逻辑帧
每隔固定的时间去处理所有逻辑事件。在不是严格锁帧的情况下,你本地即使没有收到网络数据也可以在继续执行其他的逻辑并维持高频率的渲染(正在移动的对象不会由于短暂的延迟而静止不动)。这里面的逻辑帧就是lockstep里面的“Step”,也可以叫做“turn”,“bucket”或者“步”。
早期帧是采用客户端内在的心跳按一定间隔的心跳前进
早期lockstep被广泛用于局域网游戏内(延迟基本可以保持在50ms以内),所以这种策略是很有效的。lockstep每个回合的触发,并不是由收到网络包驱动,也不是由渲染帧数驱动(当然渲染帧率稳定的话也可以以帧为单位,每N帧一个回合),而是采用客户端内在的时钟稳定按一定间隔( 比如100ms) 的心跳前进。游戏的一开始,玩家在本地进行操作,操作指令会被缓存起来。在回合结束前(网络延迟在50ms以内),我们会收到所有其他客户端的指令数据,然后和前面缓存的指令一同执行并推进到下一个回合。如果玩家在一个回合开始到50ms(网络延迟的一半)都没有任何操作,我们可以认为玩家发出了一个Idle指令,打包发给其他客户端[13][14]。
锁帧做回放系统容易的问题
换个角度来看,假如一场游戏持续了20分钟,不考虑延迟的情况下整场游戏就是12000个回合(所有客户端都是如此)。现在我们反过去给每个回合添加指令,确保每个回合都收集到所有玩家的指令,那么就可以严格保证所有客户端每个回合的表现都是一样的。假如我们再把这些指令都存储起来,那么就推演出整场比赛,这也是为什么lockstep为什么做回放系统很容易。
对于lockstep为什么要发送指令而不是状态的原因
至于lockstep为什么要发送指令而不是状态,其实是与当时的网络带宽有关。很多游戏都有明确的人数限制(一般不超过10个人),玩家在每个回合能做的操作也有限甚至是不操作,这样的条件下所有玩家的指令一共也占用不了多少带宽。如果换成同步每个角色的状态数据,那么数据量可能会膨胀10倍不止。从原则上说,锁步数据既可以是游戏角色的状态信息也可以是玩家的操作指令,只不过由于各种原因没有采取状态数据罢了。在下一章,我还会对状态同步做进一步的讲解,他与lockstep的发展是相辅相成的,也不是网上常说的那种对立关系。
//lockstep操作指令的结构体
struct Input
{
bool up;
bool down;
bool left;
bool right;
bool space;
bool Z;
};
[1]WIKI “History of video games”. Available:https://en.wikipedia.org/wiki/History_of_video_games[Accessed: 2020-03-24]
[2]T.A. Funkhouser.”RING: A Client-Server System for Multi-User Virtual Environments“, In Proc. 1995 Available:https://dl.acm.org/doi/pdf/10.1145/199404.199418[Accessed: 2020-03-24]
[3]Eric Cronin, Burton Filstrup Anthony R. Kurc, Sugih Jamin,“An Efficient Synchronization Mechanism for Mirrored Game Architectures”, 2004. Available: citeseerx.ist.psu.edu/v[Accessed:2020-03-24]
[4]WIKI “Lockstep (computing)”. Available: https://en.wikipedia.org/wiki/Lockstep_(computing)[Accessed: 2020-03-24]
[5]JMP van Waveren, “The DOOM III Network Architecture” ,2006. Available:http://fabiensanglard.net/doom3_documentation/The-DOOM-III-Network-Architecture.pdf[Accessed: 2020-03-24]
[6]Jeff S. Steinrnan,“BREATHING TIME WARP” May 1993. Available:https://dl.acm.org/doi/pdf/10.1145/174134.158473[Accessed:2020-03-24]
[7]Christophe DIOT, Laurent GAUTIER, “A Distributed Architecture for Multiplayer Interactive Applications on the Internet”, IEEE, 1999. Available: https://www.cs.ubc.ca/~krasic/cpsc538a-2005/papers/diot99distributed.pdf[Accessed: 2020-03-24]
[8]Mark Terrano,Paul Bettner “Network Programming in Age of Empires and Beyond” GDC 2001. Available:https://zoo.cs.yale.edu/classes/cs538/readings/papers/terrano_1500arch.pdf[Accessed: 2020-03-24]
[9]Nathaniel E. Baughman, Brian Neil Levine, “Cheat-Proof Playout for Centralized and Distributed Online Games”, IEEE INFOCOM, 2001. Available: http://forensics.umass.edu/pubs/baughman.infocom01.pdf[Accessed: 2020-03-24]
[10]Ho Lee, Eric Kozlowski, Scott Lenker, Sugih Jamin, “Multiplayer Game Cheating Prevention With Pipelined Lockstep Protocol”, 2002. Available: http://www.ekozlowski.com/assets/multiplayer-game-cheating-prevention.pdf[Accessed: 2020-03-24]
[11]Dacid Jefferson,Henry Sowizral "Fast concurrent simulation using the time wrap mechanism " 1982. Available: https://www.rand.org/content/dam/rand/pubs/notes/2007/N1906.pdf[Accessed: 2020-03-24]
[12]J. S. Steinman, J. W. Wallace, D. Davani, and D. Elizandro. “Scalable distributed military simulations using the SPEEDES object-oriented simulation framework”. In Proc. of Object-Oriented Simulation Conference (OOS’98), pages 3–23, 1998.
其他参考资料:
[13]云风 “lockstep网络游戏同步方案” Available:https://blog.codingnow.com/2018/08/lockstep.html
[14]Skywind “再谈网游同步技术” Available: http://www.skywind.me/blog/archives/1343#more-1343
[15]DonaldW “网络游戏同步技术概述” Available:https://zhuanlan.zhihu.com/p/56923109
[16]Gordon “帧同步联机战斗(预测,快照,回滚)” Available:
https://zhuanlan.zhihu.com/p/38468615
[17]zhepama "帧同步的相关问题"Available:http://www.igiven.com/dotnet/lock-step/
[18]kisence"关于帧同步和网游游戏开发的一些心得"Available:https://www.kisence.com/2017/11/12/guan-yu-zheng-tong-bu-de-xie-xin-de/
[19]Glenn Fiedler “Deterministic Lockstep” Available:https://gafferongames.com/post/deterministic_lockstep/
[20]Qing Wei Lim “How do multiplayer games sync their state” Available:https://medium.com/@qingweilim/how-do-multiplayer-games-sync-their-state-part-1-ab72d6a54043
[21]treeform "Don’t use Lockstep in RTS games"Available: https://medium.com/@treeform/dont-use-lockstep-in-rts-games-b40f3dd6fddb
[22]Maksym Kurylovych “Lockstep protocol” Available: http://ds.cs.ut.ee/courses/course-files/Report%20-2.pdf
[23] Glenn Fiedler, “What Every Programmer Needs To Know About Game Networking A short history of game networking techniques”, 2010. Available: https://gafferongames.com/post/what_every_programmer_needs_to_know_about_game_networking/
开发者们更关心的是网络架构的实现方式
(P2P/CS)。换句话讲,在当时业内看来,P2P
架构的同步模型虽然减少了延迟,但由于作弊、跨平台、难以维护大型网络游戏等问题
,人们更希望用CS架构来取代P2P。同时,开发者们虽然可以继续在CS架构下使用逻辑比较简洁的帧同步,但有不少开发者都认为刚刚诞生的状态同步貌似更符合CS架构的同步理念。快照的含义
快照
是一个通用的行业术语,即在任何给定时刻记录设备的状态并在设备出现故障时进行还原
。快照技术常用于计算机的各种存储系统,例如逻辑卷管理、数据库、文件系统等。在游戏领域中,快照的含义更像是照片一样,将当前场景所有的信息保存起来。严格来说,快照同步应该属于状态同步的前身,虽然思想相似但是具体实现却有不小的差异。
雷神之锤游戏背景
1996年,在doom发行不久后,Id software就公开了新作——雷神之锤(Quake)。在Quake里他们舍弃了之前的P2P而改用CS架构,同时也舍弃了lockstep的同步方式。新的架构下,客户端就是一个纯粹的渲染器(称为Dumb Client),每一帧玩家所有的操作信息都会被收集并发送到服务器,然后服务器将计算后的结果压缩后发给客户端来告知他们有哪些角色可以显示,显示在什么位置上。
照片讲解
上述的这个过程就是我们所说的快照同步
,即服务器每帧接受客户端的输入来计算整个世界的状态,然后将结果快照发送给所有客户端。Quake这里所谓的快照,就是把整个游戏世界里面所有对象的状态做一次临时保存(他更强调的是对象的可视化状态
,比如位置和旋转等)。通过这个快照,我们可以还原出这一刻世界的状态应该是什么样子的。
由于所有的核心逻辑都是在服务器进行,所以也不需要通过锁步来避免客户端不同步的问题
,只要在收到服务器消息后执行渲染就好了。当然,对于性能以及网络环境较差的玩家来说,游戏体验仍然很糟糕。因为你按下一个按钮后,可能很长时间都没有反应,当收到服务器的快照消息后,你可能已经被网络好的玩家击杀了。//client
WinMain
{
while (1)
{
newtime = Sys_DoubleTime ();
time = newtime - oldtime;
Host_Frame (time)
{
setjmp
Sys_SendKeyEvents
IN_Commands
Cbuf_Execute
/* Network */
CL_ReadPackets
CL_SendCmd
/* Prediction//Collision */
CL_SetUpPlayerPrediction(false)
CL_PredictMove
CL_SetUpPlayerPrediction(true)
CL_EmitEntities
/* Rendition */
SCR_UpdateScreen
}
oldtime = newtime;
}
}
出现的问题
但Quake里面由于客户端只是一个简单的渲染器,同步过程中会出现很多明显的问题,比如延迟过大,客户端性能浪费,服务器压力大等。而其中最明显的问题就是对带宽的浪费
,对于一个物体和角色比较少的游戏,可以使用快照将整个世界的状态都存储并发送,但是一旦物体数量多了起来,带宽占用就会直线上升。所以,我们希望不要每帧都把整个世界的数据都发过去
,而是只发送那些产生变化的对象数据(可以称为增量快照同步)。更进一步的,我们还希望将数据拆分的更细一些,并根据客户端的特点来定制发送不同的数据
。基于这种思想,《星际部落:围攻》团队的开发者们开始对网络架构进行抽象和分层,构造出来一套比较完善的"状态同步"系统并以此开发出了Tribe游戏系列。
TRIBES ENGINE介绍
The TRIBES Engine可以认为是第一个实现状态同步的游戏引擎,《星际部落:围攻》也可以认为是第一个比较完美的实现了状态同步的游戏。下图是该引擎的网络架构[6]:
预测备注
关于预测,其实就是本地先执行,所以并不需要什么特别的算法,反倒是预测后的客户端与服务器的同步处理有很多值得优化的地方。由于玩家的行为是没办法完全预测的,所以你不知道玩家会在什么时候突然停下或者转弯,所以经常会发生预测失败的情况。
若客户端复测与服务器不一致
1)
在没有时间戳的条件下,收到了一条过时的服务器位置数据。你在本地的行为相比服务器是超前的,假如你在time=10ms的和time=50ms时候分别发送了一条指令。由于网络延迟的存在,当你已经执行完第二个指令的时候才收到服务器对第一条指令的位置同步。很明显,我们不应该让过时的服务器数据来纠正你当前的逻辑。解决方法
就是在每个指令发出的时候带上他的时间戳
,这样客户端收到服务器反馈的时候就知道他处理的是哪条指令信息。
2)
假如我们在指令里面添加了时间戳的信息,并收到了一条过时的服务器位置数据。在上一篇文章里我们提到了TimeWarp算法
,即当一个对象收到了一个过去某个时刻应该执行的事件时,他应该回滚到那个时刻的状态,并且回滚前面所有的行为与状态(包括取消之前行为所产生的事件)
。这个时候我们可以用类似的方法在本地进行纠正,大体的方案就是把玩家本地预执行的指令都记录好时间戳并存放到一个MOVE BUFFER列表里(类似一个滑动窗口)。如果服务器的计算结果与你本地预测相同,可以回复你一个ACKMOVE。如果服务器发现你的某个移动位置有问题时,会把该指令的时间戳以及正确的位置打包发给你。当你收到ACKMOVE的时候,你可以把MOVE BUFFER里面的数据从表里面移除,而当你收到错误纠正信息时就需要本地回滚到服务器指定的位置同时把错误时刻后面MOVE BUFFER里面的指令重新执行一遍。这里读者可能会产生一个疑问——为什么不直接拉回
?因为这时候他想纠正的是之前的错误而不是现在的错误
,如果简单的拉回就会让你觉得被莫名其妙的拉回到以前的一个位置。同时,考虑到已经在路上的指令以及后续你要发送的预测指令,会让服务器后续的校验与纠正变得复杂且奇怪,具体流程细节可以参考下图。另外,Gabriel Gambetta博主在他的文章中,也对这种情况进行了简单的分析[10]。
(意思还是被拉回去,但是不是拉回去开始的位置,而是拉回去上个包的位置)
(P2P架构下)
所有其他客户端,我要取消前面的操作,然后其他客户端在本地也执行回滚。而在如今的CS架构状态同步的方式下,服务器可能早就拒绝了客户端的不合法行为,所以并不需要处理回滚(同理,其他客户端也是)。所以严格来说,TimeWarp技术以及优化后的BreathTimeWarp技术[12]都是针对“以事件驱动的帧同步”,并不能与预测回滚这套方案完全等价。当然,随着时间的推移,很多概念也变的逐渐宽泛一些,我们平时提到的时间回溯TimeWarp技术大体上与快照回滚是一个意思的。事件锁定的出现
1997年,Jim Greer与Zack Booth Simpson在开发出了他们第一款基于CS架构的RTS游戏——”NetStorm:Island at war“。随后在发布的文章中又提出了“事件锁定”
这一概念[13],相比帧同步会受到其他客户端延迟的影响,事件锁定是基于事件队列严格按序执行的,客户端只管发消息然后等待服务器的响应即可,其他时候本地正常模拟,不需要等待。在目前常见的游戏中,我们很少会听说到事件锁定这种同步方式,因为事件锁定的本质就是通过RPC产生事件从而进行同步(也就是排除属性同步的状态同步)。事件锁定在CS架构上是非常自然的,相比帧同步,可以定义并发送更灵活的信息,也不必再担心作弊的问题。
以时间同步来纠正服务器对客户端的不合法操作
不过,由于事件中经常会含有时间相关的信息(比如在X秒进行开火)以及服务器需要对客户端的不合法操作进行纠正,所以我们需要尽可能的保持客户端与服务器的时钟同步。实现时钟同步最常见且广泛的方式就是网络时间协议(Network Time Protocol,简称NTP)[14],NTP属于应用层协议下层采用UDP实现,1979年诞生以来至今仍被应用在多个计算机领域里,包括嵌入式系统时间、通信计费、Windows时间服务以及部分游戏等。NTP使用了一种树状、半分层的网络结构来部署时钟服务器,每个UDP数据包内包含多个时间戳以及一些标记信息用来多次校验与分析,
(备注:整个时钟同步的具体算法涉及到非常多的细节,我们这里只考虑他的时钟同步算法(其他的内容请参考历年的RFC))
详细讲解
假如一个服务器与客户端通信,客户端在t0向服务器发送数据,服务器在t1收到数据,t2响应并回包给客户端,最后客户端在t3时间收到了服务器的数据。
二者的时间差为“θ”,假如往返延迟相同,则有
所以可以将“θ”定义为
将往返延迟相加,那么可以得到一个RTT延迟
当然,该操作不会只执行一次,客户端会同时请求多个服务器,然后对结果进行统计分析、过滤,并从最好的三个剩余候选中估算时间差,然后调整时钟频率来逐渐减小偏移。如果我们的系统对精度要求不是非常高,我们还可以使用简化版的SNTP(Simple Network Time Protocal),时钟同步算法与NTP是相同的,不过简化了一些流程。
消除高阶的流式时间同步:
不过无论是NTP还是SNTP,对于游戏来说都过于复杂(而且只能用UDP实现)。因此Jim Greer等人提出了“消除高阶的流式时间同步”,流程如下:
上述算法精髓在于丢弃和中间值偏差超过一个标准偏差的数值。其目的是为了去除TCP中重传的数据包。举例来说,如果通过TCP发送了10个数据包,而且没有重传。这时延迟数据将集中在延迟的中位数附近。假如另一个测试中,如果其中第10个数据包被重传了,重传将导致这次的采样在延迟柱状图中极右端,处于延迟中位数两倍的位置。通过直接去掉超出中位数一个标准偏差的样例,可以过滤掉因重传导致的不准确样例。(排除网络很差重传频繁发生的情况)
内插值
[15]( interpolation )以及外插值
[16](extrapolation,或者叫外推法)两种。片段插值(Piecewise constant interpolation)
线性插值(Linear interpolation)
多项式插值(Polynomial interpolation)
样条曲线插值(Spline interpolation)
三角内插法(trigonometric interpolation)
有理内插(rational interpolation
小波内插(wavelets interpolation)
2)外插法
外插值,指从已知数据的离散集合中构建超出原始范围的新数据的方法,也可以指根据过去和现在的发展趋势来推断未来,属于统计学上的概念。与外插值还有一个相似的概念称为DeadReckoning(简称DR),即导航推测。DR是一种利用现在物体位置及速度推定未来位置方向的航海技术,属于应用技术方向的概念。DR的概念更贴近游戏领域,即给定一个点以及当前的方向等信息,推测其之后的移动路径,外推的算法也有很多种,
线性外推(Linear extrapolation)
多项式外推(Polynomial extrapolation)
锥形外推 (Conic extrapolation)
云形外推 (French curve extrapolation)
在游戏中,一般按照线性外推或匀变速直线运动推测即可。不过,对于比较复杂的游戏类型,我们也可以采用三次贝塞尔曲线、向心Catmull-Rom曲线等模拟预测。
总之,无论是内插值还是外插值,考虑到运算的复杂度以及表现要求,游戏中以线性插值、简单的多项式插值为主。
3)插值法应用
早期的lockstep算法中,在一个客户端在收到下一帧信息前,为了避免本地其他角色静止卡顿,会采用外插值来推断其接下来一小段时间的移动路径[17][18]。普通DR存在一个问题(参考下图),t0时刻其他客户端收到了主机的同步信息预测向虚线的方向移动,不过主机客户端却开始向红色路径方向移动,等其他客户端在t1时刻收到同步信息后会被突然拉倒t1’的位置,这造成了玩家不好的游戏体验。为了解决从预测位置拉扯到真实位置造成的视觉突变,我们会增加一些相应的算法来将预测对象平滑地移动到真实位置。
在状态同步中,由于客户端每次收到的是其他的角色的位置信息,为了避免位置突变,本地会采用内插值来从A点过度到B点
。插值的目的很简单,就是为了保证在同步数据到来之前让本地的角色能有流畅的表现。
[1]"State Synchronization's Role in High Availability" Available:http://etutorials.org/Networking/Check+Point+FireWall/Chapter+13.+High+Availability/State+Synchronization+s+Role+in+High+Availability/[Accessed:2020-07-17]
[2]WIKI, "Check Point VPN-1" Available: https://en.wikipedia.org/wiki/Check_Point_VPN-1[Accessed:2020-07-17]
[3]Check Point Documentation, "Synchronizing Connections in the Cluster" Available:https://sc1.checkpoint.com/documents/R80.10/WebAdminGuides/EN/CP_R80.10_ClusterXL_AdminGuide/html_frameset.htm?topic=documents/R80.10/WebAdminGuides/EN/CP_R80.10_ClusterXL_AdminGuide/7288[Accessed:2020-07-17]
[4]id-Software,"GitHub Game Source Code" Available:https://github.com/id-Software [Accessed:2020-07-17]
[5]FABIEN SANGLARD," FABIEN SANGLARD'S WEBSITE With GameSource Code Analysis " Available:https://fabiensanglard.net/ [Accessed:2020-07-17]
[6] Mark Frohnmayer, Tim Gift, "The TRIBES Engine Networking Model or How to Make the Internet Rock for Multi player Games", 1998. Available: https://www.gamedevs.org/uploads/tribes-networking-model.pdf[Accessed:2020-07-17]
[7]WIKI, "Client-Side Prediction" Available:https://en.wikipedia.org/wiki/Client-side_prediction [Accessed:2020-07-17]
[8]id-Software,"The Quake 2 Networking Data Flow" Available:http://www.gamers.org/dEngine/quake2/Q2DP/Q2DP_Network/Q2DP_Network.html#toc4 [Accessed:2020-07-17]
[9]WIKI, "QuakeWorld" Available:https://en.wikipedia.org/wiki/QuakeWorld[Accessed:2020-03-24]
[10]Gabriel Gambetta," Client-Server Game Architecture" Available:https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html[Accessed:2020-07-17]
[11]Dacid Jefferson,Henry Sowizral "Fast concurrent simulation using the time wrap mechanism " 1982.Available: https://www.rand.org/content/dam/rand/pubs/notes/2007/N1906.pdf[[Accessed:2020-07-17]
[12]M. Damitio S. J. Turner,"Comparing the Breathing Time Buckets Algorithm and the Time Warp Operating System on a Transputer Architecture” January 1999. Available:https://www.researchgate.net/publication/2763616_Comparing_the_Breathing_Time_Buckets_Algorithm_and_the_Time_Warp_Operating_System_on_a_Transputer_Architecture[Accessed:2020-07-17]
[13]Jim Greer, Zack Booth Simpson, "Minimizing Latency in RealTine Strategy Games" Game Progamming Gems 3 chapter 5.1, 2001.
[14]]WIKI, "Network Time Protocol" Available:https://en.wikipedia.org/wiki/Network_Time_Protocol[Accessed:2020-07-17]
[15]WIKI, "Interpolation" Available:https://en.wikipedia.org/wiki/Interpolation[Accessed:2020-07-17]
[16]WIKI, "Extrapolation" Available:https://en.wikipedia.org/wiki/Extrapolation[Accessed:2020-07-17]
[17]Jesse Aronson, "Dead Reckoning: Latency Hiding for Networked Games" September 19, 1997.Available:https://www.gamasutra.com/view/feature/131638/dead_reckoning_latency_hiding_for_.php[Accessed:2020-07-17]
[18]梁白鸥等,“Dead Reckoning技术在网络游戏中的应用” 2007.Available:http://www.arocmag.com/getarticle/?aid=2f665567e92cf534[Accessed:2020-07-17]
为了让客户端提高预测准确率(保证客户端与服务器上的代码逻辑一致),所以半条命里面他们让客户端与服务器执行的是同一套代码
。其次,(2)考虑到本地玩家的时间总是领先服务器,玩家开枪的时间到服务器执行时就一定会被延迟,所以为了尽量减小延迟所带来的问题
,他们提出了一种名为延迟补偿的
技术。然后当B的指令到达服务器的时候,如果采用延迟补偿,就需要把A回滚到之前的位置结果就是A收到了B的攻击,这对A来说显然是不公平的
。如果该情况发生在FPS
里面,就不会有很大的问题,因为A根本不知道B什么时候瞄准的A
。TSS
)。在他们看来,TimeWarp需要频繁的生成游戏快照进而占用大量内存(每次发送命令前都要生成一份),而且每次遇到过期信息就立刻回滚并可能产生大量的对冲事件(anti-message)
。这种同步方式是不适合Quake这种类型的FPS游戏的。以某种延迟(比如100ms)间隔为单位对游戏做快照。他事先保存了N个完整的游戏状态(快照)以及命令链表,让这N个状态以不同的延迟去模拟推进。游戏中延迟最低且被采用的状态称为Leading State,其他的称为Trailing State,每个状态都记录着一个命令链表
(执行的以及未执行的),各个状态的延迟间隔由开发者设定。图A
图B Trailing State S1检测冲突并触发回滚
TSS对于Timewarp的最大优势
TSS相比TimeWarp,最大的优势就是1)大大降低了快照的记录频率
(由原来的按事件记录改为按延迟时间分开记录),同时他2)可以避免由于网络延迟造成的连续多次指令错误而不断回滚的问题
(Leading State不负责触发回滚,Trailing State检测并触发)。
TSS的缺点
不过TSS同时维护了多个游戏世界的快照,也无形中增加了逻辑的复杂度
,在最近几年的网络游戏中也并没有看到哪个游戏使用了这种同步算法。在我看来,其实我们不必将整个世界的快照都记录,只要处理好移动的快照同时使用服务器状态同步就可以满足大部分情况了。
光环项目的网络架构同样被分层,但相比Tribe却更加简洁和精炼。上图的Replication层是Gameplay开发中比较重视的,他决定了我们逻辑上层可用的同步
手段。Halo里面有三种基本协议,State Data、Event、ControlData,分别是指
“基于对象的属性同步”、
“通过调用产生的事件同步”以及
“玩家的输入信息同步”,
------其中移动同步归类于ControlData协议。
守望先锋可以说是近年来将网络同步优化到极致的FPS游戏,其中涵盖了我们可以用到的大部分同步优化技术。在2018年的GDC上,来自守望先锋的Gameplay程序TimFord分享了整个游戏的架构以及网络同步的实现方式[24]。
虽然OverWatch基于CS架构,但是却同时用到了帧同步(逻辑帧概念)以及状态同步包含的多种技术手段。为了实现确定性,他们固定了更新周期为16毫秒(电竞比赛时7毫秒),每个周期称为一个“命令帧”
(等同于Lockstep中的“Turn”、“Bucket”)。在所有与客户端预表现和玩家行为有关的操作不会放在Update而是放在固定周期的UpdateFixed里更新
,方便客户端预测与回滚。不过,整个游戏同步的核心还是状态同步,玩家也并不需要等待其他客户端的行为
一句话概括:守望先锋采用的是基于ECS架构的带有预测回滚的增量状态同步
客户端本地会不断读取输入并立刻进行角色移动的模拟,他会在客户端记录一个缓冲区来保存历史的运动轨迹(即运动快照),用于后续与服务器纠正数据进行对比以及回滚。
客户端添加了一个buffer来存储玩家的输入操作(带有命令帧的序号),同时保留历史的技能快照。一旦服务器发现客户端预测执行失败,就会让客户端先通过快照回滚到错误时刻(包括移动和技能),然后把错误时刻到当前时间的所有输入都重执行一遍
图片说明:左边是服务器通知客户端被眩晕,右边是客户端收到后进行回滚
伤害计算在服务器,但是命中判定是在客户端处理(所以可能存在一些误差)。延迟补偿技术也被采用,但是不是在服务器回滚所有玩家的位置,而是检测当前玩家的准星与附近敌人的逻辑边界(bounding volumes)是否有交集,没有的话不需要回滚
(1)一旦PING值超过220毫秒,我们就会延后一些命中效果,也不会再去预测了,直接等服务器回包确认。
(2)PING为0的时候,对弹道碰撞做了预测,而击中点和血条没有预测,要等服务器回包才渲染。
(3)当PING达到300毫秒的时候,碰撞都不预测了,因为射击目标正在做快读的外插,他实际上根本没在这里,这里也用到了前面提到的DR(Dead Reckoning)外推算法(插值推测法)。
状态同步是如何实现的。同样在2018年的GDC上,来自Overwatch服务器团队的开发工程师Phil Orwig分享了有关回放与同步的相关技术细节[25]。
目的
1)为了提高通信效率,守望也采用定制的可靠UDP,
因此会有不可避免的丢包情况。为了对抗丢包,每一帧的数据包包含的是最近N帧的数据,即使某一
个数据包丢了也没什么影响。
2)除此之外,他们还在服务器添加了一个缓冲区,
记录玩家的输入信息。缓冲区越大,就能容忍越多的丢包,但是也意味着同步延迟越大。所以,在网络
条件良好的情况下,他们会尽力减小这个缓冲区的大小,而一旦客户端丢包,那么就可以提高客户端发送数据频率,进而服务器收到更多的包,缓存更多
的数据用于抵消丢包。
“状态同步”同步的是对象的状态信息,如角色的位置、生命值等。
目前的状态同步多用于CS架构,客户端通过RPC向服务器发送指令信息,服务器通过属性同步(增量状态同步)向客户端发送各个对象的状态信息。我们可以采用预测回滚、延迟补偿、插值等优化方式,甚至也可以采用“命令帧”的方式对同步做限制。不过在这个过程中,传递的内容以状态信息(即计算后的结果)为主,收到信息的另一端只需要和解同步过来的状态即可,不需要在本地通过处理其他端的Input信息来进行持续的模拟。
[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]
在网络游戏中,如果玩家的位置或者与玩家交互对象的位置需要经过物理引擎的模拟处理来得到结果,那么其中涉及到网络同步技术就可以称为物理同步!
(这里的物理模拟一般指整个对象完全交给物理引擎去计算碰撞、位置、约束等,很多情况下可以等价为对Ragdoll的模拟)备注:物理一词涉及的范围非常广,在游戏里面应用的场景也很多,但是并不一定需要进行网络同步,比如简单的抛物线运动,射线检测,与玩法无关的场景破碎等。
正如所前面解释的那样,物理同步并不是一种特殊的同步方式,而是在物理引擎和网络同步技术共同发展的条件下而诞生的一种综合行性解决方案,其核心手段还然是我们熟悉的帧同步或者状态同步。使用帧同步技术我们需要每帧把玩家的Input信息发送出去
,然后让另一端的物理引擎根据输入去模拟结果。如果使用状态同步我们则需要本地模拟好数据并把物理位置、旋转等关键信息发送到其他的客户端,然后其他客户端可以根据情况决定是否再执行本地的物理模拟
(如果是快照同步
,由于拿到的就是最终的结果,那么就不需要本地再进行模拟了
)
这样看来,物理同步好像与常规的同步也没什么本质上的区别,那么为什么他却是一个难题呢?我认为原因有以下两点:
1)物理引擎的不确定性
2)在物理引擎参与模拟的条件下,网络同步的微小误差很容易被迅速放大
1.编译器优化后的指令顺序
2.约束计算的顺序
3.不同版本、不同平台浮点数精度问题[4][5]
(问题1与问题3其实是密切相关的)
The PhysX SDK can be described as offering limited determinism(注:提供了有限程度的确定性). Results can vary between platforms due to differences in hardware maths precision and differences in how the compiler reoders instructions during optimization. This means that behavior can be different between different platforms, different compilers operating on the same platform or between optimized and unoptimized builds using the same compiler on the same platform(注:不同平台、编译器、优化版本都会影响确定性). However, on a given platform, given the exact same sequence of events operating on the exact scene using a consistent time-stepping scheme, PhysX is expected to produce deterministic results. In order to achieve this determinism, the application must recreate the scene in the exact same order each time and insert the actors into a newly-created PxScene. There are several other factors that can affect determinism so if an inconsistent (e.g. variable) time-stepping scheme is used or if the application does not perform the same sequence of API calls on the same frames, the PhysX simulation can diverge.
(Havok,PhysX,Bullet)
基本上都可以保证结果的一致性。因为我们可以通过使用同一个编译好的二进制文件
、在完全相同的操作系统
上运行来保证指令顺序并解决浮点数精度问题,同时打开引擎的确定性开关
来保证约束的计算顺序(不过会影响性能),这也是很多测试者在使用Unity等商业引擎时发现物理同步可以完美进行的原因。当然,这并不是说我们就完全放弃了跨平台确定性的目标,比如Unity新推出的DOTS架构
[7][8]正在尝试解决这个问题(虽然注释里面仍然鲜明的写着“Reserved for future”)。导致的结果
考虑到物理引擎的确定性问题,我们可以得出一个初步的结论——完全使用帧同步做物理同步是不合适的(或者说做跨平台游戏是行不通的)。而对于状态同步,我们可以定时地去纠正位置信息来避免误差被放大。如果一定要使用帧同步去做跨平台同步,那么只能选择放弃物理引擎自己模拟或者用定点数来改造物理引擎,这可能是得不偿失的。
大师的办法
下面不妨先排除掉一致性的问题,来看看如何实现所谓的“物理同步”。实际上,无论是优化手段还是实现方式与前两篇提到的方案是几乎一致的,帧同步、快照同步、状态同步都可以采用,增量压缩、Inputbuffer等优化手段也一样可以用于物理同步的开发中。Network Next的创始人Glenn Fiedler在2014年撰写了一系列的物理同步相关的文章[9],使用一个同步的Demo非常详细地阐述了同步技术是如何应用以及优化的。涉及到的技术点大致如下,涵盖了网络同步的大部分的知识细节:
1)如何确保物理引擎的确定性
2)如何实现物理帧同步
3)Inputbuffer如何改善帧同步
4)为什么用UDP替代TCP
5)如何实现快照同步
6)怎样用插值解决网络抖动
7)如何通过快照压缩减少网络流量
8)如何实现增量压缩
9)如何实现状态同步
接下来,我们再来谈谈第二个难点,即网络同步的误差是如何被物理模拟迅速放大的(尤其在多人交互的游戏中)。我们在前面的章节里也谈过,为了保证本地客户端的快速响应,通常会采取预测回滚的机制(Client prediction,即本地客户端立刻相应玩家操作,服务器后续校验决定是否合法)。这样我们就牺牲了事件顺序的严格
一致来换取主控端玩家及时响应的体验
,在一般角色的非物理移动同步时,预测以及回滚都是相对容易的,延迟比较小的情况位置的误差也可以几乎忽略。然而在物理模拟参与的时候,情况就会变得复杂起来。
主控(Autonomous/Master)以及模拟(Simulate/Replica)都是针对某个对象而言的。
假如有两个客户端,玩家A控制小车1,玩家B控制小车2。小车1在玩家A的客户端上就是主控的,小车2在玩家A的客户端上就是模拟的。同理,小车2在B客户端上就是主控的,小车1在B客户端上就是模拟的。
假如在一个游戏中(带有预测,也就是你本地的对象一定快于远端)你和其他玩家分别控制一个物理模拟的小车朝向对方冲去,他们相互之间可能发生碰撞而彼此影响运动状态,就会面临下面的问题。
1.由于网络同步的误差无法避免,那么你客户端上的发生碰撞的位置一定与其他客户端的不同
(▶本地客户端的模拟小车(对手小车)一定落后其控制端)
2.其次,对于本地上的其他模拟小车,要考虑是否在碰撞时完全开启物理模拟(Ragdoll)。如果不开启物理,那么模拟小车就会完全按照其主控端同步的位置进行移动,即使已经在本地发生了碰撞他可能还是会向前移动
。如果开启碰撞,两个客户端的发生碰撞的位置会完全不同
。无论是哪种情况,网络同步的误差都会在物理引擎的“加持”下迅速被放大进而导致两端的结果相差甚远。
3、其实对于一般角色的非物理移动同步,二者只要相撞就会迅速停止移动
,即使发生穿透只要做简单的位置“回滚”即可。然而在物理模拟参与的时候,直接作位置回滚的效果会显得非常突兀并出现很强的拉扯感
,因为我们几乎没办法在本地准确的预测一个对象的物理模拟路径。如果你仔细阅读了前面Glenn Fiedler的文章(或者上面总结的技术点),你会发现里面并没有提到常见的预测回滚技术,因为他只有一个主控端和一个用于观察结果的模拟端,并不需要回滚。
《看门狗2》
的网络模型是基于状态同步的P2P,主控角色预测先行而模拟对象会根据快照
(snapshot,即模拟对象在其主控端的真实位置)使用Projective Velocity Blengding做内插值,他们在制作时也面临和上面描述一样的问题。假如两个客户端各控制一个小车撞向对方,由于延迟问题,敌人在本地的位置一定是落后其主控端的
。那么就可能发生你开车去撞他时,你本地撞到了他的车尾,而他的客户端什么都没有发生。
TimeOffset
的概念,根据当前时间与TimeOffset的差值来决定对模拟对象做内插值
还是外插值
,有了合适的外插值后本地的模拟对象就可以做到尽量靠近敌方的真实位置。而关于碰撞后位置的误差问题,他们采用了Physics Simulation Blending
技术,即发生碰撞前开启模拟对象的RigidBody并设置位置权重为1(快照位置的权重为0),然后在碰撞发生后的一小段时间内,不断减小物理模拟的权重增大快照位置的权重使模拟对象的运动状态逐渐趋于与其主控端
,最终消除不一致性,腾讯的吃鸡手游就采用了相似的解决方案[13]。
不过实际上, Matt团队遇到的问题远不止这些,还有诸如如何用插值解决旋转抖动
问题,人物与载具相撞时不同步
怎么办等等,知乎上有一篇译文可以参考[14]。
可能有些朋友会问,如果我不使用预测回滚技术是不是就没有这个问题呢?答案依然是否定的,假如你在运行一个车辆的中间突然变向,而这个操作被丢包或延迟,只要服务器不暂停整个游戏来等待你的消息,那么你本地的结果依然与其他客户端不同进而产生误差。也就是说除非你使用最最原始的“完全帧同步”(即客户端每次行动都要等到其他客户端的消息全部就绪才行),否则由于网络同步的延迟无法避免,误差也必定会被物理模拟所放大。
《火箭联盟》
悄然上线,可谓是将物理玩法发挥到了极致。次年,《火箭联盟》的开发者Jared Cone也来到了GDC,分享了他们团队是如何解决物理同步问题的[14]。《火箭联盟》的核心玩法是“用车踢球”,每个玩家控制一个汽车,通过撞击足球来将其“踢”进敌方的球门。由于是多人竞技游戏,所以一定要有一个权威服务器
来避免作弊,最终的结果必须由服务器来决定。相比于《看门狗》,他们遇到的情况明显更复杂,除了不同玩家控制不同的小车,还有一个完全由服务器操控的小球
。按照常规的同步方式,本地的主控玩家预测先行,其他角色的数据由服务器同步下发做插值模拟。但是在这样一个延迟敏感且带有物理模拟的竞技游戏中,玩家的Input信息的丢失、本地对象与服务器的位置不统一都会频繁的带来表现不一致的问题,而且FPS中常见的延迟补偿策略
并不适合当前的游戏类型(简单来说就是延迟大的玩家会影响其他玩家的体验,具体原因我们在上一篇延迟补偿的章节也有讨论)。
ji+ 解决方案
为了解决这些问题,Jared Cone团队采用了“InputBuffer”
以及“客户端全预测”
两个核心方案。
1)InputBuffer,即服务器缓存客户端的Input信息,然后定时的去buffer里面获取(buffer大小可以动态调整),这样可以减少网络延迟和抖动带来的卡顿问题。
2)客户端全预测,即客户端上所有可能产生移动的对象(不仅仅是主控对象)全部会在本地预测先行,这样本地在预测成功时所有对象的位置都是准确的,客户端与服务器的表现也会高度一致,当然预测失败的时候自然会也要处理位置回滚。
仔细分析这两款游戏,你会发现他们采用都是“状态同步+插值+预测回滚”的基本框架,这也是目前业内上比较合适的物理同步方案。
除了同步问题,物理引擎本身对系统资源(CPU/GPU)的消耗也很大。比如在UE4引擎里面,玩家每一帧的移动都会触发物理引擎的射线检测来判断位置是否合法,一旦场景内的角色数量增多,物理引擎的计算量也会随之增大,进而改变Tick的步长,帧率降低。而帧率降低除了导致卡顿问题外,还会进一步影响到物理模拟,造成更严重的结果不一致、模型穿透等问题,所以我们需要尽量减少不必要的物理模拟并适当简化我们的计算模型。
参考资料:
[1] WIKI, "Pong", WIKI, 2020.Available:https://en.wikipedia.org/wiki/Pong[Accessed:2020-12-12]
[2] Tony Wang, "游戏物理模拟简史", 知乎, 2020.Available:https://zhuanlan.zhihu.com/p/106977617[Accessed:2020-12-12]
[3] Theraot, "How can I perform a deterministic physics simulation?",Gamedev Stackexchange, 2019.https://gamedev.stackexchange.com/questions/174320/how-can-i-perform-a-deterministic-physics-simulation[Accessed:2020-12-12]
[4] Yossi Kreinin, "Consistency: how to defeat the purpose of IEEE floating point", Personal Blog , 2008. Available:http://yosefk.com/blog/consistency-how-to-defeat-the-purpose-of-ieee-floating-point.html[Accessed:2020-12-12]
[5] Glenn Fiedler, "Floating Point Determinism", Personal Blog , 2010. Available:https://gafferongames.com/post/floating_point_determinism/[Accessed:2020-12-12]
[6] NVIDIA, "NVIDIA PhysX SDK 3.4.0 Documentation Determinism", NVIDIA , 2020. Available:https://docs.nvidia.com/gameworks/content/gameworkslibrary/physx/guide/Manual/BestPractices.html[Accessed:2020-12-12]
[7]MelvMay, "How much deterministic is Physics from Unity3d In 2019?",Unity Forum,2020.Available:https://forum.unity.com/threads/how-much-deterministic-is-physics-from-unity3d-in-2019.711311/[Accessed:2020-12-12]
[8]Unity, "Burst User Guide",Unity Manual, 2020.Available:https://docs.unity3d.com/Packages/[email protected]/manual/index.html?_ga=2.60059693.1096806956.1607653832-2097754989.1600740353[Accessed:2020-12-12]
[9] Glenn Fiedler, "Introduction to Networked Physics", Personal Blog,2014. Available: https://gafferongames.com/post/introduction_to_networked_physics/[Accessed:2020-12-12]
[10]Glenn Fiedler,"Physics for Game Programmers : Networking for Physics Programmers", 2018.Available:https://www.gdcvault.com/play/1022195/Physics-for-Game-Programmers-Networking[Accessed:2020-12-12]
[11]Glenn Fiedler,"UnityDemo: Networked Physics in Virtual Reality: Networking a stack of cubes with Unity and PhysX" , 2018. Available:https://github.com/fbsamples/oculus-networked-physics-sample/[Accessed:2020-12-12]
[12] Matt Delbosc, "Replicating Chaos Vehicle Replication in Watch Dogs 2", GDC, 2017. Available:https://www.bilibili.com/video/BV1KA41187jk[Accessed:2020-12-12]
[13]Ned,"手游中载具物理同步的实现方案", 腾讯游戏学院, 2018. Available:https://gameinstitute.qq.com/knowledge/100044[Accessed:2020-12-12]
[14]Funny David, "看门狗2的载具同步(翻译)", 知乎, 2019. Available:https://zhuanlan.zhihu.com/p/95560180[Accessed:2020-12-12]
[15]Jared Cone, "It IS Rocket Science! The Physics of 'Rocket League' Detailed", GDC, 2018. Available:https://www.bilibili.com/video/av44416219[Accessed:2020-12-12]