(翻译 gafferongames)Networked Physics
原文:
https://gafferongames.com/post/networked_physics_2004/
针对First FPS游戏
这篇文章主要说了三点:
1.Server端怎么处理Client发送过来的Input行为
2.ClientB怎么模拟Server发送过来的ClientA的State(Position和Rotation)
3.本地客户端ClientA的State预测以及在收到Server数据后进行的修正
当然这里很多细节没有说明,只是给了思路。
First Person Shooters
First person shooter physics are usually very simple. The world is static and players are limited to running around and jumping and shooting.
第一人称射击(FPS)游戏的物理通常非常简单。游戏世界是静态的,玩家只能跑动、跳跃和射击。(这篇文章在这个前提下展开)
Because of cheating, first person shooters typically operate on a client-server model where the server is authoritative over physics. This means that the true physics simulation runs on the server and the clients display an approximation of the server physics to the player.
FPS 游戏通常采用客户端-服务器(client-server)模型,其中服务器对物理计算具有权威性(authoritative)。也就是说,真正的物理模拟运行在服务器端,而客户端只是向玩家展示服务器物理计算的近似结果。
The problem then is how to allow each client to control his own character while displaying a reasonable approximation of the motion of the other players.
问题是如何让每个客户端控制自己的角色,同时合理地显示其他玩家的运动?
In order to do this elegantly and simply, we structure the physics simulation as follows:
-
Character physics are completely driven from input data.
-
Physics state is fully encapsulated in a state structure.
为了以优雅且简单的方式解决这个问题,我们对物理模拟进行如下结构化设计:
- 角色的物理完全由输入数据驱动。
- 物理状态被完全封装在一个状态结构体中。
To do this we need to gather all the user input that drives the physics simulation into a single structure and the state representing each player character into another.
Here is an example from a simple run and jump shooter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | struct Input { bool left; bool right; bool forward; bool back; bool jump; }; struct State { Vector position; Vector velocity; }; |
Next we need to make sure that the simulation gives the same result given the same initial state and inputs over time. Or at least, that the results are as close as possible. I’m not talking about perfect floating point determinism here, just a reasonable 1/2 second prediction giving approximately the same result.
这里并不要求完美的浮点数确定性(floating point determinism),但至少要保证大约0.5 秒的预测能够给出相似的结果。
Network Fundamentals 网络基础
I will briefly discuss actually networking issues in this section before moving on to the important information of what to send over the pipe. It is after all just a pipe after all, networking is nothing special right? Beware! Ignorance of how the pipe works will really bite you. Here are the two networking fundamentals that you absolutely need to know:
在本节中,我会先简要讨论实际的网络问题,然后再深入讲解该在网络上传输哪些数据。毕竟,网络本质上就像一根管道(pipe),对吧?没什么特别的?
Number one. If your network programmer is any good at all he will use UDP, which is an unreliable data protocol, and build some sort of application specific networking layer on top of this. The important thing that you as the physics programmer need to know is that you absolutely must design your physics communication over the network so that you can receive the most recent input and state without waiting for lost packets to be resent. This is important because otherwise your physics simulation will stall out under bad networking conditions.
1. 使用 UDP,并设计适用于应用层的网络架构
你的物理数据通信必须支持直接接收最新的输入和状态,而不能依赖丢失的数据包被重新传输。
**为什么?**因为如果你等待丢失的数据包被重发,物理模拟可能会在糟糕的网络环境下卡死,严重影响游戏体验。
Two. You will be very limited in what can be sent across the network due to bandwidth limitations. Compression is a fact of life when sending data across the network. As physics programmer you need to be very careful what data is compressed and how it is done. For the sake of determinism, some data must not be compressed, while other data is safe. Any data that is compressed in a lossy fashion should have the same quantization applied locally where possible, so that the result is the same on both machines. Bottom line you’ll need to be involved in this compression in order to make it as efficient as possible without breaking your simulation.
2. 受限的带宽与数据压缩
网络带宽极为有限,因此你无法随意发送所有数据,而数据压缩是必须面对的现实。
你需要非常小心哪些数据可以被压缩,以及如何进行压缩:
- 某些数据不能被压缩,否则会破坏确定性(determinism)。
- 某些数据可以安全压缩,但如果是有损压缩(lossy compression),那么在本地也应该应用相同的量化处理(quantization),确保不同机器上的计算结果一致。
总结
你必须深度参与数据压缩的设计,以确保在尽可能提高传输效率的同时,不破坏物理模拟的稳定性。
Physics Runs On The Server
The fundamental primitive we will use when sending data between the client and the server is an unreliable data block, or if you prefer, an unreliable non-blocking remote procedure call (rpc). Non-blocking means that the client sends the rpc to the server then continues immediately executing other code, it does not wait for the rpc to execute on the server! Unreliable means that if you call the rpc is continuously on the the server from a client, some of these calls will not reach the server, and others will arrive in a different order than they were called. We design our communications around this primitive because it suits the transport layer (UDP).
在客户端与服务器之间发送数据时,我们使用的基本通信方式是不可靠的数据块(unreliable data block),或者说是不可靠的非阻塞远程过程调用(non-blocking RPC)。
- 非阻塞(non-blocking):客户端向服务器发送 RPC 后,不会等待服务器执行完成,而是立刻继续执行其他代码。
- 不可靠(unreliable):如果客户端持续向服务器调用 RPC,部分调用可能丢失,而另一些可能以不同的顺序到达服务器。
我们的通信方案基于这种方式设计,因为它最适合 UDP 传输层。
The communication between the client and the server is then structured as what I call a “stream of input” sent via repeated rpc calls. The key to making this input stream tolerant of packet loss and out of order delivery is the inclusion of a floating point time in seconds value with every input rpc sent. The server keeps track of the current time on the server and ignores any input received with a time value less than the current time. This effectively drops any input that is received out of order. Lost packets are ignored.
客户端与服务器的通信采用输入数据流(stream of input)的方式,即客户端不断通过 RPC 发送输入数据。
为了使输入流能够容忍数据包丢失和乱序,每个输入 RPC 调用都附带一个 浮点数时间戳(秒)。
- 服务器记录当前时间,并忽略所有时间戳小于当前时间的输入,这可以丢弃乱序到达的数据。
- 丢失的数据包不会被重传,直接忽略。
Thinking in terms of our standard first person shooter, the input we send from client to server is the input structure that we defined earlier:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | struct Input { bool left; bool right; bool forward; bool back; bool jump; }; class Character { public : void processInput( double time , Input input ); }; |
Thats the bare minimum data required for sending a simple ground based movement plus jumping across the network. If you are going to allow your clients to shoot you’ll need to add mouse input as part of the input structure as well because weapon firing needs to be done server side.
Notice how I define the rpc as a method inside an object? I assume your network programmer has a channel structure built on top of UDP, eg. some way to indicate that a certain rpc call is directed as a specific object instance on the remote machine.
So how does the server process these rpc calls? It basically sits in a loop waiting for input from each of the clients. Each character object has its physics advanced ahead in time individually as input rpcs are received from the client that owns it. This means that the physics state of different client characters are slightly out of phase on the server, some clients being a little bit ahead and others a little bit behind in time. Overall however, the different client characters advance ahead roughly in sync with each other.
那么,服务器如何处理这些 RPC 调用?
- 服务器在循环监听来自客户端的输入。
- 每个角色对象的物理状态会在接收到对应客户端的输入时向前推进(也就是说,Server针对每个Client数据是独立处理的)
- 由于客户端数据到达时间不同,服务器上的各个角色对象的物理状态可能稍有时间偏差,有些客户端可能略超前,有些可能略滞后,但总体上它们是大致同步的。
Lets see how this rpc call is implemented in code on the server:
1 2 3 4 5 6 7 8 9 | void processInput( double time , Input input ) { if ( time < currentTime ) return ; float deltaTime = currentTime - time ; updatePhysics( currentTime, deltaTime, input ); } |
The key to the code above is that by advancing the server physics simulation for the client character is performed only as we receive input from that client. This makes sure that the simulation is tolerant of random delays and jitter when sending the input rpc across the network.
- 服务器仅在收到客户端输入时推进该角色的物理模拟,不会等待丢失的数据包。
- 这样可以容忍网络延迟和抖动(jitter),确保物理模拟的稳定性。
Clients Approximate Physics Locally
Now for the communication from the server back to the clients. This is where the bulk of the server bandwidth kicks in because the information needs to be broadcast to all the clients.
接下来,我们讨论服务器如何向客户端发送数据。
这一部分通常占用服务器的大部分带宽,因为服务器需要将信息广播给所有客户端。
What happens now is that after every physics update on the server that occurs in response to an input rpc from a client, the server broadcasts out the physics state at the end of that physics update and the current input just received from the rpc.
服务器向客户端发送物理状态
当服务器处理完某个客户端的输入 RPC 并完成物理更新后,它会广播当前物理状态以及刚刚收到的输入给所有客户端。
This is sent to all clients in the form of an unreliable rpc:
这种更新是通过不可靠的 RPC(unreliable RPC) 进行的,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void clientUpdate( float time , Input input, State state ) { Vector difference = state.position - current.position; float distance = difference.length(); if ( distance > 2.0f ) current.position = state.position; else if ( distance > 0.1 ) current.position += difference * 0.1f; current.velocity = velocity; current.input = input; } |
What is being done here is this: if the two positions are significantly different (>2m apart) just snap to the corrected position, otherwise if the distance between the server position and the current position on the client is more than 10cms, move 10% of the distance between the current position and the correct position. Otherwise do nothing.
客户端如何处理服务器状态更新?
- 如果客户端位置和服务器位置相差过大(> 2m),直接校正到服务器位置。
- 如果位置误差在 10cm 以上,则按 10% 的比例逐渐靠近正确位置(平滑修正)。
- 如果误差小于 10cm,则不进行修正。
- 速度(velocity)直接采用服务器值,而不进行平滑修正。
- 同步服务器端输入状态(input),以便客户端内部逻辑保持一致
Since server update rpcs are being broadcast continually from the server to the the clients, moving only a fraction towards the snap position has the effect of smoothing the correction out with what is called an exponentially smoothed moving average.
This trades a bit of extra latency for smoothness because only moving some percent towards the snapped position means that the position will be a bit behind where it should really be. You don’t get anything for free.
为什么要逐步靠近正确位置?
服务器会不断广播更新,如果直接瞬间同步位置,玩家会看到角色瞬移(teleporting)。
所以,我们采用指数平滑移动平均(Exponentially Smoothed Moving Average, ESMA)的方式来平滑校正,避免突兀的跳跃:
- 缺点:由于每次只移动部分距离,客户端的角色位置会比服务器位置稍微滞后,增加了一点延迟。
- 优点:大幅提升平滑度,减少视觉抖动和角色瞬移。
I recommend that you perform this smoothing for immediate quantities such as position and orientation, while directly snapping derivative quantities such as velocity, angular velocity because the effect of abruptly changing derivative quantities is not as noticeable.
Of course, these are just rules of thumb. Make sure you experiment to find out what works best for your simulation.
哪些数据应该平滑修正,哪些应该直接同步?
- 位置(Position)、方向(Orientation) → 逐步平滑修正(减少视觉突变)。
- 速度(Velocity)、角速度(Angular Velocity) → 直接同步(因为这些变化较快,突变不明显)。
这只是一个经验法则,具体实现应根据模拟需求调整,不断测试和优化,找到最合适的平衡点。
Client-Side Prediction
So far we have a developed a solution for driving the physics on the server from client input, then broadcasting the physics to each of the clients so they can maintain a local approximation of the physics on the server. This works perfectly however it has one major disadvantage. Latency!
When the user holds down the forward input it is only when that input makes a round trip to the server and back to the client that the client’s character starts moving forward locally. Those who remember the original Quake netcode would be familiar with this effect. The solution to this problem was discovered and first applied in the followup QuakeWorld and is called client side prediction. This technique completely eliminates movement lag for the client and has since become a standard technique used in first person shooter netcode.
在《QuakeWorld》中,id Software 率先引入了“客户端预测(Client-Side Prediction)”技术,彻底消除了移动延迟问题。这种技术已经成为现代 FPS 游戏的标准。
Client side prediction works by predicting physics ahead locally using the player’s input, simulating ahead without waiting for the server round trip. The server periodically sends corrections to the client which are required to ensure that the client stays in sync with the server physics. At all times the server is authoritative over the physics of the character so even if the client attempts to cheat all they are doing is fooling themselves locally while the server physics remains unaffected. Seeing as all game logic runs on the server according to server physics state, client side movement cheating is basically eliminated.
原理:
- 客户端在本地提前预测物理模拟,基于玩家输入进行自主计算,不等待服务器的返回。
- 服务器定期发送校正数据,确保客户端与服务器保持同步。
- 服务器始终拥有物理权威性,即使客户端试图作弊,也只能欺骗自己,服务器的物理模拟不会受到影响。
- 所有游戏逻辑仍然在服务器端执行,因此客户端修改数据不会影响游戏进程,基本上消除了基于移动的作弊可能。
The most complicated part of client side prediction is handling the correction from the server. This is difficult, because the corrections from the server arrive in the past due to client/server communication latency. We need to apply this correction in the past, then calculate the resulting corrected position at present time on the client.
客户端如何处理服务器校正?
客户端预测的最大难点是如何处理服务器的物理校正。
- 由于网络延迟,服务器的校正数据实际上是过去的状态。
- 客户端必须去应用校正,然后重新计算当前的正确位置
The standard technique to do this is to store a circular buffer of saved moves on the client where each move in the buffer corresponds to an input rpc call sent from the client to the server:
客户端维护一个循环缓冲区(circular buffer),存储每次输入对应的物理状态:
1 2 3 4 5 6 | struct Move { double time ; Input input; State state; }; |
When the client receives a correction it looks through the saved move buffer to compare its physics state at that time with the corrected physics state sent from the server. If the two physics states differ above some threshold then the client rewinds to the corrected physics state and time and replays the stored moves starting from the corrected state in the past, the result of this re-simulation being the corrected physics state at the current time on the client.
当客户端收到服务器的校正数据时,它会:
- 在缓冲区中找到对应时间的存档,并比较当前状态与服务器校正状态。
- 如果两者之间的误差超过某个阈值,客户端回溯到服务器提供的正确状态。
- 从该校正状态开始,重新执行存储的输入操作,重新模拟物理运动,计算当前的正确状态。
Sometimes packet loss or out of order delivery occurs and the server input differs from that stored on the client. In this case the server snaps the client to the correct position automatically via rewind and replay. This snapping is quite noticeable to the player, so we reduce it with the same smoothing technique we used above for the other player characters. This smoothing is done after recalculating the corrected position via rewind and replay.
如何处理丢包与乱序数据?
- 有时数据包丢失或乱序,导致服务器的输入状态与客户端存储的不同。
- 在这种情况下,服务器会强制让客户端回溯到正确的位置,然后进行回放(rewind & replay)。
- 但这种“瞬间修正”可能会让玩家感觉到突兀的“位置跳变”(Snap Correction)。
解决方案:平滑处理
- 在回放结束后,对最终的修正状态应用指数平滑移动平均(ESMA),就像我们之前对其他玩家角色位置所做的那样。
- 这减少了视觉上的突兀感,让修正过程更加平滑自然。
Conclusion
We can easily apply the client side prediction techniques used in first person shooters to network a physics simulation, but only if there is a clear ownership of objects by clients and these object interact mostly with a static world.
- 客户端预测可以显著减少输入延迟,让玩家的操作更加流畅。
- 服务器仍然是最终权威,即使客户端试图作弊,服务器仍然保持正确状态。
- 回溯与重放(Rewind & Replay) 机制用于处理服务器校正,但需要平滑处理以避免突兀跳变。
- 客户端预测适用于第一人称射击(FPS)等游戏,但前提是游戏对象的所有权是明确的,并且主要与静态世界交互。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
2023-02-26 C#和Net框架的关系(C#图解教程第一章)