Networking II: Networking in the Real World

In the previous lecture, we discussed basic networking -- getting a a simple networked game to work using XNA, assuiming that every process could communicate with every other process on every frame. This lecture will concentrate on how to handle real-world networking problems -- less frequent communication, lag, and dropped packets

Problem 1: Not getting Updates Every Frame

In our last lecture (and in-class assignment), we assumed that we received network information every frame. While this works just fine over a local network, it is less reasonable over the internet -- you just can't expect to get 60 updates (back and forth!) every second. Consider the simple pong game that you implemented last class -- if you only updated the opponent's possition 10 times a second, while your own position was updated 60 times a second, then there would be noticable jitter in the opponents paddle -- it would click up and down instead of moving smoothly

Demonstration

How could we fix this problem? An obvious answer is to send not only the position of the paddle, but also the velocity of the paddle, and update the position every frame, based on the old velocity. In general, we can simulate the position of the other player as if it was running on our machine, and update the position when we get fresh updates. It is often handy to pass not only the position and velocity of the player, but also the current controller state -- when simulating the player moving forward, we can assume that the controller state does not change

For insteance, consider a simple platformer game. Each player controls a character that can run left and right, as well as jump. When jumping, the player has limited air control over the character -- the velocity of the character in the air can be modified (up to a certain velocity) -- so if you jump to the right, and then hold the stick to the left, you will slow down the horizontal velocity over time while in the air, perhaps even turning around completely. The position of the character in the next frame is not just a function of the initial velocity of the character, but also a function of the current state of the controller.

If, on the last time you got a network update, the other player was moving the stick to the left, you can assume that the stick is likely still to the left, and run your simulation accordingly. You are updating the position of the other player, as if the other player was playing on your machine, giving the exact same controller input that existed the last time you got an update

As long as the other player does what you expect, then you get good, smooth gameplay. There is a cost however -- what happens if the player was moving to the right just as we got an update, and as soon as the update went out, the player started moving to the left? Then, on the next network update, we'll get a pop as we correct the position of the player -- and this pop will be bigger than the pop that we would have gotten if we hadn't done any simulation of the other player:

We can't make this problem go away -- if we don't know where the player is, then we don't know where the player is! However, we can mitiage the problem a bit by not moving the position of the player on the screen to the exact correct location, but instead blending the known actual position of the other player after the network update with the previous simulation of the player (from right before the network update)

Lerp

A Lerp is Linear Interpolation. Given any two values x1 and x2, and a real number p between 0 and 1, we can do a linear interpolation of p from x1 to x2 follows:

x1 + (x2-x1) * p

We can Lerp floating point values, vectors -- even colors.

Simulated Postion version Visual Position

Note that we will only do this smoothing for the visual appearance of the opponent. For gameplay purposes (collitions detection, etc) we will always use the most actual simulated position for the player. We are only displying an interpolated value to make the movement look more smooth.

Problem 2: Lag

OK, so we can now have nice smooth motion, even when we are only updating 10 times a second. There is still a problem however -- network packets take time to get to us. So, if the one-way latency is 100 miliseconds, when we receive a packet from the other player, it doesn't describe the current state of the other player, but the state of the player 100 milseconds -- or 6 frames, at 60 fps -- ago. So, at best, if we ignore this latency, the other player will lag behind his/her input by 6 frames. It gets worse -- the one-way latency is not constant, but can fluctuate, leading to not only a lagged response, but also flicker in the player's appearent movement -- even when the player is moving at a constant rate.

What could we do? The problem is relatively easy to solve if we have syncronized clocks between the two machines. We could send not only the position and velocity of our player, but also the current time. When we receive the packet, we can estimate the current position of the player. An easy (but potentially inaccruate) method of estimating the current position of the other player is to multiply the different in time between the network packet and the current clock time by the player's velocity, and add this number to the position send over the wire to get a better estimate of the other players current position. This works great for something like pong, but not so good for a platformer that has gravity. If our game has gravity, then the velocity of the other player will change over time -- and we are assuming that the velocity stays constant. Fortuneatly, since the change in velocity due to gravity is predicable, we could be more accurate by sending:

When we get a new packet from the other player, we can get our best guess of the current position as follows:

Note that if our game has a complicated interaction between the physics simulation and player input -- such as a platform game that has gravity and limited air control (when a character is in the air, it can be controlled in a limited way using the controller), sending the gamepad state and assuming that it doesn't change bewteen network packets allows use to get a fairly accurate estimate of the current position of the character.

There is, alas, one problem with this method -- it requires a syncronized clock, which we do not have. We can get an estimate of the average lag -- we can use this as the difference between when the packet was sent ans the current time. This would help with the laggy behavior, but would not help the jitter problem. If we had

then we could get a good estimate of when the packet was sent. As it turns out, we can get a pretty accurate average round-trip latency measure from XNA, which we can divide by 2 to get the actual latency. This will only give us an average, not the latency of the most recent packet, which is what we want. What can we do?

We will send the current time with each packet. Every time we get a new packet, we will record the delta time between when the packet was sent, and when it was received (by our clock). Now, that number may not be at all accurate if our two clocks are not in sync. However, we can keep track of the average delta between the packet send time and the packet received time. We can do this with a rolling average -- keep track of the last 100 deltas we've seen, and compare the new delta to the recent average. If the new delta is less than the average, then this packet has come a little eariler than normal If the new delta is larger than the average, then this packet has come a little later than normal. So, a good guess as to how long ago this packet was sent is:

Putting It All Together

We can combine Network Prediction (Lag Hiding) and smoothing to get resonably smooth play, even with a suboptimal network

In-Class Project

Download the following zip file

This file contains a (mostly) working simple platormer-style game. Once you've downloaded it and got it running, you need to make the following changes

Even More Fun Stuff

Quake3 and lag compensation for hit-scan weapons