原文
(本文的读者大部分为本公司程序员,文中有些地方保留了他们在日常工作中时常用到的英文术语。望其他读者见谅。)
基于Source Engine的多人游戏使用的是Client-Server网络架构。通常,server是指专门用来跑游戏的主服,把控world stimulation,游戏规则,玩家输入的处理。client是指连接到game server的玩家电脑。client和server通过高频率传送小的data packets(下文中简称为“包”)的方式进行沟通(通常为20-30个每秒)。client从server上接收到当前world state,然后根据更新内容生成声音和视频输出。client也会从输入设备(键盘,鼠标,话筒等)上进行数据采样,然后把样本送回server做进一步处理。clients只和game server沟通,彼此之间不沟通(和在peer-to-peer应用中一样)。与单人游戏相比,多人游戏需要处理很多基于传递数据包沟通方式而引发的问题。
由于受到网络带宽的限制,server不可能做到只要有world change,就发一个内容更新包到所有clients。所以,server会遵照一定频率给当前游戏状态拍快照然后广播给clients。网络数据包在client与server之间传送是需要时间的(ping值)。这样使得client时间总会比server时间晚一点。进而言之,来自client输入数据包传送回server也会delay,所以server是在处理暂时delay的用户指令。外加上其它背景流量和client自身frame rate影响,每个client会有不同程度的network delay。server和client之间的这些时差会导致逻辑问题,并且伴随着network latnecies的增加而更严重。在快节奏的动作游戏里,即使是微妙的延迟也让玩家感觉操作滞后,让击中对手或者与移动中的物体互动变难。除了带宽限制和网络延迟,传送过程中丢包也会导致信息丢失。
为了处理网络沟通带来的问题,Source engine server引入了data compression和lag compensation技术,对于client来说是隐形的。client是通过prediction和interpolation来进一步改善游戏体验的。
Basic networking
server在离散的时间段里模拟游戏,这些时间段被叫做ticks。默认的时间段是15ms,每一秒中有66.666...次ticks,不过mods可以指定自己的tickrate。在每个tick中,server要处理来自用户的指令,运行physical stimulation步骤,检查游戏规则,更新所有物体状态。完成一个tick模拟后,server决定哪个client需要world update,必要时会给当前的world state拍快照。高tickrate会增加stimulation精确度,但是需要CPU性能好,并且server和client有足够带宽。server管理员可能会通过the-tickrate command line parameter更改默认tickrate,但我们不推荐这种方式,因为一旦tickrate被修改了,mod可能不会按照既定方式工作了。
Note:CSS, DoD S, TF2, L4D and L4D2不支持the-tickrate 指令行参数,因为修改tickrate会导致server timing出问题。CSS, DoD S and TF2, and 30 in L4D and L4D2中tickrate被设定为66。
clients带宽通常都是有限的。最糟糕的情况是,玩家的modem的接收能力也就是5-7KB/sec。如果server是以较高的data rate给他们发送更新,就无法避免丢包。因此,client需要通过设定console variable rate (in bytes/second),把它的输入带宽能力告诉server。这是clients最重要的网络变量,为了最佳游戏体验一定要把它设定正确。client可以通过改变cl_updaterate (默认 20)来要求一定数量的快照,不过server将不会发出比stimulated ticks更多的更新,或者超出client需求的client rate。server管理员用sv_minrate和sv_maxrate(单位都是bytes/second)对clients要求的data rate values进行限制。snapshot rate也可以通过sv_minupdaterate和sv_maxupdaterate(单位是snapshots/second)进行限制。
clients按照server运行相同的tick rate,通过对输入设备进行取样来创造用户指令。一个用户指令基本就是当前键盘和鼠标状态的快照。但是client并没有为每一个用户指令向server发送一个新包,它是按照一定速率(通常是每秒钟30个包)发送命令包。这意味着两个或以上的用户指令会在同一个包中传送。clients可以用cl_cmdrate来提升command rate。虽然这样做可以增加更多的回应,但也需要更多的输出带宽。
用delta compression来压缩游戏数据有助于减少网络负荷。也就是说,server每次不是发整个world snapshot,只会把一次已知更新之后所发生的变化(a delta snapshot)发送出去。在client和server之间传输的包,都有编号,为的是可以追踪到它们的data flow。通常,server只会在游戏刚启动,或者几秒钟内client发生了严重数据包丢失,这些情况下发送全部(non-delta)快照。clients也可以用cl_fullupdate指令手动要求。
响应,也就是用户输入后到他在游戏中看到结果之间的时间,会受很多因素影响,包括server/client CPU load和stimulation tickrate,data rate以及快照更新设定,最主要的是受到network packet传输时间的影响。从client发出一个用户指令开始,到server对其做出响应,然后client接收到该响应,整个过程所耗时间,被叫做latency或者ping (or round trip time)。在进行多人在线游戏时,延迟低的用户会有突出优势。prediction和lag compensation都是为了尽可能削弱这个优势,使得网络连接质量不高的用户也能够得到比较好的游戏体验。在带宽和CPU性能允许的情况下,调节networking setting有助于获得更好的游戏体验。我们推荐保持默认设置,因为不恰当的修改可能导致糟糕的副作用。
支持Tickrate的服务器
可以用the-tickrate parameter调整tickrate
- Counter Strike: Global Offensive
- Half-Life 2: Deathmatch
以下servers tick rate不能调整,否则会导致server timing出问题。
Tickrate 66
- Counter Strike: Source
- Day of Defeat: Source
- Team Fortress 2
Tickrate 30
- Left for Dead
- Left for Dead 2
Entity interpolation
默认情况下,client每秒钟会收到20个快照。如果游戏世界中的物体(entities)只在server接收到的那些点上进行render,那么移动的物体以及动画效果会看起来不连贯。丢掉的数据包同样会引发明显的问题。解决这个问题的办法是及时返回去render,那么这些点和动画就能在最近两次接收到快照点之间,连续地被插值(be continuously interpolated)。每秒20次快照的情况下,每50毫秒就会到达一个新的更新。如果client render time能在50毫秒内shift回来,entities就能总是在上一次接收到的快照以及它之前的那次之间被插值。
默认情况下,client每秒钟会收到20个快照。如果world中的物体(entities)只在server接收到的那些位置上进行render,那么移动的物体和动画就会看起来不连贯。丢掉的数据包同样会引发明显的问题。解决这个问题的办法是及时返回去render,那么这些位置和动画就能在最近两次接收到快照点之间不断地interpolated。每秒20次快照的情况下,每50毫秒就会到达一个新的更新。如果client render time能在50毫秒内移回来,entities就能总是在最近一次接收到的快照以及它之前的那次之间interpolated。
Source默认的interpolation period (‘lerp’)是100毫秒(cl_interp 0.1); 这样,及时有一个快照丢了也能始终保证两个有效快照可供插值。下图展示了incoming world快照的到达次数:
client接收到的最后一次快照是在tick 344也就是第10.30秒。在这次快照和client frame rate基础上client time继续增加。如果render了一个新的video frame,那么rendering time是当前client time 10.32秒减去view interpolation延迟掉的0.1秒。也就是图中的10.22秒,所有entities以及它们的动画会按照修正后的时间段,在快照340和342之间被插值。
因为interpolation有100毫秒的延迟,即便快照342因为丢包的原因找不到了,interpolation也不会受影响。这时,interpolation可以用快照340和344。如果一列快照中丢失的不止一个,那么interpolation就不能顺畅执行了,因为历史buffer中的快照用光了。这种情况下,renderer会用extrapolation(cl_extrapolate 1),尝试一个简单的线性extrapolation of entities,基于到目前为止它们已知的历史记录。当丢包时间为0.25秒时就会执行extrapolation (cl_extrapolate_amount),否则prediction错误可能在此之后变得很大。
Entity interpolation通过默认(cl_interp 0.1)引发一个100毫秒constant view ”lag”,即使你是在一个listenserver(server与client在同一台机器)上玩。当你在向别的玩家射击时,server-side lag compensation会知道client entity interpolation并且修正该错误,但这不意味着你要早于瞄准。
Tip:近期很多Source games都有cl_interp_ratio cvar。有了它,你可以通过设定cl_interp为0,增加cl_updaterate 数值(由server tickrate决定的非常有用的数值限制),既简单又安全的减少interpolation period。你可以用 net_graph 1检查最终的perp。
Note: 如果你打开sv_showhitboxes(Source 2009不支持),你将会看到玩家hitboxes在server time中淹没,也就是说它们通过lerp period早于了渲染玩家模型。这是非常普通的!
Input prediction
假设一个玩家的网络延迟为150毫秒,现在他要前进。按下+FORWARD键的信息被存为一个用户指令并发送给server。用户指令通过前进代码处理后,玩家角色在游戏中前进。这个world state变化会在下一个快照更新时被传递给所有clients。所以,玩家会晚于操作150毫秒看到自己位置上的变化。这种delay会发生在玩家所有的动作上,比如移动和射击,延迟越高玩家看到的滞后越严重。
玩家输入与对应视觉效果之间的delay,让玩家产生不自然的感觉,移动和瞄准也都不是很精确。client-side的input prediction (cl_predict 1) 是一种去掉这种delay的方法,让玩家的动作感觉上更连贯。本地client自行预判用户指令结果,而不用等待server更新自己的位置。client运行的代码和规则,与server用来处理用户指令的那一套是一样的。prediction完成后,本地玩家瞬间移动到了新的位置,但在服务器看来他还在原位。
150毫秒之后,client将会收到来自server的快照,其中包括了刚才已经预判到的变化。client接着会将server发来的位置与自身预判的位置进行比较。如果不同,就是发生了一个预判错误。也就是说,当client进行用户指令处理时,没能完全掌握其它entities和environment的正确信息。那么client需要纠正自己的位置,因为server拥有更高权限。如果cl_showerror 1是打开的,clients可以看到预判错误的发生。预判错误的纠正会十分明显,可能导致client’s view失常地跳动。在短时间内逐渐修正这个错误(cl_smoothtime),效果会更平缓。cl_smooth 0就会关掉prediction error smoothing。
prediction只可能发生在本地用户身上,以及与其有关的实体上,因为prediction的工作原理是通过用户的键盘输入来做出一个用户要去哪儿的“最好的假设”。预判其它玩家,可能会在没有数据的情况下全方面预判未来,因为你不可能马上拿到其他人的键盘输入。
Lag compensation
lag compensation和view interpolation的所有source code在Source SDK中都是可行的。安装教程详见Lag compensation.
假设一个玩家在client time 10.5时,朝目标开枪。射击信息打包成用户指令发给server。数据包在网络上传输时server继续simulate the world,那个目标可能已经移到了新的位置。用户指令在server time 10.6时到达,server将不会觉察到该打击,即便玩家之前是非常准确的击中目标。这个错误会被server-side的lag compensation修正。
lag compensation系统会保留玩家一秒钟内的所有历史位置。如果执行了一个用户指令,server会估计一下它大约是在什么时间开始的,如下:
Command Execution Time = Current Server Time - Packet Latency - Client View Interpolation
接着,server把其他所有玩家-只是玩家-移回指令执行的时间点。用户指令执行并且打击被正确探测到。处理完用户指令,玩家回到原来的位置上。
Note: 因为entity interpolation包含在了公式中,调用失败会导致意外的结果。
在listen server上,你可以sv_showimpacts 1,然后看到不同的server和client hitboxes:
这张截图取自listen server上,有一个200毫秒lag(用net_fakelag),就在server确认了打击之后。红色hitbox显示了100ms+interp period之前,目标在client上的位置。那之后,目标继续向左移动,而用户指令还在向server传输中。用户指令到达之后,server根据预测的指令执行时间,存储目标位置(blue hitbox)。server跟踪射击并且确认打击(client看到目标掉血)。
client and server hitboxes 不会完全匹配,因为在时间计算上会有小的误差。及时一个小到几毫秒的差别也让快速移动的物体发生好几英寸的错误。Multiplayer hit detection不会精确到以像素为单位,基于tickrate和物体移动速度,也有精确度的上限。
问题来了,为什么hit detection在server上这么复杂?倒回去追踪玩家位置,以及当hit detection可以在client-side轻松完成的情况下处理精确度错误和处理pixel precision。
(原句太长不知道理解的对不对。Doing the back tracking of player positions and dealing with precision errors while hit detection could be done client-side way easier and with pixel precision.)
client只需要告诉server一个”hit”消息,player在哪儿打的什么。我们不可能这么简单的就通过了。因为在这些重要决定上,game server不会相信clients。即便client是”clean”的,并且受到Valve Anti-Cheat保护,数据包还是有可能在传输到game server的途中,被第三方机器修改。这些“cheat proxies”会把”hit”消息插入到网络数据包中,并且不会被VAC(a "man-in-the-middle" attack)探知到。
network latencies和lag compensation可以创造出看起来接近现实世界的paradoxes。例如,你会被一个自己根本看不到的人袭击,因为你已经took over了。如果server把你的player hitboxes移回到刚才,就是你暴露目标的地方,会发生什么?这些前后不一致的问题不会被完全解决的,因为相对较慢的packet speeds。在现实世界中,你不会注意到这些问题,因为光(相当于数据包)的传播非常快,你和周围的人看到的世界是一样的。
Net graph
Source引擎提供了很多工具可供你检查client的链接速度和质量。最受欢迎的一个是net graph,它可以通过net_graph 2 (or +graph)启动。细小的线条从右向左移动代表输入数据包。每条线的高度反映出包的大小。当线之间出现间隔时,表示数据包有丢失或者达到顺序出现了错乱。所包含的数据内容不同,线的颜色不同(color-coded)。
在net graph下,第一条线显示的是每秒钟当前渲染的帧,你的平均latency,和当前cl_updaterate值。第二条线显示的是最后一个输入包(快照)的大小(in bytes),平均输入带宽,每秒钟接收到的包。第三条线显示的是输出包(用户指令)一样的数据。
Opimizations
默认网络设置是为在Internet上的dedicated server(专用服务器)上玩游戏而配备的。这个设定平衡了大多数的client/server硬件和network configuration(网络队列),使得大家都能很好的工作。对于Internet游戏,唯一可以在client上进行调整手柄变量(console variable)是”rate”,它是定义你网络链接的可用 bytes/second的带宽。好的”rate”数值是调制解调器4500,ISDN 6000, 10000 DSL或者更高。
在性能高的网络环境中,也就是server和所有clients的硬件条件都很充裕,是有可能对带宽和tickrate设置进行调节以获得更精确gameplay。增加server tickrate通常是改进移动和射击精准度,但也会大量消耗CPU。Source server运行tickrate 100时,产生的CPU load大约是tickrate 66时的1.5倍或以上。这样会导致严重的calculation lags,尤其是在很多人同时射击的情况下。所以,我们不建议让game server运行比66还高的tickrate,要为紧急情况储备CPU资源。
Note:在CSS, DoD S TF2, L4D 和 L4D2上不能改变tickrate,因为这样做会导致server timing出问题。tickrate在CSS,DoD S和TF2中设置为66, 在L4D和L4D2中是30。
如果game server运行tickrate很高,clients就会增加快照更新频率(cl_updaterate)和用户指令频率(cl_cmdrate),当然是要在带宽(频率)允许的情况下。快照更新频率会受到server tickrate限制,server每tick送出的更新不可能超过一个。tickrate 66的server,cl_updatera最高的teclient数值也就是66。如果你提高快照频率后遇到了数据包丢失或者阻塞,那么你需要重新调低它。提高cl_updaterate同样会让你的view interpolation delay (cl_interp)变低。默认interpolation delay是0.1秒,是由默认cl_updaterate 20得来的。View interpolation delay会让移动中的玩家比固定住的玩家有一点小小的优势,因为移动中的玩家可以早一点点时间看见他的目标。这种效果是难以避免的,但是可以通过降低view interpolation delay来减少。如果两个玩家都在移动,那么view lag delay对他们的影响效果是一样的,不会有人有优势。
下面是快照频率和view interpolation delay之间的关系:
interpolation period = max( cl_interp, cl_interp_ratio / cl_updaterate )
“Max(x,y)”代表“其中哪一个高”。你可以把cl_interp设为0,也会有一个安全数量的interp。你可以通过增加cl_updaterate从而进一步减短interp period,但是不要超过tickrate(66)或者让你的网络连接承载超出它处理能力的数据量。
Tips
- 不要改变console设定值,除非你100%清楚自己在做什么
如果server或者network负载能力有限,很多“高性能”设置反而会引发负面效应。
- 不要关掉view interpolation 和/或者 lag compensation
这些功能可以增进移动或者射击的精确度。
- 对某个client进行的优化设置不一定也适用于其他clients
不要把其他clients的设置照搬到自己身上,而不做任何修改。
- 如果你是以spectator的身份在“First-Person”视角的游戏或者Source TV中跟踪别的玩家,你和他看到的不会完全一样
spectator看到的游戏世界没有lag compensation。