(翻译) 延迟补偿方法的协议设计与优化 Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization

Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization

客户端/服务器游戏中 延迟补偿方法的协议设计与优化

作者:Yahn W. Bernier
公司:Valve

2003 年

Overview: Designing first-person action games for Internet play is a challenging process. Having robust on-line gameplay in your action title, however, is becoming essential to the success and longevity of the title. In addition, the PC space is well known for requiring developers to support a wide variety of customer setups. Often, customers are running on less than state-ofthe-art hardware. The same holds true for their network connections. While broadband has been held out as a panacea for all of the current woes of on-line gaming, broadband is not an simple solution allowing developers to ignore the implications of latency and other network factors in game designs. It will be some time before broadband truly becomes adopted the United States, and much longer before it can be assumed to exist for your clients in the rest of the world. In addition, there are a lot of poor broadband solutions, where users may occasionally have high bandwidth, but more often than not also have significant latency and packet loss in their connections. Your game must to behave well in this world. This discussion will give you a sense of some of the tradeoffs required to deliver a cutting-edge action experience on the Internet. The discussion will provide some background on how client / server architectures work in many online action games. In addition, the discussion will show how predictive modeling can be used to mask the effects of latency. Finally, the discussion will describe a specific mechanism, lag compensation, for allowing the game to compensate for connection quality.

概述

为互联网环境设计第一人称动作游戏是一件很有挑战的事。不过,对于一款动作游戏来说,稳定可靠的在线玩法已经越来越成为决定其成功和生命周期的关键因素。除此之外,PC 平台还必须面对大量不同的用户环境:很多玩家使用的并不是最先进的硬件,网络连接条件也同样参差不齐。

虽然宽带常被视为解决在线游戏所有问题的“万能药”,但它并不是一个能让开发者忽略延迟和其他网络因素的简单答案。宽带在美国全面普及尚需时间,在世界其他地区更是如此。而且很多所谓的“宽带”连接虽然偶尔带宽较高,却也经常伴随着明显的延迟和丢包。

你的游戏必须在这样的现实环境中表现良好。本文会讨论为了在互联网环境中提供先进动作体验而必须做出的一些权衡,介绍许多在线动作游戏中客户端/服务器架构的基本工作方式,说明如何通过预测模型来掩盖延迟影响,并最终描述一种具体机制——延迟补偿(lag compensation)——用来让游戏适应不同的连接质量。

 

Basic Architecture of a Client / Server  客户端/服务器游戏的基本架构

Game Most action games played on the net today are modified client / server games. Games such as Half-Life, including its mods such as Counter-Strike and Team Fortress Classic, operate on such a system, as do games based on the Quake3 engine and the Unreal Tournament engine. In these games, there is a single, authoritative server that is responsible for running the main game logic. To this are connected one or more “dumb” clients. These clients, initially, were nothing more than a way for the user input to be sampled and forwarded to the server for execution. The server would execute the input commands, move around other objects, and then send back to the client a list of objects to render. Of course, the real world system has more components to it, but the simplified breakdown is useful for thinking about prediction and lag compensation.

如今大多数在网络上进行的动作游戏,都是经过修改的客户端/服务器游戏。像 Half-Life 这样的游戏,包括它的各种模组,如 Counter-Strike 和 Team Fortress Classic,都是运行在这种系统上的;基于 Quake3 引擎和 Unreal Tournament 引擎的游戏也是如此。在这些游戏中,存在一个单一的、权威性的服务器,负责运行主要的游戏逻辑。与之连接的是一个或多个“愚笨”的客户端。最初,这些客户端几乎仅仅只是采样用户输入并将其转发给服务器执行的一种方式。服务器会执行这些输入命令,移动其他物体,然后再向客户端发回一个待渲染对象列表。当然,现实世界中的系统包含更多组件,但这种简化后的分解方式有助于理解预测和延迟补偿。

With this in mind, the typical client / server game engine architecture generally looks like:

牢记这一点,典型的客户端/服务器游戏引擎架构通常如下所示:

image

 

 

For this discussion, all of the messaging and coordination needed to start up the connection between client and server is omitted. The client’s frame loop looks something like the following:

在本文讨论中,客户端与服务器之间建立连接所需的所有消息和协调过程均被省略。客户端的帧循环大致如下:

Sample clock to find start time                      1.采样时钟以找到开始时间       【当前本机时钟 start = 10.000s】

Sample user input (mouse, keyboard, joystick)                 2.采样用户输入(鼠标、键盘、摇杆)

Package up and send movement command using simulation time             3.使用模拟时间打包并发送移动命令    【 上一轮测出来usercmd.msec = 16 】

Read any packets from the server from the network system            4.从网络系统读取来自服务器的所有数据包  【serverSnapshot 服务器时间戳 各对象状态】

Use packets to determine visible objects and their state             5.使用数据包确定可见对象及其状态

Render Scene                                  6.渲染场景

Sample clock to find end time                       7.采样时钟以找到结束时间

End time minus start time is the simulation time for the next frame                 8.结束时间减开始时间,作为下一帧的模拟时间

 

Each time the client makes a full pass through this loop, the “frametime” is used for determining how much simulation is needed on the next frame. If your framerate is totally constant then frametime will be a correct measure. Otherwise, the frametimes will be incorrect, but there isn’t really a solution to this (unless you could deterministically figure out exactly how long it was going to take to run the next frame loop iteration before running it…).

每当客户端完整执行一次这个循环时,“frametime”就会被用于决定下一帧需要进行多少模拟。如果你的帧率完全恒定,那么 frametime 将是一个正确的度量。否则,frametime 就会不准确,但实际上对此并没有真正的解决办法(除非你能在执行下一次帧循环迭代之前,确定性地准确算出它将花费多长时间……)。

The server has a somewhat similar loop:

服务器也有一个类似的循环:

Sample clock to find start time

采样时钟以找到开始时间

 

Read client user input messages from network

从网络读取客户端用户输入消息

Execute client user input messages

执行客户端用户输入消息

Simulate server-controlled objects using simulation time from last full pass

使用上一次完整循环的模拟时间来模拟服务器控制的对象

 

For each connected client, package up visible objects/world state and send to client

对每个已连接客户端,打包可见对象/世界状态并发送给客户端

Sample clock to find end time

采样时钟以找到结束时间

 

End time minus start time is the simulation time for the next frame

结束时间减开始时间,作为下一帧的模拟时间

 

In this model, non-player objects run purely on the server, while player objects drive their movements based on incoming packets. Of course, this is not the only possible way to accomplish this task, but it does make sense.

在这个模型中,非玩家对象完全运行在服务器上,而玩家对象则根据传入的数据包驱动其移动。当然,这并不是完成该任务的唯一方式,但它确实是合理的。

 

Contents of the User Input messages  用户输入消息的内容

In Half-Life engine games, the user input message format is quite simple and is encapsulated in a data structure containing just a few essential fields:

在 Half-Life 引擎游戏中,用户输入消息的格式非常简单,被封装在一个只包含少数字段的数据结构中:

typedef struct usercmd_s
{
// Interpolation time on client 客户端上的插值时间
 short lerp_msec;
// Duration in ms of command 命令持续时间(毫秒)
 byte msec;
// Command view angles. 视角
vec3_t viewangles;
// intended velocities 
// Forward velocity.前向速度
 float forwardmove;
// Sideways velocity.侧向速度
 float sidemove;
// Upward velocity.向上速度
 float upmove;
// Attack buttons 攻击按钮
 unsigned short buttons;
 //
 // Additional fields omitted…  省略其他字段……
 //
} usercmd_t;

 

The critical fields here are the msec, viewangles, forward, side, and upmove, and buttons fields.

The msec field corresponds to the number of milliseconds of simulation that the command corresponds to (it’s the frametime).

The viewangles field is a vector representing the direction the player was looking during the frame.

The forward, side, and upmove fields are the impulses determined by examining the keyboard, mouse, and joystick to see if any movement keys were held down.

Finally, the buttons field is just a bit field with one or more bits set for each button that is being held down.

这里最关键的字段是 msec、viewangles、forwardmove、sidemove、upmove 以及 buttons。

msec 字段对应于该命令所代表的模拟毫秒数(也就是 frametime)。

viewangles 字段是一个向量,表示玩家在该帧期间观察的方向。

forward、side 和 upmove 字段,是通过检查键盘、鼠标和摇杆,看是否按下了任何移动按键后得出的移动输入量

最后,buttons 字段只是一个位字段,每个被按住的按钮会对应设置一个或多个位。

 

Using the above data structures and client / server architecture, the core of the simulation is as follows. First, the client creates and sends a user command to the server. The server then executes the user command and sends updated positions of everything back to client. Finally, the client renders the scene with all of these objects. This core, though quite simple, does not react well under real world situations, where users can experience significant amounts of latency in their Internet connections. The main problem is that the client truly is “dumb” and all it does is the simple task of sampling movement inputs and waiting for the server to tell it the results. If the client has 500 milliseconds of latency in its connection to the server, then it will take 500 milliseconds for any client actions to be acknowledged by the server and for the results to be perceptible on the client. While this round trip delay may be acceptable on a Local Area Network (LAN), it is not acceptable on the Internet.

使用上述数据结构和客户端/服务器架构,模拟的核心流程如下:首先,客户端创建并向服务器发送一个用户命令。服务器随后执行该用户命令,并将一切对象的更新后位置发送回客户端。最后,客户端使用这些对象来渲染场景。这个核心虽然相当简单,但在真实世界中表现并不好,因为用户在互联网连接中可能会经历显著的延迟。主要问题在于客户端确实是“愚笨”的,它所做的仅仅是采样移动输入并等待服务器告知结果。如果客户端与服务器之间的连接延迟达到 500 毫秒,那么客户端的任意操作都要在 500 毫秒后才能被服务器确认,并让客户端感知到结果。虽然这种往返延迟在局域网中或许可以接受,但在互联网环境下则不可接受。

 

Client Side Prediction 客户端预测

One method for ameliorating this problem is to perform the client’s movement locally and just assume, temporarily, that the server will accept and acknowledge the client commands directly. This method is can be labeled as client-side prediction.

缓解这个问题的一种方法,是在本地执行客户端移动,并暂时假设服务器会接受并确认客户端命令。这种方法可以称为客户端预测。

Client-side prediction of movements requires us to let go of the “dumb” or minimal client principle. That’s not to say that the client is fully in control of its simulation, as in a peer-to-peer game with no central server. There still is an authoritative server running the simulation just as noted above. Having an authoritative server means that even if the client simulates different results than the server, the server’s results will eventually correct the client’s incorrect simulation. Because of the latency in the connection, the correction might not occur until a full round trip’s worth of time has passed. The downside is that this can cause a very perceptible shift in the player’s position due to the fixing up of the prediction error that occurred in the past.

对移动进行客户端预测,要求我们放弃“愚笨”或最小化客户端原则。但这并不是说客户端会像没有中央服务器的对等联网游戏那样完全控制自己的模拟。依然存在一个权威服务器,如前所述负责运行模拟。拥有权威服务器意味着,即使客户端模拟出的结果与服务器不同,服务器的结果最终也会纠正客户端的错误模拟。由于连接延迟,这种纠正可能要等完整一个往返时间后才会发生。其缺点是,这可能导致玩家位置发生非常明显的突变,因为系统需要修正过去发生的预测误差

To implement client-side prediction of movement, the following general procedure is used. As before, client inputs are sampled and a user command is generated. Also as before, this user command is sent off to the server. However, each user command (and the exact time it was generated) is stored on the client. The prediction algorithm uses these stored commands.

为了实现移动的客户端预测,通常采用如下过程。和以前一样,采样客户端输入并生成用户命令;同样地,这个用户命令也会发送给服务器。然而,每一个用户命令(以及其生成的精确时间)都会被存储在客户端。预测算法会使用这些已存储的命令。

For prediction, the last acknowledged movement from the server is used as a starting point. The acknowledgement indicates which user command was last acted upon by the server and also tells us the exact position (and other state data) of the player after that movement command was simulated on the server. The last acknowledged command will be somewhere in the past if there is any lag in the connection. For instance, if the client is running at 50 frames per second (fps) and has 100 milliseconds of latency (roundtrip), then the client will have stored up five user commands ahead of the last one acknowledged by the server. These five user commands are simulated on the client as a part of client-side prediction. Assuming full prediction1 , the client will want to start with the latest data from the server, and then run the five user commands through “similar logic” to what the server uses for simulation of client movement. Running these commands should produce an accurate final state on the client (final player position is most important) that can be used to determine from what position to render the scene during the current frame.

在进行预测时,以服务器最后一次ack的移动作为起点。该ack会指出服务器最后执行的是哪一个用户命令,同时还会告诉我们在服务器上模拟完该移动命令后,玩家所处的精确位置(以及其他状态数据)。如果连接存在延迟,那么最后一个已确认命令一定停留在过去。例如,如果客户端以每秒 50 帧【20ms】运行,并且存在 100 毫秒往返延迟,那么客户端会比服务器最后确认的那个命令多存下 5 个用户命令。这 5 个用户命令将由客户端作为客户端预测的一部分来模拟。假设进行完全预测,客户端就会从服务器发来的最新数据开始,然后使用与服务器用于客户端移动模拟“相似的逻辑”去执行这 5 个用户命令。执行这些命令应当在客户端产生一个准确的最终状态(其中最重要的是玩家的最终位置),这一状态可用于决定当前帧从何处渲染场景。

In Half-Life, minimizing discrepancies between client and server in the prediction logic is accomplished by sharing the identical movement code for players in both the server-side game code and the client-side game code. These are the routines in the “pm_shared/” (which stands for “player movement shared”) folder of the HL SDK (http://download.cnet.com/downloads/0- 10045-100-3422497.html). The input to the shared routines is encapsulated by the user command and a “from” player state. The output is the new player state after issuing the user command. The general algorithm on the client is as follows:

在 Half-Life 中,为了尽量减小客户端与服务器预测逻辑之间的差异,服务器端游戏代码和客户端游戏代码共享完全相同的玩家移动代码。这些例程位于 HL SDK 的“pm_shared/”目录中(表示“player movement shared”,玩家移动共享)。

这些共享例程的输入,是用户命令和一个“from”玩家状态;输出则是在执行该用户命令后的新玩家状态。客户端上的通用算法如下:

from state” <- 服务器最后确认的用户命令之后的状态;
“command” <- 服务器最后确认的用户命令之后的第一条命令;
while (true)
{
在 “from state” 上运行 “command” 以生成 “to state”;
if (这是最新的 “command”)
break;

“from state” = “to state”;
“command” = 下一条 “command”;
};

The origin and other state info in the final “to state” is the prediction result and is used for rendering the scene that frame. The portion where the command is run is simply the portion where all of the player state data is copied into the shared data structure, the user command is processed (by executing the common code in the pm_shared routines in HalfLife’s case), and the resulting data is copied back out to the ”to state”.

最终“to state”中的原点和其他状态信息就是预测结果,并被用于渲染该帧场景。运行命令的那一部分,本质上就是把所有玩家状态数据拷贝到共享数据结构中,处理用户命令(在 Half-Life 中即执行 pm_shared 例程中的公共代码),然后再把结果数据拷贝回“to state”

 

In the Half-Life engine, it is possible to ask the client-side prediction algorithm to account for some, but not all, of the latency in performing prediction. The user could control the amount of prediction by changing the value of the “pushlatency” console variable to the engine. This variable is a negative number indicating the maximum number of milliseconds of prediction to perform. If the number is greater (in the negative) than the user’s current latency, then full prediction up to the current time occurs. In this case, the user feels zero latency in his or her movements. Based upon some erroneous superstition in the community, many users insisted that setting pushlatency to minus one-half of the current average latency was the proper setting. Of course, this would still leave the player’s movements lagged (often described as if you are moving around on ice skates) by half of the user’s latency. All of this confusion has brought us to the conclusion that full prediction should occur all of the time and that the pushlatency variable should be removed from the Half-Life engine.

在 Half-Life 引擎中,可以让客户端预测算法只补偿一部分延迟,而不是补偿全部延迟。

用户可以通过修改引擎里的控制台变量 pushlatency 来控制预测量。这个变量是一个负数,表示最多要进行多少毫秒的预测。

如果这个数值的绝对值大于用户当前的延迟,那么就会一直预测到当前时刻,也就是进行完整预测。在这种情况下,用户会感觉自己的移动没有任何延迟。

但由于社区里某种错误的迷信,很多用户坚持认为,应该把 pushlatency 设为“当前平均延迟的一半再取负值”。当然,这样做仍然会让玩家的移动落后于实际输入半个延迟时间,人们常把这种感觉形容为“像穿着冰鞋在移动”。

这些混乱最终让我们得出结论:应该始终进行完整预测,并且应当把 pushlatency 这个变量从 Half-Life 引擎中移除。

There are a few important caveats to this system. First, you’ll notice that, depending upon the client’s latency and how fast the client is generating user commands (i.e., the client’s framerate), the client will most often end up running the same commands over and over again until they are finally acknowledged by the server and dropped from the list (a sliding window in Half-Life’s case) of commands yet to be acknowledged. The first consideration is how to handle any sound effects and visual effects that are created in the shared code. Because commands can be run over and over again, it’s important not to create footstep sounds, etc. multiple times as the old commands are re-run to update the predicted position. In addition, it’s important for the server not to send the client effects that are already being predicted on the client. However, the client still must re-run the old commands or else there will be no way for the server to correct any erroneous prediction by the client. The solution to this problem is easy: the client just marks those commands which have not been predicted yet on the client and only plays effects if the user command is being run for the first time on the client.

这个系统有几个重要的注意事项。

首先你会发现,根据客户端的延迟,以及客户端生成用户命令的速度,也就是客户端的帧率,客户端通常会反复执行同一批命令,直到这些命令最终被服务器确认,并从“尚未确认的命令列表”中移除。在 Half-Life 里,这个列表是一个滑动窗口。

第一个要考虑的问题是,如何处理那些在客户端与服务器共享代码中产生的声音效果和视觉效果。因为同一条命令可能会被一遍又一遍地执行,所以必须避免在旧命令重复执行、用于更新预测位置时,多次生成脚步声之类的效果。

另外,服务器也不应该再向客户端发送那些客户端已经自行预测出来的效果。【?服务器进行预测效果评估?把肯定预测成功的部分不发送?决定一段时间内是否信任Client的预测??】

不过,客户端仍然必须重新执行这些旧命令,否则服务器就无法纠正客户端可能出现的错误预测。【每一帧都从最新的“服务器已确认状态”出发,把还没确认的命令重新 replay 一遍,得到当前预测状态

这个问题的解决办法很简单:客户端只要把那些“尚未在本地预测过”的命令标记出来,并且仅在某条用户命令第一次在客户端执行时才播放相关效果即可。【声音特效之类的表现层】

 

The other caveat is with respect to state data that exists solely on the client and is not part of the authoritative update data from the server. If you don’t have any of this type of data, then you can simply use the last acknowledged state from the server as a starting point, and run the prediction user commands “in-place” on that data to arrive at a final state (which includes your position for rendering). In this case, you don’t need to keep all of the intermediate results along the route for predicting from the last acknowledged state to the current time. However, if you are doing any logic totally client side (this logic could include functionality such as determining where the eye position is when you are in the process of crouching—and it’s not really totally client side since the server still simulates this data also) that affects fields that are not replicated from the server to the client by the networking layer handling the player’s state info, then you will need to store the intermediate results of prediction. This can be done with a sliding window, where the “from state” is at the start and then each time you run a user command through prediction, you fill in the next state in the window. When the server finally acknowledges receiving one or more commands that had been predicted, it is a simple matter of looking up which state the server is acknowledging and copying over the data that is totally client side to the new starting or “from state”.

另一个注意事项与仅存在于客户端、而不属于服务器权威更新数据一部分的状态数据有关。如果你根本没有这类数据,那么你可以简单地使用服务器最后确认的状态作为起点,并直接在该数据上“原地”运行预测用户命令,以得到最终状态(其中包含用于渲染的位置)。在这种情况下,你不需要保存从最后确认状态预测到当前时间的所有中间结果。可是,如果你在做任何完全位于客户端的逻辑(这种逻辑可能包括诸如当你正在蹲下时确定视点位置之类的功能——虽然这并不是真正意义上的完全客户端逻辑,因为服务器也会模拟这些数据),并且这些逻辑会影响那些不会由网络层从服务器复制到客户端的玩家状态字段,那么你就需要保存预测的中间结果。可以使用一个滑动窗口来完成:窗口起始是“from state”,然后每次通过预测运行一个用户命令时,就把下一个状态填入窗口中。当服务器最终确认收到了一个或多个已经被预测的命令时,只需查找服务器当前确认的是哪个状态,然后把那些完全位于客户端的数据复制到新的起始状态,也就是新的“from state”中即可。

So far, the above procedure describes how to accomplish client side prediction of movements. This system is similar to the system used in QuakeWorld2 .

到目前为止,上述过程描述的是如何完成移动的客户端预测。这个系统与 QuakeWorld 所使用的系统类似。

 

Client-Side Prediction of Weapon Firing  武器开火的客户端预测

Layering prediction of the firing effects of weapons onto the above system is straightforward. Additional state information is needed for the local player on the client, of course, including which weapons are being held, which one is active, and how much ammo each of these weapons has remaining. With this information, the firing logic can be layered on top of the movement logic because, once again, the state of the firing buttons is included in the user command data structure that is shared between the client and the server. Of course, this can get complicated if the actual weapon logic is different between client and server. In HalfLife, we chose to avoid this complication by moving the implementation of a weapon’s firing logic into “shared code” just like the player movement code. All of the variables that contribute to determining weapon state (e.g., ammo, when the next firing of the weapon can occur, what weapon animation is playing, etc.), are then part of the authoritative server state and are replicated to the client so that they can be used on the client for prediction of weapon state there.

把武器开火效果的预测叠加到上述系统之上是直接明了的。当然,客户端本地玩家还需要额外的状态信息,包括当前持有哪些武器、哪把武器处于激活状态,以及这些武器各自还剩多少弹药。拥有这些信息后,开火逻辑就可以叠加在移动逻辑之上,因为开火按钮的状态同样也包含在客户端与服务器共享的用户命令数据结构中。当然,如果客户端和服务器上的实际武器逻辑不同,这会变得复杂。在 Half-Life 中,我们通过把武器开火逻辑的实现像玩家移动代码一样转移到“共享代码”中来避免这种复杂性。所有影响武器状态判定的变量(例如弹药量、下一次武器何时可以开火、当前播放的是哪一个武器动画等等),都会成为服务器权威状态的一部分,并被复制到客户端,以便客户端能在那里预测武器状态。

Predicting weapon firing on the client will likely lead to the decision also to predict weapon switching, deployment, and holstering. In this fashion, the user feels that the game is 100% responsive to his or her movement and weapon activation activities. This goes a long way toward reducing the feeling of latency that many players have come to endure with today’s Internet-enabled action experiences.

在客户端预测武器开火,往往也会进一步导致去预测武器切换、装备和收起。这样一来,用户会感觉游戏对其移动和武器操作行为都是百分之百即时响应的。这在很大程度上减轻了许多玩家在当今互联网动作游戏中不得不忍受的那种延迟感

 

Umm, This is a Lot of Work 

Replicating the necessary fields to the client and handling all of the intermediate state is a fair amount of work. At this point, you may be asking, why not eliminate all of the server stuff and just have the client report where s/he is after each movement? In other words, why not ditch the server stuff and just run the movement and weapons purely on the client-side? Then, the client would just send results to the server along the lines of, “I’m now at position x and, by the way, I just shot player 2 in the head.” This is fine if you can trust the client. This is how a lot of the military simulation systems work (i.e., they are a closed system and they trust all of the clients). This is how peer-to-peer games generally work. For Half-Life, this mechanism is unworkable because of realistic concerns about cheating. If we encapsulated absolute state data in this fashion, we’d raise the motivation to hack the client even higher than it already is3 . For our games, this risk is too high and we fall back to requiring an authoritative server.

把必要字段复制到客户端并处理中间状态,是相当多的工作。此时,你也许会问:为什么不干脆把所有服务器相关的内容都去掉,直接让客户端在每次移动后上报自己的位置呢?换句话说,为什么不完全抛弃服务器逻辑,把移动和武器纯粹放在客户端执行呢?这样一来,客户端只需要向服务器发送结果,比如:“我现在位于位置 x,顺便说一下,我刚刚爆了玩家 2 的头。”如果你能够信任客户端,这当然没问题。许多军事模拟系统就是这样工作的(也就是说,它们是一个封闭系统,并且信任所有客户端)。对等联网游戏通常也是这么做的。对 Half-Life 而言,由于现实中的作弊风险,这种机制无法采用。如果我们以这种方式封装绝对状态数据,我们会让篡改客户端的动机比现在还要更高。对于我们的游戏来说,这个风险太大,所以我们仍然要求一个权威服务器。

A system where movements and weapon effects are predicted client-side is a very workable system. For instance, this is the system that the Quake3 engine supports. One of the problems with this system is that you still have to have a feel for your latency to determine how to lead your targets (for instant hit weapons). In other words, although you get to hear the weapons firing immediately, and your position is totally up-to-date, the results of your shots are still subject to latency. For example, if you are aiming at a player running perpendicular to your view and you have 100 milliseconds of latency and the player is running at 500 units per second, then you’ll need to aim 50 units in front of the target to hit the target with an instant hit weapon. The greater the latency, the greater the lead targeting needed. Getting a “feel” for your latency is difficult. Quake3 attempted to mitigate this by playing a brief tone whenever you received confirmation of your hits. That way, you could figure out how far to lead by firing your weapons in rapid succession and adjusting your leading amount until you started to hear a steady stream of tones. Obviously, with sufficient latency and an opponent who is actively dodging, it is quite difficult to get enough feedback to focus in on the opponent in a consistent fashion. If your latency is fluctuating, it can be even harder.

一种在客户端预测移动和武器效果的系统,是完全可行的。例如,Quake3 引擎支持的就是这种系统。这个系统的一个问题是,你仍然必须对自己的延迟有感觉,才能知道该如何提前量瞄准目标(针对即时命中武器)。换句话说,虽然你能立即听到武器开火声,并且你自己的位置始终是最新的,但你的射击结果仍然受延迟影响。举例来说,如果你正在瞄准一个相对你的视线垂直移动的玩家,而你的延迟是 100 毫秒,对方正以每秒 500 个单位的速度奔跑,那么要用即时命中武器击中目标,你就必须瞄准其前方 50 个单位【前提是没做server端的命中补偿?】延迟越高,所需提前量就越大。而培养对自己延迟的“手感”是很困难的。Quake3 曾尝试通过在你收到命中确认时播放一个短促音调来缓解这个问题。那样一来,你可以通过快速连续开火并调整你的提前量,直到你开始听到稳定的提示音流,从而找出需要多大的提前量。显然,在延迟足够高且对手正在主动闪避的情况下,要获得足够的反馈并稳定锁定对手是非常困难的。如果你的延迟还在波动,那就更难了。

 

Display of Targets 目标的显示

Another important aspect influencing how a user perceives the responsiveness of the world is the mechanism for determining, on the client, where to render the other players. The two most basic mechanisms for determining where to display objects are extrapolation and interpolation4 

影响用户如何感知世界响应性的另一个重要方面,是客户端决定将其他玩家渲染在何处的机制。决定对象显示位置的两种最基本机制是外推和插值。

For extrapolation, the other player/object is simulated forward in time from the last known spot, direction, and velocity in more or less a ballistic manner. Thus, if you are 100 milliseconds lagged, and the last update you received was that (as above) the other player was running 500 units per second perpendicular to your view, then the client could assume that in “real time” the player has moved 50 units straight ahead from that last known position. The client could then just draw the player at that extrapolated position and the local player could still more or less aim right at the other player.

对于外推,其他玩家/对象会根据其最后已知的位置、方向和速度,或多或少按一种弹道式的方式向前模拟。因此,如果你落后了 100 毫秒,而你收到的最后一次更新表明(如前所述)另一个玩家正以每秒 500 个单位的速度垂直于你的视线方向奔跑,那么客户端就可以假设,在“实时”中该玩家已从最后已知位置继续直线向前移动了 50 个单位。客户端随后就可以直接在这个外推位置上绘制该玩家,而本地玩家也大致仍可直接对准对方进行瞄准。

The biggest drawback of using extrapolation is that player’s movements are not very ballistic, but instead are very non-deterministic and subject to high jerk5 . Layer on top of this the unrealistic player physics models that most FPS games use, where player’s can turn instantaneously and apply unrealistic forces to create huge accelerations at arbitrary angles and you’ll see that the extrapolation is quite often incorrect. The developer can mitigate the error by limiting the extrapolation time to a reasonable value (QuakeWorld, for instance, limited extrapolation to 100 milliseconds). This limitation helps because, once the true player position is finally received, there will be a limited amount of corrective warping. In a world where most players still have greater than 150 milliseconds of latency, the player must still lead other players in order to hit them. If those players are “warping” to new spots because of extrapolation errors, then the gameplay suffers nonetheless.

使用外推的最大缺点在于,玩家的移动并不真正具有弹道特性,而是高度非确定性的,并且会出现很大的 jerk。再叠加大多数 FPS 游戏所采用的不现实玩家物理模型——玩家可以瞬间转向,并施加不现实的力,从而在任意角度上制造巨大的加速度——你就会发现,外推往往经常是错误的。开发者可以通过将外推时间限制在一个合理值内来缓解这种误差(例如 QuakeWorld 将外推限制为 100 毫秒)。这种限制之所以有帮助,是因为一旦真正的玩家位置最终被接收到,就只会发生有限程度的纠正性扭曲。然而,在一个大多数玩家延迟仍高于 150 毫秒的世界里,玩家仍然必须对其他玩家做提前量瞄准才能命中。如果这些玩家又由于外推误差而“跳动”到新位置,那么游戏体验仍然会受到影响。

The other method for determining where to display objects and players is interpolation. Interpolation can be viewed as always moving objects somewhat in the past with respect to the last valid position received for the object. For instance, if the server is sending 10 updates per second (exactly) of the world state, then we might impose 100 milliseconds of interpolation delay in our rendering. Then, as we render frames, we interpolate the position of the object between the last updated position and the position one update before that (alternatively, the last render position) over that 100 milliseconds. As the object just gets to the last updated position, we receive a new update from the server (since 10 updates per second means that the updates come in every 100 milliseconds) we can start moving toward this new position over the next 100 milliseconds.

另一种决定对象和玩家显示位置的方法是插值。插值可以理解为:相对于对象最后一次收到的有效位置,总是把对象显示在略微过去的位置。比如,如果服务器每秒恰好发送 10 次世界状态更新,那么我们可以在渲染中引入 100 毫秒的插值延迟。然后,在渲染每一帧时,我们会在这 100 毫秒内,把对象的位置从最后一次更新前的那个位置(或者也可以是上一帧渲染位置)插值到最后更新的位置。当对象刚好移动到最后更新位置时,我们又会收到来自服务器的新更新(因为每秒 10 次更新意味着每 100 毫秒来一次),于是我们就可以在接下来的 100 毫秒中朝这个新位置继续移动。

If one of the update packets fails to arrive, then there are two choices: We can start extrapolating the player position as noted above (with the large potential errors noted) or we can simply have the player rest at the position in the last update until a new update arrives (causing the player’s movement to stutter).

如果某个更新包没有到达,就有两种选择:我们可以像前面所述那样开始对玩家位置进行外推(伴随前述那些巨大的潜在误差),或者我们也可以让玩家停留在最后一次更新中的位置,直到新的更新到达(这会导致玩家移动产生卡顿)。

The general algorithm for this type of interpolation is as follows: Each update contains the server time stamp for when it was generated6 From the current client time, the client computes a target time by subtracting the interpolation time delta (100 ms) If the target time is in between the timestamp of the last update and the one before that, then those timestamps determine what fraction of the time gap has passed. This fraction is used to interpolate any values (e.g., position and angles).

这种插值的一般算法如下:
每个更新都包含它生成时的服务器时间戳;
客户端根据当前客户端时间减去插值时间差(100 毫秒),计算出目标时间;
如果该目标时间落在最后一次更新时间戳与再前一次更新时间戳之间,那么这两个时间戳就确定了时间间隔中已经过去了多少比例;
该比例被用于对任意值(例如位置和角度)进行插值。

In essence, you can think of interpolation, in the above example, as buffering an additional 100 milliseconds of data on the client. The other players, therefore, are drawn where they were at a point in the past that is equal to your exact latency plus the amount of time over which you are interpolating. To deal with the occasional dropped packet, we could set the interpolation time as 200 milliseconds instead of 100 milliseconds. This would (again assuming 10 updates per second from the server) allow us to entirely miss one update and still have the player interpolating toward a valid position, often moving through this interpolation without a hitch. Of course, interpolating for more time is a tradeoff, because it is trading additional latency (making the interpolated player harder to hit) for visual smoothness.

从本质上说,在上述例子中,你可以把插值理解为在客户端额外缓冲了 100 毫秒的数据。因此,其他玩家被绘制在一个过去的时间点上,这个时间点等于你的精确延迟,再加上你正在进行插值的那段时间

为了应对偶发的丢包,我们可以把插值时间从 100 毫秒改成 200 毫秒。这样一来,仍然假设服务器每秒发送 10 次更新,即使我们完全丢失一次更新,玩家仍然可以继续朝着一个有效的位置进行插值,通常整个过程看起来也不会有明显卡顿。

当然,增加插值时间本身是一种权衡,因为它是用额外的延迟作为代价来换取画面的平滑性。这样会让经过插值显示出来的玩家更难被击中【如果server端回到过去时间去击中补偿,还有这个问题吗?答:看到后面server补偿后不存在这个问题】

 

It is assumed in this paper that the client clock is directly synchronized to the server clock modulo the latency of the connection. In other words, the server sends the client, in each update, the value of the server’s clock and the client adopts that value as its clock. Thus, the server and client clocks will always be matched, with the client running the same timing somewhat in the past (the amount in the past is equal to the client’s current latency). Smoothing out discrepancies in the client clock can be solved in various ways.

本文假设:客户端时钟与服务器时钟是直接同步的,只差一个连接延迟的偏移量。

换句话说,服务器会在每次更新时把自己的时钟值发送给客户端,而客户端就采用这个值作为自己的时钟。因此,服务器和客户端的时钟始终是一致的,只不过客户端运行的是“稍早一些”的那个时间点,而这个“早了多少”正好等于客户端当前的网络延迟。

至于如何平滑处理客户端时钟中的不一致,则可以通过多种方式来解决。

In addition, the above type of interpolation (where the client tracks only the last two updates and is always moving directly toward the most recent update) requires a fixed time interval between server updates. The method also suffers from visual quality issues that are difficult to resolve. The visual quality issue is as follows. Imagine that the object being interpolated is a bouncing ball (which actually accurately describes some of our players). At the extremes, the ball is either high in the air or hitting the pavement. However, on average, the ball is somewhere in between. If we only interpolate to the last position, it is very likely that this position is not on the ground or at the high point. The bounciness of the ball is “flattened” out and it never seems to hit the ground. This is a classical sampling problem and can be alleviated by sampling the world state more frequently. However, we are still quite likely never actually to have an interpolation target state be at the ground or at the high point and this will still flatten out the positions.

此外,上面那种插值方式,也就是客户端只跟踪最近两次更新、并且始终直接朝最新一次更新的位置移动的方法,要求服务器更新之间的时间间隔必须固定。

这种方法还存在一个很难解决的画面质量问题。问题如下:假设被插值的对象是一个弹跳的球,这其实也很贴切地描述了我们某些玩家的运动状态。这个球在运动的极端位置,要么处在空中最高点,要么正好砸到地面;但从平均意义上看,它大部分时间是在这两者之间。如果我们只朝“最后一个位置”做插值,那么这个位置很可能既不在地面上,也不在最高点。结果就是球的弹跳感会被“压平”,看起来它似乎从来没有真正落到地面上。这是一个经典的采样问题,可以通过更高频率地采样世界状态来缓解。不过,即使这样,我们仍然很可能始终拿不到一个恰好位于地面或最高点的插值目标状态,因此位置曲线依旧会被压平。

In addition, because different users have different connections, forcing updates to occur at a lockstep like 10 updates per second is forcing a lowest common denominator on users unnecessarily. In Half-Life, we allow the user to ask for as many updates per second as he or she wants (within limit). Thus, a user with a fast connection could receive 50 updates per second if the user wanted. By default, Half-Life sends 20 updates per second to each player the Half-Life client interpolates players (and many other objects) over a period of 100 milliseconds.

The time spacing of these updates is not necessarily fixed. The reason why is that during high activity periods of the game (especially for users with lower bandwidth connections), it’s quite possible that the game will want to send you more data than your connection can accommodate. If we were on a fixed update interval, then you might have to wait an entire additional interval before the next packet would be sent to the client. However, this doesn’t match available bandwidth effectively. Instead, the server, after sending every packet to a player, determines when the next packet can be sent. This is a function of the user’s bandwidth or “rate” setting and the number of updates requested per second. If the user asks for 20 updates per second, then it will be at least 50 milliseconds before the next update packet can be sent. If the bandwidth choke is active (and the server is sufficiently high framerate), it could be 61, etc., milliseconds before the next packet gets sent. Thus, Half-Life packets can be somewhat arbitrarily spaced. The simple move to latest goal interpolation schemes don’t behave as well (think of the old anchor point for movement as being variable) under these conditions as the position history interpolation method (described below).

另外,由于不同用户的网络连接状况不同,强制所有更新都按固定节奏进行,比如每秒 10 次更新,其实是不必要地把所有用户都限制在最低共同标准上。在 Half-Life 中,我们允许用户在一定上限内,请求自己想要的每秒更新次数。因此,网络状况好的用户如果愿意,甚至可以每秒接收 50 次更新。默认情况下,Half-Life 会向每位玩家每秒发送 20 次更新,而 Half-Life 客户端会在 100 毫秒的时间窗口内对玩家以及许多其他对象进行插值。

这些更新的时间间隔并不一定是固定的。原因在于,在游戏活动特别频繁的时段,尤其是对带宽较低的用户来说,游戏很可能会想发送比连接所能承载更多的数据。如果采用固定更新间隔,那么客户端可能不得不额外再等一个完整周期,服务器才能发送下一个数据包,这样并不能有效利用可用带宽。
因此,服务器在每次向某个玩家发送完一个数据包后,都会重新计算“下一个数据包最早什么时候可以发送”。这个时间取决于用户设置的带宽,也就是 rate,以及用户请求的每秒更新次数。比如用户请求每秒 20 次更新,那么下一次更新包至少要 50 毫秒后才能发送。如果带宽限制生效了,而且服务器帧率又足够高,那么实际发送间隔也可能变成 61 毫秒,或者其他值。也就是说,Half-Life 的数据包间隔可能是相当不规则的。在这种条件下,那种“简单地朝最新目标点移动”的插值方案表现不如位置历史插值法,因为前者的旧锚点本身会不断变化

 

To avoid the flattening of the bouncing ball problem, we employ a different algorithm for interpolation. In this method, we keep a more complete “position history” for each object that might be interpolated.

为了解决“弹跳小球被压平”的问题,我们采用了另一种插值算法。在这种方法中,我们会为每个可能需要插值的对象保留一份更完整的“位置历史”。

The position history is the timestamp and origin and angles (and could include any other data we want to interpolate) for the object. Each update we receive from the server creates a new position history entry, including timestamp and origin/angles for that timestamp. To interpolate, we compute the target time as above, but then we search backward through the history of positions looking for a pair of updates that straddle the target time. We then use these to interpolate and compute the final position for that frame. This allows us to smoothly follow the curve that completely includes all of our sample points. If we are running at a higher framerate than the incoming update rate, we are almost assured of smoothly moving through the sample points, thereby minimizing (but not eliminating, of course, since the pure sampling rate of the world updates is the limiting factor) the flattening problem described above.

这个位置历史记录了对象在各个时刻的时间戳、位置和朝向角度(当然也可以包含任何其他我们希望参与插值的数据)。【客户端不会只记住“这个对象最新的位置”,
而是会把这个对象最近一段时间内收到的多个历史位置样本都存起来,等渲染时再从这些历史样本里挑两帧做插值。】

服务器每发送一次更新,客户端就会新增一条位置历史记录,其中包含该时间戳对应的位置和角度信息。

在进行插值时,我们仍然像前面那样先计算目标时间,但接下来会在位置历史中向后查找,寻找一对刚好把目标时间夹在中间的更新记录。然后,我们用这两条记录做插值,计算出该帧最终的位置。

这样做的好处是,我们可以更平滑地沿着一条包含所有采样点的曲线运动。如果客户端的帧率高于接收更新的频率,那么几乎可以保证它能平滑地穿过这些采样点,从而尽量减轻上面提到的“压平”问题。当然,这只能减轻,不能彻底消除,因为世界状态更新本身的采样频率终究才是最终限制因素。

 

The only consideration we have to layer on top of either interpolation scheme is some way to determine that an object has been forcibly teleported, rather than just moving really quickly. Otherwise we might “smoothly” move the object over great distances, causing the object to look like it’s traveling way too fast. We can either set a flag in the update that says, “don’t interpolate” or “clear out the position history,” or we can determine if the distance between the origin and one update and another is too big, and thereby presumed to be a teleportation/warp. In that case, the solution is probably to just move the object to the latest know position and start interpolating from there.

无论采用哪一种插值方案,我们唯一还需要额外考虑的是:必须有办法判断一个对象是被强制传送了,而不只是移动得特别快。

否则,我们可能会把对象在很长一段距离上的位移也“平滑”插值出来,结果看起来就像这个对象正以不合理的超高速度飞过去。

处理这个问题有两种办法:

  1. 在更新数据里设置一个标记,例如“不要插值”或者“清空位置历史”。
  2. 检查两次更新之间的位置距离是否过大,如果大到异常,就认为这是一次传送或瞬移。

在这种情况下,合理的做法通常是:直接把对象移动到最新已知位置,然后从这个位置开始重新进行插值。

 

Lag Compensation 延迟补偿

Understanding interpolation is important in designing for lag compensation because interpolation is another type of latency in a user’s experience. To the extent that a player is looking at other objects that have been interpolated, then the amount of interpolation must be taken into consideration in computing, on the server, whether the player’s aim was true.

理解插值对于设计延迟补偿非常重要,因为插值本身也是玩家体验中的一种额外延迟。只要玩家看到的其他对象是经过插值显示出来的,那么服务器在判断玩家瞄准是否准确时,就必须把这部分插值延迟也考虑进去。

 

Lag compensation is a method of normalizing server-side the state of the world for each player as that player’s user commands are executed. You can think of lag compensation as taking a step back in time, on the server, and looking at the state of the world at the exact instant that the user performed some action. The algorithm works as follows:

延迟补偿是一种在服务器端为每个玩家“归一化世界状态”的方法,具体做法是在执行该玩家的用户命令时,把世界状态还原到那个玩家当时实际操作的时刻。

你可以把延迟补偿理解为:服务器在时间上后退一步,去查看玩家执行某个动作那一瞬间,世界究竟处于什么状态。

算法流程如下:

Before executing a player’s current user command, the server:

Computes a fairly accurate latency for the player Searches the server history (for the current player) for the world update that was sent to the player and received by the player just before the player would have issued the movement command From that update (and the one following it based on the exact target time being used), for each player in the update, move the other players backwards in time to exactly where they were when the current player’s user command was created. This moving backwards must account for both connection latency and the interpolation amount8 the client was using that frame. Allow the user command to execute (including any weapon firing commands, etc., that will run ray casts against all of the other players in their “old” positions). Move all of the moved/time-warped players back to their correct/current positions

在执行某个玩家当前的用户命令之前,服务器会:

  1. 计算该玩家一个相当准确的延迟值。
  2. 在服务器保存的历史记录中查找:对当前玩家而言,哪一个世界更新包是“在玩家发出这条移动命令之前,已经发送给玩家并被玩家接收到的最后一个更新包”。
  3. 基于这个更新包,以及紧随其后的那个更新包(取决于所使用的精确目标时间),把该更新中所有其他玩家的位置回退到当前玩家创建这条用户命令时他们所处的准确位置。这个“回退”过程必须同时考虑网络连接延迟,以及该帧客户端使用的插值量1
  4. 允许这条用户命令执行,包括其中任何开火命令等,因为这些命令可能会对所有处于“旧位置”的其他玩家进行射线检测。
  5. 再把所有这些被移动过、被“时间回溯”过的玩家恢复到它们正确的当前位置。

Note that in the step where we move the player backwards in time, this might actually require forcing additional state info backwards, too (for instance, whether the player was alive or dead or whether the player was ducking). The end result of lag compensation is that each local client is able to directly aim at other players without having to worry about leading his or her target in order to score a hit. Of course, this behavior is a game design tradeoff.

需要注意的是,在把玩家回退到过去状态的这一步里,可能还需要连带回退更多状态信息,比如玩家当时是活着还是已经死亡,或者当时是否处于蹲伏状态。

延迟补偿最终达到的效果是:每个本地客户端都可以直接瞄准其他玩家,而不必为了命中目标而刻意提前量。

当然,这种行为本身也是一种游戏设计上的权衡。

 

Game Design Implications of Lag Compensation  延迟补偿游戏设计影响

The introduction of lag compensation allows for each player to run on his or her own clock with no apparent latency. In this respect, it is important to understand that certain paradoxes or inconsistencies can occur. Of course, the old system with the authoritative server and “dumb” or simple clients had it’s own paradoxes. In the end, making this tradeoff is a game design decision. For Half-Life, we believe deciding in favor of lag compensation was a justified game design decision.

引入延迟补偿后,每个玩家都可以按照自己的时钟进行操作,并且几乎感觉不到延迟。从这个意义上说,必须理解:系统中会出现某些悖论或不一致现象。当然,旧的那套“服务器权威、客户端只是简单执行”的系统本身也有它自己的悖论。归根结底,这是一项游戏设计决策。对于 Half-Life 来说,我们认为选择采用延迟补偿是合理的设计决定。

The first problem of the old system was that you had to lead your target by some amount that was related to your latency to the server. Aiming directly at another player and pressing the fire button was almost assured to miss that player. The inconsistency here is that aiming is just not realistic and that the player controls have non-predictable responsiveness.

旧系统的第一个问题是:玩家必须根据自己到服务器的延迟,提前量来瞄准目标。也就是说,直接把准星对准另一个玩家再按下开火键,几乎注定会打空。这种不一致在于:瞄准行为不符合直觉,玩家控制的响应也显得不可预测。

With lag compensation, the inconsistencies are different. For most players, all they have to do is acquire some aiming skill and they can become proficient (you still have to be able to aim). Lag compensation allows the player to aim directly at his or her target and press the fire button (for instant hit weapons9 ). The inconsistencie that sometimes occur, however, are from the points of view of the players being fired upon.

而在使用延迟补偿后,不一致的形式则不同了。对于大多数玩家来说,他们只需要掌握基本的瞄准技巧,就能够打得很准。延迟补偿让玩家可以直接瞄准目标并按下开火键(这里指的是即时命中武器1)。不过,偶尔出现的不一致,更多是从“被击中的玩家”的视角体现出来的。

For instance, if a highly lagged player shoots at a less lagged player and scores a hit, it can appear to the less lagged player that the lagged player has somehow “shot around a corner”10. In this case, the lower lag player may have darted around a corner. But the lagged player is seeing everything in the past. To the lagged player, s/he has a direct line of sight to the other player. The player lines up the crosshairs and presses the fire button. In the meantime, the low lag player has run around a corner and maybe even crouched behind a crate. If the high lag player is sufficiently lagged, say 500 milliseconds or so, this scenario is quite possible. Then, when the lagged player’s user command arrives at the server, the hiding player is transported backward in time and is hit. This is the extreme case, and in this case, the low ping player says that s/he was shot from around the corner. However, from the lagged player’s point of view, they lined up their crosshairs on the other player and fired a direct hit. From a game design point of view, the decision for us was easy: let each individual player have completely responsive interaction with the world and his or her weapons.

9 For weapons that fire projectiles, lag compensation is more problematic. For instance, if the projectile lives autonomously on the server, then what time space should the projectile live in? Does every other player need to be “moved backward” every time the projectile is ready to be simulated and moved by the server? If so, how far backward in time should the other players be moved? These are interesting questions to consider. In Half-Life, we avoided them; we simply don’t lag compensate projectile objects (that’s not to say that we don’t predict the sound of you firing the projectile on the client, just that the actual projectile is not lag compensated in any way). 10 This is the phrase our user community has adopted to describe this inconsistency.

例如,如果一个高延迟玩家朝一个低延迟玩家开枪并命中,那么在低延迟玩家看来,高延迟玩家似乎是“隔着拐角把他打中了”。在这种情况下,低延迟玩家可能已经快速躲到拐角后面了;但高延迟玩家看到的仍然是过去的世界状态。对高延迟玩家来说,他确实仍然能直接看到对方,于是瞄准并开火。与此同时,低延迟玩家已经绕过拐角,甚至可能还蹲到了箱子后面。如果高延迟玩家的延迟足够高,比如达到 500 毫秒左右,那么这种情况完全可能发生。等到高延迟玩家的用户命令到达服务器时,服务器会把那个已经躲起来的玩家回退到过去的时刻,于是命中判定成立。
这是最极端的情况。此时,低延迟玩家会觉得自己是“被隔墙角打中了”;但从高延迟玩家的视角看,他只是把准星对准了对方并打出了一发正中目标的子弹。就游戏设计而言,我们的选择很明确:让每个玩家都能与世界以及自己的武器进行完全即时响应的交互。

对于发射实体投射物的武器,延迟补偿会复杂得多。比如,如果投射物是在服务器上独立存在和运动的,那么它究竟应该处在哪个时间坐标系里?是不是每次服务器要模拟和移动这个投射物时,都需要把其他玩家回退到过去?如果是,又该回退多少时间?这些都是很有意思的问题。在 Half-Life 中,我们回避了这些问题:我们并不对投射物进行延迟补偿。当然,这并不意味着客户端不会预测你发射投射物时的声音,只是说投射物本身不会以任何方式进行延迟补偿。

 

In addition, the inconsistency described above is much less pronounced in normal combat situations. For first-person shooters, there are two more typical cases. First, consider two players running straight at each other pressing the fire button. In this case, it’s quite likely that lag compensation will just move the other player backwards along the same line as his or her movement. The person being shot will be looking straight at his attacker and no “bullets bending around corners” feeling will be present.The next example is two players, one aiming at the other while the other dashes in front perpendicular to the first player. In this case, the paradox is minimized for a wholly different reason. The player who is dashing across the line of sight of the shooter probably has (in firstperson shooters at least) a field of view of 90 degrees or less. In essence, the runner can’t see where the other player is aiming. Therefore, getting shot isn’t going to be surprising or feel wrong (you get what you deserve for running around in the open like a maniac). Of course, if you have a tank game, or a game where the player can run one direction, and look another, then this scenario is less clear-cut, since you might see the other player aiming in a slightly incorrect direction.

另外,上面这种不一致在一般战斗场景中并没有那么明显。对于第一人称射击游戏来说,更常见的情况大致有两种。

第一种情况是:两个玩家正面朝彼此冲锋,并同时按下开火键。在这种情况下,延迟补偿通常只会把对方沿着其移动轨迹向后回退一点【针对不同体验,细分服务器的延迟补偿逻辑】被击中的玩家本来就是正面对着攻击者,因此不会产生那种“子弹拐弯穿墙”的感觉。

第二种情况是:一个玩家瞄准另一个玩家,而后者从前者视线前方横向冲过。在这种情况下,这种悖论之所以不明显,是出于完全不同的原因。那个横穿视线的玩家,尤其在第一人称射击游戏里,通常视野只有大约 90 度甚至更小。也就是说,这个奔跑中的玩家往往看不到对方究竟瞄向哪里。因此,被打中并不会显得特别离谱或不合理。说白了,你在空旷地带乱跑,被打中了也不奇怪。
当然,如果这是坦克游戏,或者是那种角色可以朝一个方向跑、却朝另一个方向看的游戏,那么这个场景就没那么简单了,因为此时你可能真的能看到对方的瞄准方向有些偏差。

 

Conclusion

Lag compensation is a tool to ameliorate the effects of latency on today’s action games. The decision of whether to implement such a system rests with the game designer since the decision directly changes the feel of the game. For Half-Life, Team Fortress and Counter Strike, the benefits of lag compensation easily outweighed the inconsistencies noted above

延迟补偿是一种用来减轻当代动作游戏中网络延迟影响的工具。是否要实现这样一套系统,最终取决于游戏设计师,因为这个决定会直接改变游戏的手感。对于 Half-Life、Team Fortress 和 Counter-Strike 来说,延迟补偿带来的好处显然远远超过了前面提到的那些不一致问题。

 

 

 

 

 

 

 

 

 

 

posted @ 2026-03-14 00:26  sun_dust_shadow  阅读(1)  评论(0)    收藏  举报