unnreal所采用的网络和物理模拟(ZT)


原文:http://www.gaffer.org/articles/NetworkedPhysics.html

Networked Physics

Networking a physics simulation is the holy grail of multiplayer gaming and the massive popularity of multiplayer first person shooters on the PC is a testament to the just how immersive networked physics simulations can be. In this article I will present the key techniques used in first person shooters and how to apply them to your own physics simulations.


First person shooters

Character physics in first person shooters today is typically very simple. Usually the simulation is completely linear (no rotation) and the player is limited to running around and jumping and shooting. 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.

The problem then is how to allow each client to control his own character remotely on the server and display a realistic approximation of the server physics on each of the clients. In order to do this elegantly and efficiently we need to structure our physics simulation in the following way:

    1. character physics are completely driven from input data
    2. physics state is known and can be fully encapsulated in a state structure
    3. the physics simulation is deterministic given the same initial state and input

This means that we need to structure all the user input that drives the physics simulation into a single structure and another structure for the current physics state. Here is an example from a simple run and jump shooter:

    struct Input
    {
         bool left,
         bool right,
         bool forward,
         bool back,
         bool jump;
    };

    struct State
    {
         Vector position;
         Vector velocity;               
    };

Structuring your simulation in this way is extremely important. If you neglect to do this then the techniques presented in the article will simply not work. And, to be absolutely clear, you need to make sure that you physics simulation gives exactly the same result each time it runs given the same initial physics state and input over time.


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:

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 it can handle packets arriving out of order, or worst case never arriving at all. This is important because otherwise your physics simulation will stall while waiting for out of order packets to be revolved and lost packets to be resent.

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. The bottom line is that you'll need to be involved in this compression in order to make it as efficient as possible without breaking your simulation.


Physics is run on the server according to a stream of input from clients

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 underlying unreliable data pipe.

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 ignores input that is received out of order. There is nothing we can do about lost packets - they are gone.

Thinking in terms of our standard first person shooter, the input sent from client is simply the input structure that we defined earlier sent via rpc:

    struct Input
    {
         bool left,
         bool right,
         bool forward,
         bool back,
         bool jump;
    };

    class Character
    {
    public:
         void receiveInput(float time, Input input);          // rpc method called on server
    };

Now thats not a lot of data and it is just the bare minimum required for sending a simple ground based movement plus jumping type fps movement across the network. Note that you can pack the strafe input state input into less than 4 bits (try to work out how) but I will leave it this way for clarity. Also, if you are going to allow your clients to shoot you'll need to add their aiming orientation in as part of the input structure because weapon firing needs to be done server side. Finally, notice how I define the rpc as a method inside an object. I will assume that 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's characters on the server are generally slightly out of sync, some clients being a little bit ahead and others behind. Overall however, input is sent in a constant stream from each of the clients to the server, meaning that all the different client objects advance ahead roughly in sync with each other, even with the presence of packet loss and out of order delivery.

Lets see how this rpc call is implemented in code on the server:

    void receiveInput(float time, Input input)
    {
         if (time<currentTime)
              return;

         const float deltaTime = currentTime - time;

         updatePhysics(currentTime, deltaTime, input);
    }

The key to the code above is that by advancing the server physics simulation in lockstep with the client input, we make sure that the simulation is isolated from random delays that occur when sending the input rpc across the network and ensure that the server keeps in sync with the client's own concept of current time. This technique only works where a client clearly owns each object on the server such as player characters in fps games.


Clients approximate server 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. This is sent to all clients in the form of an unreliable rpc:

    void clientUpdate(float time, State state, Input input)
    {
         if (time<lastClientUpdateTime)
              return;

         lastClientUpdateTime = time;

         if ((state.position-currentState.position).length()>threshold)
              currentState.position += (position - currentState.position) * 0.5f;

         currentState.velocity = velocity;

         currentInput = input;
    }

Now for some important information about this method. Firstly, consider the time parameter optional if bandwidth is tight. It is useful if you can get away with it because you can use it to reject out of order packets on clients as we do in the code above, but thats all it is used for. Next, the way that the current client position is snapped to the corrected position is complex and requires explanation.

Exactly what is being done here is this: If the distance between the server position and the current position is greater than a threshold, move 50% of the distance between the current position and the snapped position. If the two positions are within some threshold distance (say 2-3cms) do nothing. 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 out the snap with what is called an exponentially smoothed moving average. This means that instead of snapping directly to the target position it smoothly interpolates towards it over time.

This trades a bit of extra latency for smoothness because only moving halfway 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. Generally you should do this smoothing for immediate quantities such as position and orientation, while directly snapping derivative quantities such as velocity, angular velocity and so on. This is because the effect of abruptly changing derivative quantities is not as noticeable. Make sure you experiment to find out what works best for your simulation.

Finally, it is extremely important to understand that this rpc code, unlike the one sent from the client to the server, does not perform any physics updating. All it does is snap the client to the current physics state as specified by the server, then update the current input values for the character. These current input values are then used in the normal physics update elsewhere which advances physics forward in dt increments based on local time. This effectively extrapolates client side physics from the last physics state and input received from the server.


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 netplay in the original Quake 1 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's own character and has since become a standard technique used in first person shooter netcode.

Client side prediction works by predicting physics ahead locally using the player's input directly 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 eliminated.

The most complicated part of client side prediction is handling the correction from the server. This technique 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:

    struct Move
    {
         float time;
         Input input;
         State state;
    };

The circular buffer stores the last n moves performed on the client. The actual number of moves is application dependent, but lets say the buffer has a maximum capacity of 1024 moves. Moves are stored in chronological order from head to tail, so if the tail of the buffer runs past the head of the circular buffer, we just advance the head index, effectively overwriting the oldest move in the buffer with the newest.

This buffer of stored moves is required because when the server sends a correction to the client, it is a correction in the past as the client has already predicted ahead locally. 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 back in order to get the corrected physics state at the current time on the client:

    const int maximum = 1024;

    Move moves[maximum];
    
    void advance(int &index)
    {
         index ++;
         if (index>=maximum)
              index -= maximum;
    }

    int head = 0;
    int tail = 100;          // lets assume 100 moves are currently stored

    void clientCorrection(float time, State state, Input input)
    {
         while (time>moves[index].time && head!=tail)
              advance(head);          // discard old moves

         if (head!=tail && time==moves[head].time)
         {
              if ((moves[head].state.position-currentState.position).length>threshold)
              {
                   // rewind and apply correction
              
                   currentTime = time;
                   currentState = state;
                   currentInput = input;

                   advance(head);          // discard corrected move

                   int index = head;

                   while (index!=tail)
                   {
                        const float deltaTime = moves[index].time - currentTime;

                        updatePhysics(currentTime, deltaTime, currentInput);

                        currentTime = moves[index].time;
                        currentInput = moves[index].input;

                        moves[index].state = currentState;

                        advance(index);
                   }
              }     
         }
    }

This is quite a complicated function. Read it carefully because if I explained it in words it would take several pages of text alone. Just remember that its function is to rewind then replay back physics from a corrected state in the past, in order to determine the correct state at the current time on the client. While it replays the moves it rejects moves older than the current correction from the server (they are no longer needed), and automatically updates the physics state in each of the saved moves it plays back.

There is of course nothing we can do to stop some snapping when packet loss or out of order delivery occurs and the server input differs from that stored on the client. In this case the server will snap the client to the correct position automatically and afterwards is well. If this snapping is annoying to the end user we can reduce it by using an exponentially smoothed moving average as we did with the client physics approximation earlier.


Disadvantages of client side prediction

Client side prediction seems to good to be true. With one simple trick we can eliminate latency when the client character moves. But at what cost? The answer is that whenever two physics simulations on the server interact then snapping will occur on the clients. What happened? In effect, the client predicted the physics ahead using its own approximation of the world but ended up at an entirely different position from the physics on the server. Snap.

In our simple run and jump fps, this situation would occur if one player runs into another, tries to stand on another player's head or gets knocked back by an explosion. The bottom line is that any change to the physics of a character that occurs on the server that is not directly related to a change in input sent from its owner client will cause snapping. In truth there is no way to avoid this. This is simply the cost of using client side prediction.

This leads me to an interesting point. The evolution of networked physics games is from first person shooters where characters run around in a static world while shooting each other, towards a dynamic world where players interact with the surroundings and each other. Given this trend I am willing to make the following bold prediction - client side prediction will soon be obsolete. I believe that the next generation of networked games will not use client side prediction because the snapping it would cause will be unacceptable. This generation of games will trade latency for an increased depth of physics interaction.


General networked physics

The solutions presented so far work great for first person shooter games. The key is that there is a clear ownership of objects by a certain client, meaning that in the vast majority of cases the owner client is the only influence on the physics of a physics object being networked. It is this simplifying assumption that enables all of the common techniques and tricks used when writing netcode for first person shooters. If your physics simulation is structured in this way, then these techniques are perfect for you. For example, a car racer has each client controlling a single car could extend this technique by simply adding more physics state and input.

But what if the simulation you want to network has no clear concept of object ownership? For example, consider a physics simulation of a stack of blocks where each client is free to click and drag to move the blocks around. In this case no client owns an object, and there could even be multiple clients pulling on the same block simultaneously.

In a case such as this, more general techniques need to be used. For starters, client side prediction is obviously out because the snapping that it would cause would be unacceptable. So the key then is to run the physics simulation in such a way that the stream of input is sent from the clients to the server but the server does not wait for a clients input before proceeding with the simulation, because the server has no concept of a client owning the object like a player client owns their character in an first person shooter.

This means that the server physics update will actually look more traditional because all objects would be updated simultaneously on the server using the last known input from the clients. This makes the server simulation much more susceptible to network problems such as delayed delivery of packets, bunching up of input data arrival on the server, plus work needs to be done to keep the client's concept of time in sync with that of the server. Solving these problems in general networked physics will be a challenge, and I hope to present solutions and sample source code for multiplayer interaction with a stack of objects in a future article.


Conclusion

Networking physics seems complicated but once you understand the core techniques used in first person shooters its easy to implement. The trick is to ensure that your physics simulation is deterministic and driven by input so you can send your physics state and input across the network and everything will stay in sync. If you implement this technique correctly then snapping will only ever occur when packets are lost or received out of order, or when client side prediction mispredicts based on an unforeseen event occuring on the server.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值