目录(终篇):
六.TCP VS UDP
七.常见同步优化技术
1.表现优化
- 插值优化
- 客户端预先执行+回滚
2.延迟对抗
- 延迟补偿
- 命令缓冲区
- 通过具体的实现技巧
3.丢包对抗
- 使用TCP
- 冗余的UDP
4.带宽优化
- 同步对象裁剪
- 分区、分房间
- 数据压缩与裁剪
- 减少遍历等其他手段
5.帧率优化
- 提升帧率
- 保持帧率稳定
- 计算压力分担
八.总结
上一篇文章我们分析了物理同步的概念以及实现手段,这篇是该系列的最后一篇(完结撒花)。主要针对网络协议以及常见优化技术两个方面做分析和总结。
六、TCP VS UDP
网络同步本质是数据的传输,当逻辑层面优化已经不能满足游戏的即时性要求时,我们就不得不考虑更深一层协议上的优化,而这件事开发者们从上世纪90年代就开始尝试了。
按照OSI模型(Open System Interconnection Model),我们可以将计算机网络分为七层。一般来说,我们在软件层面(游戏开发)最多能干涉的到协议就是传输层协议了,即选择TCP还是UDP。网上关于TCP和UDP的文章与讨论有很多[15],这里会再帮大家梳理一下。
▷▶TCP处理流程图
TCP(Transmission Control Protocol),即传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议[16]。该协议早在1974年就被提出并被写进RFC(Request for Comments)中,自发布几十年来一直被不断优化和调整,如今已经是一个包含“可靠传输”,“拥塞控制”等多个功能的协议了(RFC 2581中增加)。
在21世纪早期,我们因特网上的数据包有大约95%的包都使用了TCP协议,包括HTTP/HTTPS,SMTP/POP3/IMAP、FTP等。当然,也包括一大部分网络游戏。我们如此偏爱于TCP就是因为他从协议层面上提供了许多非常重要的特性,如数据的可靠性、拥塞控制、流量控制等。这些可以让软件应用的开发者们无需担心数据丢失、重传等细节问题。
然而在游戏开发中,这些特性却可能是网络同步中的负担。在FPS游戏中,玩家每帧都在移动,我们期望这些数据在几毫秒内就能送达,否则就会对玩家产生干扰、影响游戏体验。因此对于FPS、RTS这种要求及时响应的游戏,TCP协议那些复杂的机制看起来确实有点华而不实。
▷▶客户端位置要落后一些,与服务器有一定误差(守望先锋)
考虑到TCP协议非常复杂,这里只从几个关键的点来谈谈他的问题[17]。
-
1.在TCP中,数据是通过字节流的方式发送的,但由于建立在IP协议上必须将字节流拆分成不同的包,默认情况下协议会将你的数据包缓冲,到达一定值才会发送。这样可能会出现在游戏某个阶段你最后几个包明明已经执行了发送逻辑,但是由于缓冲机制被限制而无法到达。不过好在我们可以通过TCP_NODELAY来设置TCP立即刷新写入它的所有数据。
-
2.其次,TCP的可靠数据传输响应并不及时。一旦数据包发生丢失或乱序,那么接收方就会一直等待这个数据的到来,其他新收到的数据只会被缓存在协议层,你在应用层根本获取不到任何数据也无法做任何处理。这个时候你可能要等超时重传机制响应后才能拿到重发的数据包,这时候可能已经过了几十毫秒。即使TCP拥有快速重传机制,仍然达不到理想的延迟效果。
-
3.拥塞控制和流量控制不可控。TCP在网络环境比较差的条件下,会通过调整拥塞控制窗口大小来减少包的发送来降低吞吐量,这对于延迟敏感的游戏完全是无法接受的。同样,我们在应用层上面也无能为力。
-
4.其他的还有一些的小问题,比如每个TCP的报头都需要包含序列号、确认号、窗口等数据结构,无形中增加了流量大小;TCP需要在端系统中维护连接状态,包括接收与发送缓存、拥塞控制参数等,在处理大量连接的消息时也更为繁琐和耗时。
那么这些问题能解决么?也许能,但是从协议层面我们无能为力,因为TCP协议设计之初就不是为了及时响应,而另一个运输层协议UDP看起来比较符合我们的理念。
UDP(User Datagram Protocol),即用户数据包协议,是一个简单的面向数据报通信的协议[18]。该协议由David P. Reed在1980年设计并写入RFC 768中。顾名思义,UDP设计之初就是为了让用户可以自由的定义和传输数据,不需要建立链接、没有流量控制也没有拥塞控制,但是会尽可能快的将数据传输到目的IP和端口。
▷▶UDP报头
在上世纪90年代,Quake等游戏就开始使用UDP协议取代TCP进行数据同步,结果也很理想。除了游戏外,其他诸如视频、语音通信等领域也在广泛使用UDP,开发者们开始基于UDP创建自定义的Reliable UDP通信框架(QUIC、WbRTC、KCP、UDT等[19]),一些游戏引擎(如UnrealEngine)也将RUDP集成进来。随着网络带宽的提高,使用UDP代替TCP目测是一个趋势(参考Http3[20])。
▷▶虚幻引擎的UDP数据包,包含ACK标记位
虽然UDP很自由,但是需要开发者们自己写代码完善他。我们需要自己去写服务器客户端建立链接的流程,我们需要手动将数据分包,我们还需要自己实现应用层面的可靠数据传输机制。另外,UDP还有一个传输上的小劣势——当路由器上的队列已满时,路由器可以根据优先级决策在丢弃TCP数据包前先丢失UDP,因为他知道TCP数据丢失后仍然会进行重传。
总的来说,对于那些对延迟很敏感的游戏,UDP的传输模式更加适合而且弹性很大,同时他也可以胜任那些同步频率比较低的游戏,但是UDP的开发难度比较高,如果是自己从零开发确实有相当多的细节需要考虑,所以建议大家在已有的RUDP框架上进行优化。
七、常见同步优化技术
梳理完同步的发展历史,我们最后再来总结一下常见的网络同步优化技术。首先,提出一个问题,网络同步优化到底是在优化什么?
在单机游戏中,从我们按下按键到画面的响应中间经历了输入采样延迟、渲染流水线、刷新延迟、显示延迟等。而在一个网络游戏中,从我们按下按键到另一个机器收到指令,则会经历一个极为耗时的网络延迟(相比之下,单机的延迟可以忽略不计)。网络延迟其实也包括处理延迟、传输延迟(主要延迟)、排队延迟以及传播延迟,一般我们会将这些延迟统称为网络延迟,我们优化的目的就是想尽各种办法降低或是抵消掉这个延迟。
▷▶单机延迟
数据从客户端传输到服务器的一个来回称为一个RTT。在CS架构下,其实每个客户端的行为一直是领先于服务器1/2个RTT的,数据从客户端发送到服务器有一个1/2的RTT延迟,服务器处理后通知客户端又有一个1/2的RTT延迟。P2P架构下,由于没有权威服务器,我们可以省去1/2的 RTT延迟,但是在目前的网络游戏中,为了对抗作弊行为以及容纳更多的玩家,我们不得不采用CS架构。
▷▶网络延迟RTT示意
由于在网络游戏中,延迟是不可避免的,所以我们的优化手段就是如何减小这个延迟以及如何让玩家感受不到延迟。下面我会从表现优化、延迟对抗、丢包对抗、带宽优化以及帧率优化这几个方面来做一下总结,
1.表现优化(弱化玩家对延迟的感受):
a.插值优化
内插值的目的是解决客户端离散信息更新导致的突变问题,外插值的目的是解决网络延迟过大或者抖动导致间歇性收不到数据而卡顿的问题,两种方案并不冲突,可以同时采用。在具体应用时,我们可以使逻辑帧与渲染帧分离(参考 王者荣耀技术总监复盘),这样在客户端没有收到新数据的时候还可以继续更新渲染位置(只对渲染的模型位置信息进行插值)。
b.客户端预先执行+回滚
预测的目的是让玩家能在本地操作后立刻收到反馈,提升游戏体验,回滚是为了保证服务器的权威性。客户端预测包括位置预测以及行为预测两种,位置预测需要高频率的执行,因为移动在每帧都可能发生,而其他行为预测则相对低频一些,包括开枪、扔手雷、释放技能等。另外,对于延迟不太敏感的游戏(比如MMO),可以放宽校验条件(超过一定误差再纠正),这样即使降低服务器帧率客户端也不会有什么感觉。
▷▶仔细可以看出来本地的角色响应更快,同时利用插值解决位置突变问题
2.延迟对抗(弱化玩家对延迟的感受):
a.延迟补偿
服务器(注意是服务器而不是客户端)记录一段时间内所有玩家的位置历史,在发生伤害计算时根据延迟对所有玩家角色进行位置的回滚与处理,可以尽量还原当时的场景。
▷▶角色身后的线表示延迟补偿回退的位置信息
b.命令缓冲区
把远端的数据缓存在一个buffer里面,然后按照固定频率从buffer里面取,可以解决客户端卡顿以及网络抖动问题。不过缓冲区与延迟是有冲突的,缓冲区越大,证明我们缓存的远端数据就越多,延迟就越大。
▷▶守望先锋采用的Inputbuffer
c.从具体实现的技巧上对抗延迟
客户端操作加一个前幺时间,释放技能等行为前有一个播放动画表现的时间来抵消掉同步RTT的延迟。比如角色释放无敌技能在进入无敌状态前做一个过度动画,客户端播放动画后进入无敌,但是服务器可以在收到指令后直接进入无敌状态从而抵消延迟。在游戏Halo中,有很多类似的例子,如在客户端玩家扔手雷的时候,我们可以在本地立刻播放扔手雷的动画并发送请求到服务器,然后服务器收到后不需要播放动画立刻生成手雷并同步,这样客户端真正扔出手雷的表现就是0延迟的。
▷▶光环中采用的扔手雷方案
3.丢包对抗(弱化玩家对延迟的感受):
a.使用TCP而不是UDP
由于TCP不会丢包,对于延迟不敏感的游戏还是优先采取TCP来对抗丢包
b.冗余UDP数据包
一次性发送多个帧的数据来对抗丢包。UDP同步数据时经常容易丢包,我们虽然可以使用上层实现UDP的可靠性,但是像帧同步这种同步数据量比较小的游戏可以采用冗余UDP的方案,即后续的UDP包会冗余一定量的前面已发送的UDP包,这样即使丢失了部分包我们也能保证拿到完整的远端数据。
注:王者荣耀、守望先锋、火箭联盟等等游戏都使用类似的方案,该方案不仅仅适用于帧同步
4.带宽优化(减小延迟):
带宽优化的目的是减小客户端以及服务器的同步压力,避免大量数据同时传输造成处理不过来,排队甚至是丢失。带宽优化是非常灵活且多变的,我们需要根据游戏的玩法来调整我们的优化行为。
a.同步对象裁剪
核心目的是根据相关性剔除那些不需要同步的对象(这里都是指在同一个服务器内),比如一个玩家距离我很远,我们的行为彼此不会影响,所以就不需要互相同步对方的数据。裁剪方式有非常多,常见的SOI(Spheres of Influence),静态区域(把场景划分为N个小区域,不在一个区域不同步),视锥裁剪(更多用于渲染),八叉树裁剪等。相关性还可能会涉及到声音等其他因素,需要根据自己项目来决定。
这里着重提一点AOI ( Area Of Interest [21][22]) ,即根据玩家的位置维护一个动态的视野列表,视野外的对象会被完全忽略(能大幅的减少同步对象的遍历与比较)。其基本思想也是判断相关性,实现方式有很多,其中基于格子的空间划分算法是网络游戏中常见的实现方案。在虚幻引擎中,大世界同步框架ReplicationGraph[23]的核心思想也是如此。不过要注意的是,对于MMO这种可能有大量角色同时进行连续移动的游戏,视野列表频繁的增删查操作也可能对服务器造成一定的压力。
▷▶虚幻中的ReplicationGraph方案,宝箱会被添加到附近的格子里面
b.分区,分房间
对于大型MMO来说,这是常见的手段,将不同的玩家分散到不同的场景内(不同的服务器),这样减小服务器处理数据的压力,减小延迟。对于大世界游戏而言,不同服务器可能接管同一个地图不同区域的服务,其中的跨服数据同步比较复杂。
c.数据压缩与裁剪
坐标与旋转是我们常见的同步内容,但是很多数据其实是不需要同步的。比如对于大部分3D游戏角色的Pitch以及Roll是不会改变的,我们只要同步Yaw值即可。对于非第一人称游戏,我们可以接着把四个字节float类型的Yaw压缩到两个字节的uint16里面,玩家根本不会有什么体验上的差异。类似的方法可以应用到各种同步数据里面。
此外,在状态同步里面,我们可以采用增量发送来减少数据量,即第一次发送完整的数据信息后只发送哪些发生过变化的数据,这可以大大减少网络同步的流量。
可以搜一下这两篇文章:《Exploring in UE4》网络同步原理深入(下)
以及《守望先锋》回放技术-阵亡镜头、全场最佳和亮眼表现 分别讲解了虚幻引擎的属性同步系统以及守望的回放增量同步处理
d.减少遍历以及更细力度的优化
在Halo以及虚幻引擎里面都会对同步对象做优先级划分,发送频率调整等。在状态同步中,我们还需要合适的手段来快速定位发生变化的数据,如属性置脏、利用反射减少非同步属性的遍历等。进一步的,我们还可以根据客户端的类型以及信息作出更细致的同步信息过滤以及设置优先级,比如对同步属性进行优先级划分等(目前还没有见到过粒度如此细致的,但理论上是可行的)。
5.帧率优化(减小延迟):
帧率优化是一个重要且复杂的难题,涉及到方方面面的技术细节,这里主要针对网络同步相关内容做一些分析。
相比单机游戏,网游需要同时考虑客户端与服务器的帧率,这并不是单纯地提升帧率的问题,如何优化与平衡是一个很微妙的过程。
a.提升帧率
这个不用多说,帧率低就意味着卡顿,玩家的体验就会很差。不同游戏的性能瓶颈都可能不一样,包括内存问题(GC、频繁的申请与释放)、IO(资源加载、频繁的读写文件,网络包发送频率过大,数据库读取频繁)、逻辑问题(大量的遍历循环,无意义的Tick,频繁的创建删除对象,过多的加锁,高频率的Log)、AI(寻路耗时[24])、物理问题(复杂模拟,碰撞次数过多)、语言特性(脚本语言比较费时)等,客户端相比服务器还有各种复杂的渲染问题(Drawcall太多,半透明,动态阴影等)。这些问题需要长期的测试与调试,每个问题涉及到的具体细节可能都有所不同,需要对症下药才行。
▷▶虚幻引擎中的性能数据展示
b.保持帧率稳定与匹配
假如你的客户端与服务器帧率已经优化到极致,你也不能任其自由变化。首先,要尽量保持服务器的帧率稳定(减少甚至是消除玩家比赛时的所有潜在的卡顿问题),考虑一款对延迟比较敏感的射击游戏,如果你的客户端在开枪时遇到了服务器卡顿,那么就可能造成校验失败,导致客户端看到的行为与服务器行为不一致。其次,还要保持客户端与服务器的帧率匹配。对于延迟不敏感的游戏,考虑到玩家的体验以及服务器的压力,客户端的帧率可以高于服务器多倍,但是这个比例是需要通过实际的测试来调整。而对于延迟敏感的游戏,我们一般需要尽量让服务器的帧率接近客户端,这样服务器才能更及时的相应,减少延迟带来的误差。此外,我们也不能让客户端的帧率无限提高,对于某些同步算法,客户端与服务器过高的帧率差异可能造成不断的拉回卡顿。所以,很多游戏会采取锁帧的方式来保证游戏的稳定性。
c.计算压力分担
对于MMO这种服务器压力比较大的游戏,我们通常会考虑把一部分计算资源转交给客户端去计算(甚至是计算后再返还给服务器),比如物理运算、自动寻路、AI逻辑计算等。其实将这种方式使用到极致的例子就是帧同步,服务器只做一些简单的校验即可。
总的来说,网络同步优化是一个长期的不断试错的过程,我们需要合理的利用计算机资源,把最重要的资源用在最重要的功能上面,减少重复的计算与流程,并需要配合一些经验和技巧来规避那些不好解决的问题。
八、总结
▷▶历史上“帧同步”和“状态同步”的网络游戏对比
▷▶“帧同步”和“状态同步”的使用场景与特点对比
我们从最开始的网络游戏架构谈起,按照时间线梳理了近几十年“帧同步”与“状态同步”的发展历程,并讲述了各种同步技术以及优化方案。虽然网络同步是游戏中的技术,但其本质还是计算机数据的同步。无论是Lockstep还是TimeWarp,最初都是用于计算机系统通信的技术,只不过应用场景从一台机器的内部通信转变为多台机器的通信,从传统的应用转移到网络游戏上面。
游戏的类型会影响到网络同步的解决方案,也会影响到项目的整体架构,所以我们在制作一款网络游戏前要事先做好需求分析并确定网络同步方案。同时也要意识到,网络同步延迟是不可消除的,除了算法层面的优化外还可以从实现技巧上来规避一些难题。
到此,历时半年多的网络同步系列终于迎来完结。不过网络技术还在进步,历史也还在前行,让我们一同继续关注同步技术的发展和变化,期待未来的游戏世界。