2021-02-06

原文:https://zhuanlan.zhihu.com/p/164686867?utm_source=wechat_session&utm_medium=social&utm_oi=57424723574784&utm_content=first

细谈网络同步在游戏历史中的发展变化(中)

细谈网络同步在游戏历史中的发展变化(中)

网易游戏雷火事业群

网易游戏雷火事业群

已认证的官方帐号

已关注

350 人赞同了该文章

非常不好意思让大家久等了,上一篇文章细谈网络同步在游戏历史中的发展变化(上)我们讨论了网络同步的基本概念以及锁步同步(帧同步)的发展历史,这篇我们继续讲述状态同步的发展历程与基本原理。本文作者依旧是网易游戏雷火的游戏开发工程师 

@Jerish

 ,欢迎大家在评论区提问以及和我们互动!

目录(中篇):

四. 状态同步 State Synchronization

1.雷神之锤与快照同步(Quake and Snapshot)

2.《星际围攻:部落》中的网络架构(The TRIBES Engine Networking Model)

3.客户端预测与回滚(Client-side prediction and Rollback)

4.事件锁定与时钟同步(Event Locking and Clock Synchronization)

5.插值技术(Interpolation and Extrapolation )

6.延迟补偿(Lag Compensation)

7.跟随状态同步(自译)(Trailing state synchronization)

8.状态同步框架的演变

9.守望先锋与ECS架构

10.状态同步小结

四、状态同步 State Synchronization

 

上一篇我们曾提到,国内对“Lockstep”的翻译经常会引起一些初学者的误解。但好在网上的参考文献和资料比较丰富的,查证起来还比较容易。反倒是当我开始仔细研究大家都耳熟能详的“状态同步”时,我竟然一时半会难以追溯其发展来源。可能是因为早期的技术领域里面好像并没有这个概念,笔者能找到最早的关于状态同步的资料源于上世界90年代的防火墙产品——FireWalls-1[1][2][3]。

这款产品出自公司Check Point,是第一个使用状态检测(stateful inspection)的商业防火墙软件。其中的stateful inspection表示让多个防火墙共享各自状态表中包含的信息,这种信息传递的方式与我们游戏中的状态同步非常相似。由此可见,状态同步与Lockstep一样,也是经历了一个相对漫长的时间才发展成如今我们熟知的模样。

在二十年前,相比于使用帧同步(为了方便描述,后续的文章中以帧同步代替Lockstep)还是状态同步,开发者们更关心的是网络架构的实现方式(P2P/CS)。换句话讲,在当时业内看来,P2P架构的同步模型虽然减少了延迟,但由于作弊、跨平台、难以维护大型网络游戏等问题,人们更希望用CS架构来取代P2P。同时,开发者们虽然可以继续在CS架构下使用逻辑比较简洁的帧同步,但有不少开发者都认为刚刚诞生的状态同步貌似更符合CS架构的同步理念。

需要强调的是,帧同步与状态同步并不是一个简单的对立概念,其中的差异包括“数据格式与内容”,“逻辑的计算位置”,“是否有权威服务器”等 。随着时间的推进,两种算法互相借鉴互相发展,早已不是当年的样子。网上存在很多概念模糊的文章,包括一些大佬对同步的概念理解也有偏差,这些都很容易对新手产生误导。所以笔者建议,如果你想真正的了解或者学习网络同步,不妨跟着这篇文章去了解二者的发展历史,相信看过后的你一定能更深刻的理解到帧同步与状态同步的异同。(文末同上篇一样贴出了大量的文献内容)

 

1.雷神之锤与快照同步(Quake and Snapshot)

快照是一个通用的行业术语,即在任何给定时刻记录设备的状态并在设备出现故障时进行还原。快照技术常用于计算机的各种存储系统,例如逻辑卷管理、数据库、文件系统等。在游戏领域中,快照的含义更像是照片一样,将当前场景所有的信息保存起来。严格来说,快照同步应该属于状态同步的前身,虽然思想相似但是具体实现却有不小的差异。

1996年,在doom发行不久后,Id software就公开了新作——雷神之锤(Quake)。在Quake里他们舍弃了之前的P2P而改用CS架构,同时也舍弃了lockstep的同步方式。新的架构下,客户端就是一个纯粹的渲染器(称为Dumb Client),每一帧玩家所有的操作信息都会被收集并发送到服务器,然后服务器将计算后的结果压缩后发给客户端来告知他们有哪些角色可以显示,显示在什么位置上。

上述的这个过程就是我们所说的快照同步,即服务器每帧接受客户端的输入来计算整个世界的状态,然后将结果快照发送给所有客户端。Quake这里所谓的快照,就是把整个游戏世界里面所有对象的状态做一次临时保存(他更强调的是对象的可视化状态,比如位置和旋转等)。通过这个快照,我们可以还原出这一刻世界的状态应该是什么样子的。

Quake运行时,逻辑帧率与渲染帧率是保持一致的。由于所有的核心逻辑都是在服务器进行,所以也不需要通过锁步来避免客户端不同步的问题,只要在收到服务器消息后执行渲染就好了。当然,对于性能以及网络环境较差的玩家来说,游戏体验仍然很糟糕。因为你按下一个按钮后,可能很长时间都没有反应,当收到服务器的快照消息后,你可能已经被网络好的玩家击杀了。

 

这里借用守望先锋的GDC分享展示快照同步

 

2.《星际围城:部落》引擎中的网络架构 (The TRIBES Engine Networking Model)

IdSoftware自2012年以来已经陆续把Quake以及Doom相关的源码上传到了GitHub上面[4]。如果你看过其中Quake的源码,会发现整个网络的架构还是比较简单清晰的,博主FABIEN SANGLARD就在网上分享了关于Quake源码的剖析[5](还有很多其他项目的源码剖析)。

 

 

//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游戏系列。

The TRIBES Engine可以认为是第一个实现状态同步的游戏引擎,《星际部落:围攻》也可以认为是第一个比较完美的实现了状态同步的游戏。

下图是该引擎的网络架构[6]:

 

平台数据包模块(Platform Packet Module)可以理解成被封装的Socket模块,连接管理器(Connection Manager)处理多个客户端与服务器的连接,流管理器(Stream Manager)负责将具体的数据分发到上面的三个高级管理器。

Ghost管理器:负责向客户端发送需要同步对象的状态信息,类似属性同步。

事件管理器:维护事件队列,每个事件相当于一个的RPC。

移动管理器:本质上与事件管理器相同,但是由于移动数据的需要高频的捕捉和发送,所以单独封装成一个特殊的管理器。

3.客户端预测与回滚(Client-side prediction and Rollback)

《毁灭公爵》是上世纪90年代一个经典的FPS游戏系列,首部作品的发布时间与Doom几乎相同,网络架构也极为相似。在1996年发布的《毁灭公爵3D》里面,为了提高客户端的表现与响应速度,他放弃了“Dumb客户端”的方案并首次采用客户端预测来进行优化(这里主要指移动预测)[7]。即在服务器确认输入并更新游戏状态之前,让客户端立即对用户输入进行本地响应。由于这种方式可以大大的降低网络延迟所带来的困扰,很快的Quake也开始参考对网络架构进行的大刀阔斧的修改。在1997年发布的更新版本QuakeWorld里面[8][9],Quake添加了对互联网对战的支持以及客户端预测等新的内容。

关于预测,其实就是本地先执行,所以并不需要什么特别的算法,反倒是预测后的客户端与服务器的同步处理有很多值得优化的地方。由于玩家的行为是没办法完全预测的,所以你不知道玩家会在什么时候突然停下或者转弯,所以经常会发生预测失败的情况。

如果玩家本地的预测结果与服务器几乎一致,那么我们认为预测是成功且有效的,玩家不会受到任何影响,可以继续操作。反之,如果客户端结果与服务器不一致,我们应该如何处理呢?这里分为两种情况。

一. 在没有时间戳的条件下,收到了一条过时的服务器位置数据。你在本地的行为相比服务器是超前的,假如你在time=10ms的和time=50ms时候分别发送了一条指令。由于网络延迟的存在,当你已经执行完第二个指令的时候才收到服务器对第一条指令的位置同步。很明显,我们不应该让过时的服务器数据来纠正你当前的逻辑。解决方法就是在每个指令发出的时候带上他的时间戳,这样客户端收到服务器反馈的时候就知道他处理的是哪条指令信息。

二. 假如我们在指令里面添加了时间戳的信息,并收到了一条过时的服务器位置数据。在上一篇文章里我们提到了TimeWarp算法,即当一个对象收到了一个过去某个时刻应该执行的事件时,他应该回滚到那个时刻的状态,并且回滚前面所有的行为与状态(包括取消之前行为所产生的事件)。这个时候我们可以用类似的方法在本地进行纠正,大体的方案就是把玩家本地预执行的指令都记录好时间戳并存放到一个MOVE BUFFER列表里(类似一个滑动窗口)。如果服务器的计算结果与你本地预测相同,可以回复你一个ACKMOVE。如果服务器发现你的某个移动位置有问题时,会把该指令的时间戳以及正确的位置打包发给你。当你收到ACKMOVE的时候,你可以把MOVE BUFFER里面的数据从表里面移除,而当你收到错误纠正信息时就需要本地回滚到服务器指定的位置同时把错误时刻后面MOVE BUFFER里面的指令重新执行一遍。这里读者可能会产生一个疑问——为什么不直接拉回?因为这时候他想纠正的是之前的错误而不是现在的错误,如果简单的拉回就会让你觉得被莫名其妙的拉回到以前的一个位置。同时,考虑到已经在路上的指令以及后续你要发送的预测指令,会让服务器后续的校验与纠正变得复杂且奇怪,具体流程细节可以参考下图。另外,Gabriel Gambetta博主在他的文章中,也对这种情况进行了简单的分析[10]。

 

关于TimeWarp算法的补充:Timewarp技术最早出现于仿真模拟中[11],我们可以认为这些仿真程序中采用的是“以事件驱动的帧同步”。也就是说,给出一个指令,他就会产生并触发多个事件,这些事件可能进而触发更多的事件来驱动程序,同理取消一个过去发生的事件也需要产生一个新的取消事件才行。这样造成的问题就是回滚前面的N个操作,就需要产生N个新的对抗事件,而且这N个事件还需要发送到所有其他的客户端执行。如果这N个事件又产生了新的事件,那么整个回滚的操作就显得复杂了很多。换成前面移动的例子来解释一下,就是客户端收到服务器的纠正后,他会立刻发送回滚命令告诉(P2P架构下)所有其他客户端,我要取消前面的操作,然后其他客户端在本地也执行回滚。而在如今的CS架构状态同步的方式下,服务器可能早就拒绝了客户端的不合法行为,所以并不需要处理回滚(同理,其他客户端也是)。所以严格来说,TimeWarp技术以及优化后的BreathTimeWarp技术[12]都是针对“以事件驱动的帧同步”,并不能与预测回滚这套方案完全等价。当然,随着时间的推移,很多概念也变的逐渐宽泛一些,我们平时提到的时间回溯TimeWarp技术大体上与快照回滚是一个意思的。

 

4.事件锁定与时钟同步(Event Locking and Clock Synchronization)

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等人提出了“消除高阶的流式时间同步”,流程如下:

1. 客户端把当前本地时间附在一个时间请求数据包上,然后发送给服务器

2. 服务器收到以后,服务器附上服务器时间戳然后发回给客户端

3. 客户端收到之后,用当前时间减去发送时间除以2得到延迟。再用当前时间减去服务器时间得到客户端和服务端时间差,再加上半个延迟得到正确的时钟差异 delta=(Currenttime - senttime)/2

4. 第一个结果应该立刻被用于更新时钟,可以保证本地时间和服务器时间大致一致

5. 客户端重复步骤1至3多次,每次间隔几秒钟。期间可以继续发送其他数据包的,但是为了结果精确应该尽量少发

6. 每个包的时间差存储起来并排序,然后取中位数作为中间值

7.丢弃和中间值偏差过大(超出一个标准偏差,或者 超过中间值1.5倍)的样例,然后对剩余样例取算术平均

上述算法精髓在于丢弃和中间值偏差超过一个标准偏差的数值。其目的是为了去除TCP中重传的数据包。举例来说,如果通过TCP发送了10个数据包,而且没有重传。这时延迟数据将集中在延迟的中位数附近。假如另一个测试中,如果其中第10个数据包被重传了,重传将导致这次的采样在延迟柱状图中极右端,处于延迟中位数两倍的位置。通过直接去掉超出中位数一个标准偏差的样例,可以过滤掉因重传导致的不准确样例。(排除网络很差重传频繁发生的情况)

 

5.插值技术 (Interpolation and Extrapolation )

插值技术在早期的帧同步就被应用到游戏里面了。或者说更早的时候就被应用到军事模拟,路径导航等场景中。插值分为内插值[15]( interpolation )以及外插值[16](extrapolation,或者叫外推法)两种。内插值是一种通过已知的、离散的数据点,在范围内推求新数据点的方法(重建连续的数据信息),常见于各种信号处理和图像处理。在这篇文章中,我们指根据已知的离散点在一定时间内按照一定算法去模拟在点间的移动路径。内插值具体的实现方法有很多,如

  • 片段插值(Piecewise constant interpolation)
  • 线性插值(Linear interpolation)
  • 多项式插值(Polynomial interpolation)
  • 样条曲线插值(Spline interpolation)
  • 三角内插法(trigonometric interpolation)
  • 有理内插(rational interpolation)
  • 小波内插(wavelets interpolation)

 

多项式插值与线性插值对比

 

外插值,指从已知数据的离散集合中构建超出原始范围的新数据的方法,也可以指根据过去和现在的发展趋势来推断未来,属于统计学上的概念。与外插值还有一个相似的概念称为DeadReckoning(简称DR),即导航推测。DR是一种利用现在物体位置及速度推定未来位置方向的航海技术,属于应用技术方向的概念。DR的概念更贴近游戏领域,即给定一个点以及当前的方向等信息,推测其之后的移动路径,外推的算法也有很多种,

  • 线性外推(Linear extrapolation)
  • 多项式外推(Polynomial extrapolation)
  • 锥形外推 (Conic extrapolation)
  • 云形外推 (French curve extrapolation)

在游戏中,一般按照线性外推或匀变速直线运动推测即可。不过,对于比较复杂的游戏类型,我们也可以采用三次贝塞尔曲线、向心Catmull-Rom曲线等模拟预测。

总之,无论是内插值还是外插值,考虑到运算的复杂度以及表现要求,游戏中以线性插值、简单的多项式插值为主。

应用:

早期的lockstep算法中,在一个客户端在收到下一帧信息前,为了避免本地其他角色静止卡顿,会采用外插值来推断其接下来一小段时间的移动路径[17][18]。普通DR存在一个问题(参考下图),t0时刻其他客户端收到了主机的同步信息预测向虚线的方向移动,不过主机客户端却开始向红色路径方向移动,等其他客户端在t1时刻收到同步信息后会被突然拉倒t1'的位置,这造成了玩家不好的游戏体验。为了解决从预测位置拉扯到真实位置造成的视觉突变,我们会增加一些相应的算法来将预测对象平滑地移动到真实位置。

 

 

在状态同步中,由于客户端每次收到的是其他的角色的位置信息,为了避免位置突变,本地会采用内插值来从A点过度到B点。插值的目的很简单,就是为了保证在同步数据到来之前让本地的角色能有流畅的表现。

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信息来进行持续的模拟。

最后,再次拿出虚幻引擎的网络同步模型来展示当今的状态同步。

到此帧同步和状态同步的发展历史讲述基本完结,下篇会继续和大家谈谈物理同步、常见同步优化技术等内容。

[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]

[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]

 

 

 

编辑于 2020-07-28

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值