我是靠谱客的博主 文艺鞋垫,这篇文章主要介绍Source 引擎网络通信原理,现在分享给大家,希望可以做个参考。

 


        基于Source引擎的多人联机游戏使用主从式(客户端-服务器)网络构架。服务器通常指运行游戏的专用主机,客户端指连接到服务器的玩家电脑。客户端与服务器之前通过发送数据包(又称封包)来通信(频率很高,通常每秒20-30个封包)。客户端从服务器接收到当前“世界”的状态后,据这些数据来创建视频和音频输出。客户端同样也从输入设备(键盘、鼠标、麦克风等)采集数据并发回服务器进行进一步处理。客户端仅仅与服务器进行着通信,而不与其他客户端通信(即非P2P)。与单机游戏相比,多人联机游戏需要解决各种各样的通信问题。

        网络带宽是有限的,所以服务器无法在每次“世界”发生改变时发送数据给所有客户端。实际上服务器是按照一定的频率对“世界”进行快照,然后再广播给客户端的。封包在传送中也需要一定的时间(即Ping)。这意味着客户端状态总是滞后于服务器的。而且客户端将封包发回服务器又需要时间,服务器对客户端命令的处理就会有延时。每一个客户端的网络情况又不同。这一系列的时间差产生了许多逻辑问题。在快节奏的动作游戏中,每一毫秒的延迟都会造成顿卡顿卡的感觉,自然玩家玩起来也非常艰难了。除了带宽限制和网络延迟问题,封包在传输过程中也存在丢失的危险(丢包)。
 

pic1.jpg



        Source引擎采用了多种技术方案来解决由封包通信引起的问题(至少要让玩家感觉不出这些问题)。这些技术包括压缩数据法、插值法、预估法和滞后补偿法。各种技术系统之间结合紧密,某一系统的变化可能影响其他系统。本文将会讲述各系统的功能和如何它们协同工作。

基础网络设计

        服务器是按照一定的频率刷新游戏状态的。默认情况下刷新间隔为15ms,即每秒刷新率为66.666...,不过每个基于Source引擎的游戏(称之为“Mod”)可以设定其自己的刷新率。在每次刷新时,服务器会处理客户端输入的玩家指令,进行一系列的物理模拟计算、检测游戏规则、更新所有实体的状态等等。在刷新完一次后,服务器会决定是否向所有客户端发送“世界”状态,判断是否有必要对“世界”进行一次快照。当设定为比较高的刷新率时,服务器的模拟精度会提高,但也要为此付出高CPU和带宽消耗的代价。服务器管理员可以自行设定刷新率,方法是在命令行里加入 -tickrate 参数。但是我们不推荐通过这种方式设定刷新率,因为Mod在其他刷新率下可能不会按照当初设计的程序运行。
提示:-tickrate 参数在CSS、TF2、L4D和L4D2已取消,因为改变刷新率会造成服务器计时出错。在CSS和TF2里刷新率为66,在L4D和L4D2里刷新率为30。

        客户端带宽通常十分有限,最糟糕的情况是客户端仍在使用调制解调器(Modem,猫),最大速度只有5-7KB/s。如果服务器尝试以高速率发送封包时,丢包将会不可避免的出现。因此,客户端需要告诉服务器它的下行带宽能力,即控制台变量rate,单位为字节/秒(bytes/s)。这是客户端中最重要的网络设定量,想要提升游戏体验务必要设定合适的rate值。客户端也可以申请一个特定的接收快照的频率,方法是修改cl_updaterate的值(默认为20),但是即使把它的值修改的再高,服务器向此客户端发送快照的频率最大也不会超过服务器设定的刷新率,当然发送的数据量也不会超过客户端设定rate值。服务器管理员可以限制数据传输速率(即限制向每个客户端的上传速度),方法是设定sv_minrate(最小速率)和sv_maxrate(最大速率)的值(单位均为bytes/s)。同理,服务器也可以限制快照发送速率,方法是设定sv_minupdaterate(最小速率)和sv_maxupdaterate(最大速率)的值(单位均为快照数/秒)。

        客户端按照相同的刷新率从输入设备采集数据,并以玩家指令(User Command)的形式进行处理。玩家指令其实就是当前鼠标和键盘状态的一个快照。客户端并不会把每一条玩家指令都发送到服务器,而是按照一定的刷新率发送封包(通常是30)。也就是说同一个封包里可能包含着2条以上的玩家指令。客户端可以使用控制台命令cl_cmdrate来提高此刷新率,但是占用更多的上行带宽。

        游戏数据经过差值压缩法进行过处理,以减少带宽占用。所谓差值压缩法,你可以理解为服务器无需每次都发送完整的“世界”快照,而只发送与上一幅快照相比有变化的部分。服务端与服务器之间的每个封包都经过编号以实现对数据流的追踪。一般而言,完整的快照只在游戏开始或者客户端经过持续数秒的大丢包(LOSS)后才发送的。客户端可以使用控制台命令cl_fullupdate来手动申请接收一次完整的快照。

        很显然可以看出,从输入设备输入到反映到游戏世界的延时是由很多因素决定的,包括服务器、客户端的CPU负载、游戏刷新率(tickrate)、传输速度、快照接收刷新率等等,但最重要的还是封包传输延迟。延迟(Ping)是指从客户端发送一条玩家指令,服务器接收并响应,客户端收到服务器响应所用的时间。延迟低,意味着玩家可以有很好地游戏体验。预估法和滞后补偿法等技术为游戏提供了一个公平的环境。对网络进行适当的设置可以更好地提升游戏体验,但是我们还是强烈推荐默认设置。不恰当地设置反而会造成反面影响。

实体插值法

        客户端默认每秒接收20幅快照,如果实体仅仅按照从服务器接收到的那点点位置、状态信息来渲染的话,那么像那些移动的对象、动作会神经兮兮地顿卡顿卡的。丢包同样也会造成明显的卡机。如何解决这个问题呢?其实还要回过头来,用实时渲染方式来解决,这样在每两幅快照之间会不断插入由客户端计算生成的实体位置、动作信息,以实现平稳过渡。按照每秒接收20幅快照来算,每隔50毫秒就有一幅快照抵达,如果令客户端推迟50毫秒再渲染接收到的快照,这样就可以在最后接收的2幅快照之间插入计算生成的信息了。

        Source引擎的默认插值周期(lerp)为100毫秒(cl_interp 0.1),这样的话,如果有1幅快照接收失败,依然会有2幅有效的快照会插入其中,如果不理解,请看看下面的时间关系图:
 

pic2.jpg



        客户端接收到的最后一幅快照是10.30秒处的第344帧,而客户端当前时间增加。当一个新的游戏图像帧渲染好时,客户端实际时间已经为10.32秒了,因为插值计算过程使之延迟了0.1秒(称为插值延时)。在例子中,10.22秒处的这幅画面其实是根据对第340帧和第342帧的插值计算结果来渲染的。

        因为我们设定了100毫秒插值延时,所以当第342帧由于丢包未接收失败时,插值计算仍会进行。当然此时就会根据第340和344帧来进行计算了。但是如果超过一幅快照接收失败时,插值法就不能完美地工作,这是因为缓存的快照不够用了;在这种情况下,渲染引擎使用外推法(cl_extrapolate 1)来渲染画面,即从之前收到的实体信息中进行简单的线性推断。但是在发生丢包后,这种推断计算仅持续0.25秒(cl_extrapolate_amount),否则预测误差会太大,毫无意义。

        从上面可以看出,即使你把客户端和服务器运行在同一台机器(称为监听服务器)上,都会有一个由实体插值法造成的恒定的延迟(可以说是lag),默认为100毫秒(cl_interp 0.1。但这并不是说玩狙击枪时都要有预瞄量,服务端会用滞后补偿法(下面会讲解到)来修正这个问题。
提示:近期发布的大多数Source游戏都支持cl_interp_ratio命令,用这个命令你可以很轻松地把cl_interp设为0,而增加cl_updaterate的值。用net_graph 1可以查看到最终的插值周期(lerp)。
注:如果你打开了sv_showhitboxes(在Source 2009中不可用),你能看到按服务器时间绘制的有效射击区(hitboxes),正因为有插值延时的影响,它会显示在玩家模型的前方。这个现象是完全正常的。


输入预估法

        我们假设有名延迟为150ms的玩家开始向前移动,“+FORWARD键被按下”被存储在了一个玩家指令中,然后发送给服务器,服务器会将该指令处理为不断变化的位置码,于是玩家角色便向前移动起来。在下一次快照发给客户端时,“世界”状态改变才会体现出来。所以这名玩家会在他按下前行键的150ms后才能看到他走动了起来。同理,类似的延迟在射击、移动等玩家动作中均存在,对于高延迟玩家,情况会很糟糕。

        如果从玩家的输入设备开始输入到呈现在屏幕上的延迟过大,玩家会有一中很奇怪、不自然的感觉,无法精准射击。客户端输入预估法(cl_predict 1)解决了这个问题。客户端会根据玩家指令自己预估位置码,而不是等待服务器返回位置码。因此,客户端需要进行与服务器处理玩家指令所相同的计算。在预估计算完后,客户端中玩家会立即移动起来,而服务器依然认为玩家在原来的位置没有移动。

        在150ms延时后,客户端会收到服务器的新的快照。客户端会比较快照中的位置与之前的预估结果,如果不相同,那么说明预估结果错了,客户端当前的位置信息与实际有些偏差。客户端便会去修正这些错误。如果开启cl_showerror 1的话,客户端可以看到预估错误的提示信息。在修正预估错误时,玩家可以明显感觉到屏幕视野有些跳动。但是可以分步来修正错误,每次稍微修正一点,就可以实现平滑过渡了。cl_smoothtime参数指定了修正错误所用的时间。使用cl_smooth 0可以关闭平滑修正预估错误功能。

        照此来看,预估法仅在客户端完全知道服务器的规则设定、实体状态时才工作。但事实上大多数情况下并非如此,客户端仅仅知道服务器的一小部分信息,已经足够来预估对象了。预估法所产生的效果仅对本机有效。想要完全准确地预估其他玩家或者对象从根本上讲是不可能实现的。

滞后补偿法
 

所有有关滞后补偿法和实体插值法的源代码均可在Source SDK中找到。


        接着实体插值法中的例子来讲,假设玩家在10.5秒时十分精准地向目标开火,“开火”这条信息会被打包到玩家指令中发送到服务器。封包的传输过程需要一定时间,可能在10.6秒时才能抵达服务器,此时地方目标可能已经移动,封包中描述的准心指向的位置可能已经没有任何目标了。服务端方使用滞后补偿法来解决这个问题。

        滞后补偿系统会收集记录前1秒内所有附近玩家的位置信息。当一条玩家指令抵达服务器时,服务器会估算出它是何时开始执行的:

指令执行时间 = 当前服务器时间 - 封包传输时间 - 客户端插值延时


然后服务器会将所有其他玩家的位置信息(注意:仅仅是玩家,没有其他实体对象)还原为指令执行时间的状态。这样玩家指令就可以像预期一样被执行了。在对指令处理过后,其他玩家的位置会恢复为现行状态。
提示:因为客户端插值延时已经写到了等式里,所以前面讲到由插值延时造成的影响不复存在。

        在运行监听服务器时,可以打开sv_showimpacts 1,能看到服务器和客户端分别定义了不同的有效射击区(Hitboxes):
 

pic3.jpg



        上图是在监听服务器中截取的,使用net_fakelag设置了200ms的延迟。红色的有效射击区(Hitbox)显示的是客户端上100ms前的目标位置。在玩家指令的传输过程中,目标会继续向左移动。在玩家指令抵达服务器后,服务器会根据指令执行时间,重现之前的目标位置(蓝色有效射击区)。然后服务器据此来验证子弹是否击中了目标。

        客户端与服务端的有效射击区(Hitboxes)并未完全重合,这是由于时间精度误差造成的。即使几毫秒的小误差也会在快速移动的对象上放大呈现。多人联机游戏中,攻击侦测并不是通过击中某一个像素来判断的,在测算精度上会受到刷新率(tickrate)和对象移动速度的限制。可以通过增加刷新率来提高精度,但是无论对于客户端还是服务器,增加刷新率都需要消耗更多CPU资源、内存和带宽。

        太麻烦了,为什么服务器对攻击的判定这么复杂?上面说的“还原位置到历史状态”不是完全可以在客户端实现?使用像素判断不是完全可以避免时间精度误差吗?答案是:“难道能让客户端说‘我爆头了’,服务器就认为客户端真的爆头了吗?”我们不能设定的如此简单,服务器在这些重要方面不可以过度信任客户端。要知道即使客户端未被修改,而且受到VAC保护,它发出的封包依然可以在另一台机器或者路由器上修改,向其中插入“击中”的信息,便可轻易逃过VAC的侦测了(即所谓“man-in-the-middle attack”,拦截式攻击)。

        滞后补偿法,看起来十分有悖常理:即使你已经隐蔽了起来,你也有可能被敌人击中,因为服务器没有及时地把有效射击区(Hitboxes)移入隐蔽区。这些矛盾通常没办法解决,毕竟封包传送速度太慢了。你可以把现实世界中的光线当作封包来看待,因为光速太快了,你身边每一个人看到的都是一样的景象,所以你察觉不到这些问题。

网络状态监视图

        Source引擎提供了一系列网络监测工具。其中大家最熟悉的莫过于“网络状态监视图”(Net Graph,以下简称“图形”)了。使用net_graph 2(或 +graph)命令可以开启图形。接收到的封包会以“短线”的形式显示在图形右端。每条短线的高度反应了一个封包的体积大小。如果图形中出现了“缺口”(各个短线没有紧密排列起来),这意味着发生了丢包,或者收到的封包并不是预期想要的。短线颜色代表了该封包包含的数据类型。

        图形下方的第一行数据分别为:当前每秒渲染画面帧数、平均延迟、当前cl_updaterate值。第二行为:上一个封包的体积大小(字节)、平均下行带宽、每秒接收封包数。第三行与第二行含义基本相同,只是针对发送出去的封包的描述。
 

pic4.jpg



性能优化

        Source的默认网络设定已针对连接因特网服务器进行了优化,基本可以在大多数客户端、服务器上很好地工作。若要进行因特网游戏,你需要手动调整的唯一客户端参数仅有“rate”值,这个值限制了客户端通信占用的网络带宽(单位 bytes/s)。一些参考值:调制解调器 4500,ISDN 6000,DSL 10000,其他性能更好的网络可以适当调高。

        在高性能的网络环境中,如果服务器和客户端双方都有非常优秀的硬件资源,可以尝试着增加带宽占用和刷新率(tickrate)以提升游戏计算精度。增加服务器刷新率一般可以大幅提升运动和射击精度,但是需要占用更多CPU资源。使用100刷新率的服务器比使用66刷新率大约消耗1.5倍CPU资源。在CPU资源不足时,就会造成计算产生严重的延迟,特别是在许多人同时射击时,表现尤为明显。我们并不建议使用超过66的刷新率,留下些空闲CPU资源总是好的。

提示:强烈不建议更改CSS、TF2、L4D和L4D2的刷新率。更改这些游戏的刷新率会造成游戏计时错误。CSS和TF2的刷新率默认为66,L4D和L4D2为30。

        当服务器使用很高的刷新率时,客户端如果带宽足够,可以适当调高其快照刷新率(cl_updaterate)和玩家指令刷新率(cl_cmdrate)。客户端快照刷新率受到服务器刷新率的限制,举例来说,在刷新率为66的服务器上,客户端的最高快照刷新率应该设定为66。如果你的快照刷新率过高,造成偶然的loss和choke,你应该降低你的快照刷新率。在使用高快照刷新率时,你可以降低插值周期(cl_interp)。默认插值周期是0.1秒,这是根据cl_updaterate 20设计的。(译注:下面这句话自己慢慢品味。)插值系统会给移动中的玩家造成一小点优势,因为移动中的玩家比静止的玩家能提前看到敌方目标,而所能提前的时间等于插值周期。这种情况是绝对无法避免的,但是可以通过降低插值周期来减免此影响。如果两个玩家都在移动,此插值延时效果对双方都有影响,因此也就谁都没有优势了。

        快照刷新率和插值周期的关系应该如下:
Source Engine 2006(HL2DM)
        插值周期 = cl_interp_ratio / cl_updaterate

        例如,客户端每秒接收66幅快照,插值比例(cl_interp_ratio)为2,那么插值周期就是0.03s。这样你的插值周期就从 100ms减到了30ms。cl_interp在Source Engine v7上已被禁用,所以你需要用cl_interp_ratio和cl_updaterate来指定插值周期。使用cl_interpolate 0来关闭插值延时,会造成动作抖动和完全错误的攻击判定,但事实上插值延时是不准许关闭的。

Source Engine 2007 / Source Engine 2009 (TF2 - DoD S - CSS) + Left 4 Dead Engine + Left 4 Dead 2 Engine + Alien Swarm Engine
        cl_interp = cl_interp_ratio / cl_updaterate

        例如,客户端每秒接收66幅快照,插值比例(cl_interp_ratio)为2,你可以手动设置cl_interp为0.03,这样你的插值周期就从 100ms减到了30ms。此处,cl_interp_ratio仅仅限制了cl_interp的值域。在橙盒引擎中插值系统是无法关闭的。


提示与技巧
·除非你100%了解你在干什么,否则不要更改任何控制台参数
        当服务器或网络负载达到极限时,许多“高性能”的设定恰恰会起到反作用。
·切忌关闭插值系统和滞后补偿系统
        这不会对提升移动、射击精度有任何好处。
·对某一客户端的优化设定,或许不会在其他客户端上产生好的效果
        不要傻傻地用别人的设定来优化自己的性能。
·当你在SourceTV或者游戏中以第一人称方式查看别人的视角时,你看到的东西并不等于此玩家看到的东西
        滞后补偿不会对观察员起作用。

最后

以上就是文艺鞋垫最近收集整理的关于Source 引擎网络通信原理的全部内容,更多相关Source内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(49)

评论列表共有 0 条评论

立即
投稿
返回
顶部