这篇文章介绍了cs这样的第一人称射击游戏中如何实现延迟补偿。非第一人称设计游戏设计也可以参考其中一些思想
原文地址:
Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization
C/S架构游戏协议设计和优化中延迟补偿的应用
Contents [hide]
1 Overview
1 综述
2 Basic Architecture of a Client / Server Game
2 C/S游戏的基本架构
3 Contents of the User Input messages
3 用户消息的内容
4 Client Side Prediction
4 客户端预测
5 Client-Side Prediction of Weapon Firing
5 开火过程中的客户端预测
6 Umm, This is a Lot of Work
6 一些工作
7 Display of Targets
7 目标的显示
8 Lag Compensation
8 延迟补偿
9 Game Design Implications of Lag Compensation
9 游戏涉及中延迟补偿的使用
10 Conclusion
10 总结
11 Footnotes
11 脚注
Overview
1.综述
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-of-the-art hardware. The same holds true for their network connections.
第一人称角色网络游戏的设计是一项很有挑战性的工作。网络环境下的健壮性,是动作游戏能否成功的一个重要因素。另外,PC上面的开发者需要考虑到玩家层次不齐的机器配置以及网络状况,很多用户的硬件配置跟网络跟当前最好的配置跟网络有一定差距。
While broadband has been held out as a panacea for all of the current woes of on-line gaming, broadband is not a 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 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 on-line 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.
我们应该提供给玩家良好的游戏。本篇文章讨论了如何提供给玩家顶尖的操作体验;介绍了很多在线动作游戏中采用的C/S架构背景。此外,我们还讨论了如何通过一个预测模型来掩饰延迟带来的影响。文章的最后描述了一个叫做延迟补偿的机制,弥补了因为网络质量不好带来的负面影响
Basic Architecture of a Client / Server Game
2.C/S游戏的基本架构
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.
网络上可玩的大部分动作游戏都是C/S结构游戏基础上修改完成的,比如半条命以及其修改版反恐精英、军团要塞,以及一些基于quake3引擎和虚幻引擎的游戏。这类游戏都有一个用来执行游戏逻辑的服务器以及连接到这个服务器的多个客户端。客户端仅仅是用来接收玩家的操作并发给服务器,服务器对这些操作作出响应,移动玩家周围物体,并将游戏世界的信息发给客户端显示出来。当然世界的游戏系统有更多组件,我们这样简化有利于分析预测和延迟补偿。
With this in mind, the typical client / server game engine architecture generally looks like this:
基于这种考虑,典型的C/S游戏引擎通常看起来是这样的
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:
为了便于讨论,我们假定客户端跟服务器之间已经建立连接;客户端的每一帧循环如下:
1.Sample clock to find start time
1.获取帧开始时间
2.Sample user input (mouse, keyboard, joystick)
2.采集用户输入
3.Package up and send movement command using simulation time
3.根据模拟时间将移动命令打包发送给服务器
4.Read any packets from the server from the network system
4.获取处理服务器传过来的数据包
5.Use packets to determine visible objects and their state
5.根据服务器数据包的内容决定可见物体及其状态
6.Render Scene
6.渲染场景
7.Sample clock to find end time
7.获取帧结束时间
8.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:
服务器的循环大同小异:
1.Sample clock to find start time
1.获取帧开始时间
2.Read client user input messages from network
2.读取客户端发过来的操作信息
3.Execute client user input messages
3.根据客户端操作执行逻辑运算
4.Simulate server-controlled objects using simulation time from last full pass
4.采用上一个循环得到的模拟时间来模拟服务器控制的物体移动状态
5.For each connected client, package up visible objects/world state and send to client
5.对每一个连接的客户端,发送打包相应的物体/世界状态
6.Sample clock to find end time
6.获取帧结束时间
7.End time minus start time is the simulation time for the next frame
7.结束时间减去开始时间就是下一帧的模拟时间
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
3.用户消息的内容
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
shortlerp_msec;
// Duration in ms of command
bytemsec;
// Command view angles.
vec3_tviewangles;
// intended velocities
// Forward velocity.
floatforwardmove;
// Sideways velocity.
floatsidemove;
// Upward velocity.
floatupmove;
// 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,forward,side,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.
基于C/S架构的游戏采用以上数据结构运行如下:客户端创建命令并发送到服务器,服务器响应这些命令并把更新了的世界和物体位置信息发回客户端,客户端收到以后进行渲染。这种方式非常简单,但是在实际应用中效果差强人意,用户会感觉到网络连接带来的明显延迟。这主要是由于客户端完全没有逻辑操作,发出消息以后就等待服务器响应。如果客户端跟服务器有500ms的延迟,客户端执行了操作到看到操作的结果就需要500ms,这种延迟在局域网通常可以接受(因为通常延迟比较小),但在因特网上是没法接受的
Client Side Prediction
4.客户端预测
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 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.
采用客户端运动预测以后,客户端就不再是一个“小型客户端”,不再单单响应服务器命令;但也不是说客户端可以像没有中央服务器的p2p游戏完全自治。服务器仍然在运行并保证在客户端跟服务器运行结果不一致的情况下纠正客户端错误的模拟。由于网络延迟,修正在一个网络传输周期以后才会执行,这个时候纠正信息通常已经过期,这样会导致明显的位置漂移,因为客户端收到的修正信息是过去某个时间的。
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 prediction[1], 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.
预测的过程中,我们把服务器确认的移动信息作为开始,这样客户端就可以确定服务器执行上次命令以后游戏中玩家的准确信息(比如位置)。如果网络有延迟,这个确认命令也会有一定延迟。假设客户端运行帧率为50fps,网络延时为100ms,这样在客户端收到服务器的确认命令的时候,本地命令队列中已经有5条信息,这5条信息被用来执行客户端预测。假设执行完全预测【1】客户端在收到来自服务器的最新信息后,就开始按照与服务器相同的逻辑执行本地消息队列中的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. 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:
在半条命这个游戏中,客户端跟服务器采用相同的代码来计算移动,这样可以减小客户端预测跟服务器之间的误差。这些代码位于HLSDK中的pm_shared/(意思是“player movement shared”)。这段代码的输入是玩家操作和客户端的初始状态,输出是玩家操作以后的状态。客户端算法大致如下:
"from state" <- state after last user command acknowledged by the server;
"command" <- first command after last user command acknowledged by server;
while (true)
{
run "command" on "from state" to generate "to state";
if (this was the most up to date "command")
break;
"from state" = "to state";
"command" = next "command";
};
“初始状态” <-上个玩家命令执行以后玩家的状态
"命令" <-尚未执行的下一条命令
while (true)
{
以"from state" 为基础执行"command"得到 "to state";
if (没有更多的 "command")
break;
"from state" = "to state";
"command" = next "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 Half-Life's case), and the resulting data is copied back out to the "to state".
玩家的初始状态和预测结果用来渲染场景。命令的执行过程就是:将玩家状态复制到共享数据结构中,执行玩家操作(执行hlsdk中pm_shared中的共用代码),然后将结果复制到目标状态(to state)
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.
这个系统中有几个需要注意的地方,首先,由于网络延迟,客户端又在不停地以一定速度(客户端帧率)生成命令,一个命令通常会被客户端多次执行,知道得到服务器的确定以后将其从命令列表中删除(这就是半条命中的滑动窗口)。首先要考虑的是如何处理共享代码中生成的声效和动画效果。因为命令可能会被多次执行,预测位置的过程被多次执行的时候要注意避免重声等不正确的效果。另外,服务器也要避免客户端意见预测的效果。然而,客户端必须重新运行旧的命令,否则就没法根据服务器来纠正客户端的预测错误。解决方法很简单:客户端将没有执行的客户端命令进行标记,如果这些命令在客户端第一次执行,则播放相应的效果。
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".
另外需要注意的是服务器不处理,只有客户端才有的一些数据;如果没有这种类型的数据,我们可以如上面所述,以服务器第一条消息作为起点进行预测得到下一帧状态(包括用来渲染的位置信息)。然而,如果有些逻辑是纯客户端的,服务器不会处理(比如玩家蹲下来眼睛的位置-然而这也不是纯客户端信息,因为服务器也会处理这个数据),这种情况下我们需要将预测的中间结果存起来。可以用一个滑动窗口完成这项工作,其中“开始状态”是开始,以后每次执行一个玩家命令预测完成后,填写窗口中的下一个状态;当服务器通知某个命令被接受并执行以后,从窗口中查找服务器处理的是哪条命令并将相应的数据传到下一个帧的“起始状态”
So far, the above procedure describes how to accomplish client side prediction of movements. This system is similar to the system used in QuakeWorld2.
到此为止,我们描述了客户端的运动预测。quakeworld2中采用了这种类型的预测
Client-Side Prediction of Weapon Firing
5.开火过程中的客户端预测
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 Half-Life, 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.
上面描述的系统可以很自然地用于武器开火效果预测。客户端玩家需要记录一些状态,比如身上有哪些武器,正在使用的是哪一个,每把武器都还剩多少弹药。有了这些信息,开火逻辑可以建立在运动逻辑上面,只需要在客户端和服务器使用的命令里面加上玩家开火的按键信息。在半条命中,为了简单,武器开火逻辑代码也跟运动代码一样也作为“共享代码”。所有会影响到武器状态的变量,比如弹药、下次可开火时间、正在播放那个武器动画,都作为服务器的状态,这些状态会通知给客户端用来预测武器状态。
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.
客户端武器开火预测包括预测武器切换、部署、皮套。这样,玩家会感觉游戏中的移动和武器状态100%受他控制。这在减小网络延迟给玩家带来的不爽上面迈出了一大步。
Umm, This is a Lot of Work
6.一些工作
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的脑袋”。如果客户端可信的话,这样做是可以的,很多军方仿真系统就是这样做的(他们是一个封闭系统,所有客户端都可信)。点对点的游戏也是这么做的。对于半条命来说不可以这样做,因为客户端可能“欺骗”服务器。如果我们以这种方法封装状态数据,就会诱导玩家破解客户端【3】。对于我们的游戏来说这样做奉献太大,我们还是选择采用服务器模式来做校验。
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就支持这样的预测。这个系统需要注意一点,在判断目标的时候需要考虑到延迟(比如即时射击武器)。换句话说,虽然你看到自己用即时武器进行了射击,你自己的位置也是最新的,射击结果仍然跟延迟有关。例如,如果你射击一个玩家,这个玩家沿与你实现垂直的方向奔跑,假设你客户端延迟为100ms,玩家奔跑速度是500单位每秒,这样你需要瞄准玩家前方50单位才能准确击中。延迟越大,就需要更大的提前量。靠感觉弥补延迟太困难了。为了减轻这种效果,quake3对你的射击播放一个短音来进行确定。这样,玩家可以算出快速发射武器的时候需要多大的提前量,同时调整提前量直到听到稳定的音调串。如果延迟比较大,而你的对手又在不断躲避,就很难获得足够的反馈判断。如果延迟也不断变化,就更难了。
Display of Targets
7.目标的显示
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 interpolation[4].
影响玩家游戏体验的另一个重要方面是客户端如何渲染其它玩家。两种基本的判断机制是:外推法和内插法【4】
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.
外推法把其它玩家/物体看作一个点,这个点开始的位置、方向、速度已知,沿着自己的弹道向前移动。因此,假设延时是100ms,最新的协议通知客户端这个玩家奔跑速度是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 jerk[5]. 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"【5】。大部分FPS游戏采用非现实的玩家系统,玩家可以随时转弯,可以在任意角度作用不现实的加速度,因此外推法得到的结果经常是错误地。开发者可以通过限制外推时间来减轻外推误差(比如quake限制不能超过100ms)。这种限制使得在客户端收到玩家正确位置以后,纠错不至于太大。当前大部分玩家的网络延迟高于150ms,玩家必须对游戏中的其他玩家进行外推以便正确击中。如果别的玩家因为外推错误,被服务器拉回,游戏体验将非常差。
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次世界信息,客户端渲染的时候会有100ms滞后。这样,每一帧渲染的时候,我们通过最新收到的位置信息和前100ms的位置信息(或者上一帧渲染位置)进行差值得到结果。我们每收到一个物体位置的更新信息,(每秒10个更新意味着每100ms收到一个更新)接下来的100ms我们就可以朝这个新的位置移动。
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).
如果一个更新包没有收到,有2种处理方法:第一、用上面介绍的外推法(有可能产生较大误差);第二、保持玩家位于当前位置直到收到下一个更新包(会导致玩家移动顿挫)
The general algorithm for this type of interpolation is as follows:
内插法的大致过程如下:
1.Each update contains the server time stamp for when it was generated[6]
1.每个更新包包含生成的服务器时间戳【6】
2.From the current client time, the client computes a target time by subtracting the interpolation time delta (100 ms)
2.根据客户端当前时间,客户端通过减去时间差(100ms)计算 一个目标时间
3.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.
3.如果计算得到的目标时间在上一个更新时间和上上个更新时间之间,这些时间戳可以决定目标时间在过去的时间间隙中的情况
4.This fraction is used to interpolate any values (e.g., position and angles).
4.目标时间情况用来通过插值计算结果(如位置、角度)
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.
上面提到的插值法,本质上是客户端缓存了接下来100ms的数据。对于每一个周围的玩家,他们都位于过去某个时间的位置,根据每一个具体的时间点进行插值。如果偶尔发生丢包,我们就将插值时间延长到200ms。这样我们就可以忽略一次更新(假设同步频率还是10次每秒),玩家还可以移动到合理的目标位置,这样进行插值通常不会有什么问题。当然,插值多少时间需要权衡,因为这种方法是用延时(玩家更难击中)来换取平滑。
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.
另外,上述插值方法(客户端通过2个更新信息插值并且朝最新更新位置移动)需要服务器更新信息间隔固定。对于所谓的“视觉效果因素”,这种方式很难处理,“视觉效果因素”是这样的:假设我们插值的物体是弹球(这种模型可以准确描述某些玩家)。极端情况下,球或者在空中,或者正在碰地板。然而,通常情况下球在这两种状态之间。如果我们只插值上一个位置,这个位置可能既不在地面上,也不是最高点,这样,弹球弹的效果就被平滑掉了,好像永远没有弹到地面一样。这是一个经典问题,增加采样率可以减轻这种影响,但是仍然有可能我们采样不到球在地面的点跟最高点,这些点会给平滑掉。
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.[7]
另外,不同用户网络状况不同,强迫每个用户都以固定速度更新(比如每秒10次)效果不是很好,在半条命中,用户每秒可以请求任意数量的更新包(没有限制)。这样,高速网络用户可以每秒更新50次,只要用户愿意。半条命的默认设置是每秒每个用户(以及游戏中其它物体)发送20次更新,以100ms为时间窗口进行插值。【7】
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.
需要注意的是,上面提到的插值方法使用的时候,物体有时候会被服务器拉回,而不是快速移动。当然我们也可以平滑地将物体移动一段较长的距离,这样看起来物体移动很快。更新的过程中我们可以设一个标志表示不插值或清除历史记录,或者如果起始点与目标点距离过长,我们就认为数据不正常。这种情况我们就将物体直接拉过去。并以这个位置为起始点进行插值。
Lag Compensation
8.延迟补偿
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:
延迟补偿是服务器执行的一种策略,当服务器收到客户端命令并执行的过程中,根据客户端的具体情况进行归一。延迟补偿可以看做服务器处理用户命令的时候回退一段时间,退到客户端发送命令时候的准确时间。算法流程如下:
1.Before executing a player's current user command, the server:
1.服务器执行客户端命令之前执行以下操作:
1.Computes a fairly accurate latency for the player
1.计算玩家正确的延迟
2.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
2.对每个玩家,从服务器历史信息中找到发送给玩家信息和收到玩家响应的信息。
3.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 andthe interpolation amount[8] the client was using that frame.
3.对于每一个玩家,将其拉回到这个更新时间(插值得到的精确时间)中执行用户命令。这个回退时间需要考虑到命令执行的时候的网络延时和插值量【8】
2.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).
2.执行玩家命令(包括武器开火等。)
3.Move all of the moved/time-warped players back to their correct/current positions
3.将所有移动的、错位的玩家移动到他们当前正确位置。
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
9.游戏涉及中延迟补偿的使用
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.
采用延迟补偿以后,每个玩家游戏的过程中感觉不到明显延迟。在这里需要理解可能会产生一些矛盾和不一致。当然,验证服务器和无逻辑的客户端老系统也会有自相矛盾的情况。最后,这个这种事游戏设计决定的。对于半条命,我们相信采用延迟补偿是正确的游戏决定。
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 weapons[9]). The inconsistencies that sometimes occur, however, are from the points of view of the players being fired upon.
采用延迟补偿以后带来的是另一种形式的不一致。对于大部分玩家,他们只需要专注于得到更多的射击技能来武装他们(当然他们也是需要瞄准的)。延时补偿使得玩家只需要直接瞄准他的目标并按下开火按钮即可(对于即时击中武器【9】)。不一致也时有发生,但是是在击中以后。
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.
例如,如果一个延时比较大的玩家击中一个延时比较小的玩家并且得到一分,低延时的玩家会感觉高延时玩家“在角落里被击中”【10】。这种情况下,低延迟玩家可能已经从角落里冲出,而高延时玩家看到的是过去的信息。每一个有延迟的玩家都有一个朝向别的玩家的直的视线,直的视线指向一个瞄准点然后开火。这个时候,低延时的玩家可能已经跑到角落里并且蹲在一个箱子后面,如果高延迟玩家延迟比较大,比如500ms,这是经常发生的;这样当高延时玩家的命令传到服务器的时候,已经隐藏起来的玩家需要取一个历史位置并计算是否击中,在这种极端情况下,低延时玩家会觉得他再角落里被击中了。然而,对于高延时玩家来说,他是正对着别的玩家开火的。从游戏设计的角度来讲,我们需要这样决定:让每个玩家即时与世界交互并开火。
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 first-person 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
10.总结
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.
延迟补偿是当前动作游戏改善延迟影响的一种方法。是否采用这种方法取决于游戏设计者,因为如何设计直接影响到游戏的体验。对于把那条命、军团要塞、cs这样的游戏,延迟补偿所带来的效果提升显著大于其带来的错误。
Footnotes
脚注
[1]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.
【1】在半条命引擎中,预测的过程中允许一定的延迟,但不能容忍实际网络延迟这么大的延迟。通过调整参数,我们可以控制预测过程中的延迟,这个参数pushlatency是一个负数,以毫秒为单位表示预测过程中的延迟。如果这个值大于(绝对值)实际网络延迟,这时预测就是完全的预测(译注:客户端服务器完全同步)。这种情况下玩家感觉不到任何延迟。实际应用中,一些人错误地认为参数pushlatency应该设为实际网络延迟的一半,这种情况下玩家移动仍然有网络延迟一半的延迟(感觉类似于冰面移动)。基于这个原因,实际应用总应该总是采用完全预测,pushlatency这个变量应该从半条命引擎中移除
[2]http://www.quakeforge.net/files/q1source.zip (Return)
[3]A discussion of cheating and what developers can do to deter it is beyond the scope of this paper. (Return)
【3】关于作弊和反作弊的问题超出了本篇文章讨论的范围
[4]Though hybrids and corrective methods are also possible. (Return)
【4】虽然混合纠正方法也可以使用
[5]"Jerk" is a measure of how fast accelerative forces are changing. (Return)
【5】“jerk”用来度量使玩家改变加速度的作用的快慢
[6]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. (Return)
【6】本文假设计算连接延时的时候客户端与服务器完全同步,也就是说,每次更新的时候客户端收到服务器发过来的时间被直接当做客户端的时间使用。这样,客户端跟服务器完全匹配,只是客户端稍微晚一点(晚多少取决于延时多少)。平滑客户端时钟差值可以有很多方法。
[7]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). (Return)
【7】更新时间间隔没必要是固定的。因为对于剧烈运动的游戏,如果带宽不够,很有可能客户端发过来的数据超过了处理能力。如果采用固定更新间隔,在发完一个更新包以后就需要等待一个固定更新周期时间以后再发下一个包。这种逻辑不能很好地使用带宽。因此,服务器发给每个客户端数据包以后,应该自己决定下一个包什么时候发,决定的依据是用户的带宽、用户设置的每秒更新频率。如果用户要求更新20次每秒,那么需要等待50ms以后下个更新包才能发送。如果激活了带宽限制(而服务器帧率又足够高),我们可能就需要等待比如61ms(或其他值)以后发送下一个更新包。因此,半条命游戏数据包发送间隔是随机的。基于服务器的这种情况,将启动点作为一个变量,移动到最新目标点进行插值这种方法效果欠佳。
[8]Which Half-Life encodes in the lerp_msec field of the usercmd_t structure described previously. (Return)
【8】半条命代码中usercmd_t结构中变量lerp_msec前面描述过。
[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). (Return)
【9】对于发射导弹的武器,延迟补偿有更多需要解决的问题。假如导弹是由服务器处理的,那么导弹应该位于哪个时间区间?每次导弹准备发射的时候,是否需要把每个玩家往后拉一段时间的?如果是这样,那么需要往后拉多少?这些问题是需要考虑的。在半条命中,为了避免这种问题,我们对导弹不进行延迟补偿(这并不意味着客户端不进行声音预测,只是实际的导弹不进行延迟补偿)。
[10]This is the phrase our user community has adopted to describe this inconsistency. (Return)
【10】用户社区通常采用这种情况来描述不一致性。