Creating a Cross-Platform Multi-Player Game in Unity — Part 3

In the third part of this tutorial, you’ll learn how to deal with shaky networks, provide a winning condition, and deal with clients who exit the session. By Todd Kerpelman.

Leave a rating/review
Save for later
Share

In part one, you developed your game to the point where your game clients could connect to each other.

In part two, you allowed clients to join a game room, send their current position to each other, and update the game play based on the positions received. It’s starting to look like a real game

However, you uncovered a few things in that need to be addressed: you’re making network calls more frequently than you need to; the cars’ movements look a little jittery and there’s no way to end the game! You’ll take care of these things in this part of the tutorial series — and learn some game design tips along the way.

In this tutorial, you will do the following:

  • Learn some of the various techniques with handling network latency.
  • Implement a game over state.
  • Provide various ways for clients to disconnect from a game.

And when you finish, you’ll have an honest-to-goodness multiplayer game.

Reducing Network Traffic

You’re currently sending updates once every frame, which is way more than you need. Frequent updates can ensure smooth gameplay, but that comes at the cost of data usage, battery drain and potentially clogging the network by sending more updates than it can handle.

So how frequently should you send updates? The annoyingly vague answer is: “as often as you need to keep the gameplay good, but no more than that.” Frankly, update frequency is always a delicate balancing act with no “perfect” solution. For this tutorial, you’ll reduce your network update calls to six per second, which is an arbitrary amount, but it’s a good place to start.

An Aside on Compression Strategies

Cutting down your network calls from 30 times a second to 6 is a pretty good way to reduce your network traffic, but you could do even more if you wanted. For instance, you’re using a luxurious 4 bytes to represent the player’s rotation – which is probably overkill. You could reduce this down to a single byte if you took the player’s rotation (between 0 and 360), multiplied it by 256/360, and then rounded it to the nearest whole number. To translate it back, you would multiply that number again by 360/256.

There would be some loss of accuracy, of course; for instance a car originally with a z rotation of 11 degrees would end up as 11.25. But, really, is that something you would notice when these cars are going 300 pixels per second around a track?

// Converting degrees to 1 byte...
(int)(11.0 * 256 / 360) = 8

// Converting back again...
8 * 360.0 / 256.0 = 11.25

Similarly, you’re using 4 bytes each to represent your car’s position and velocity in the x and y axis. You could represent the velocity with a single byte in each axis, and you might even be able to do the same with its position. And suddenly, your update message goes from 22 bytes to 7 bytes each. What a savings!

This might seem like you’re going a little nutty with compression, particularly if you’re coming from the web world where you’re probably used to dealing with messages represented as strings and numbers in a JSON blob. But real-time multiplayer game developers tend to worry about this stuff a little more than other, saner, folks.

Decreasing Update Frequency

Open your project. If you want to start with this part, feel free to download the starter project.

Note: You will have to configure the starter project to have your own ClientID as described in “>Part One

.

Open GameController.cs found in the scripts folder, and add the following variable near the top:

private float _nextBroadcastTime = 0;

This variable will hold the time to determine when the next time the script should broadcast client data.

Find the following line in DoMultiplayerUpdate:

MultiplayerController.Instance.SendMyUpdate(myCar.transform.position.x, 
                                            myCar.transform.position.y,
                                            myCar.rigidbody2D.velocity, 
                                            myCar.transform.rotation.eulerAngles.z);

…and modify it as shown below:

if (Time.time > _nextBroadcastTime) {
    MultiplayerController.Instance.SendMyUpdate(myCar.transform.position.x, 
                                                myCar.transform.position.y,
                                                myCar.rigidbody2D.velocity, 
                                                myCar.transform.rotation.eulerAngles.z);
    _nextBroadcastTime = Time.time + .16f;
}

This sends updates once every 0.16 seconds, or about six times per second.

Build and run your project; update the game on one device only, so you can compare the two versions side-by-side. What do you notice about the gameplay?

My Eyes! The Goggles Do Nothing!

The gameplay looks terrible — it’s like you’ve replaced your awesome game with a slideshow! :[

So you need to bump up the rate of your network updates, right? Not so fast; there are a few tricks of the trade that can improve your cruddy slideshow experience — without bumping up the frequency of your update calls.

Trick #1: Interpolation

The first trick to improve your gameplay is to interpolate your opponent’s position; instead of teleporting your opponent’s car to its new position, you can smooth out the action by calculating the path the car should take instead.

Add the following variables to the top of OpponentCarController:

private Vector3 _startPos;
private Vector3 _destinationPos;
private Quaternion _startRot;
private Quaternion _destinationRot;
private float _lastUpdateTime;
private float _timePerUpdate = 0.16f;

Now add the following code to Start():

_startPos = transform.position;
_startRot = transform.rotation;

This initially sets the start position of the car at the start of the game.

Next, replace all of SetCarInformation() with the following:

public void SetCarInformation(float posX, float posY, float velX, float velY, float rotZ) {
    // 1
    _startPos = transform.position;
    _startRot = transform.rotation;
    // 2
    _destinationPos = new Vector3 (posX, posY, 0);
    _destinationRot = Quaternion.Euler (0, 0, rotZ);
    //3
    _lastUpdateTime = Time.time;
}

Taking each numbered comment in turn:

  1. Each time you receive a position update, keep track of the current position and rotation of the car.
  2. Next, record the new position and location.
  3. Finally, record the current time — you’ll see in a moment why this is necessary.

Now, add the following code to Update():

void Update () {
    // 1
    float pctDone = (Time.time - _lastUpdateTime) / _timePerUpdate;
    
    if (pctDone <= 1.0) {
        // 2
        transform.position = Vector3.Lerp (_startPos, _destinationPos, pctDone);
        transform.rotation = Quaternion.Slerp (_startRot, _destinationRot, pctDone);
    }   
}

This is not a lot of code, but it is a lot of math. Here's what's going on:

  1. pctDone stores how far along you should be on the interpolated path, based on the assumption that it takes approximately 0.16 seconds to process an update.
  2. Then you use the value in pctDone information to update your transform's position and rotation accordingly.

Build and run your game in Unity and update it on both devices; you should notice that the gameplay runs a little more smoothly.

Things still don't look perfect, though, as the cars still lurch around the track a bit — and it only gets worse if your network conditions deteriorate. Why? Your cars travel for 0.16 seconds until they reach their new position — and then they stop dead in their tracks until you receive the next update from the network. If that update takes more than 0.16 seconds to arrive, say 0.30 seconds, then the car will sit there for 0.14 seconds until it moves again.

So...is it time to bump up the rate of your network updates? No! :] You can fix the lurchiness with a second trick from the game developer's toolbox.

Todd Kerpelman

Contributors

Todd Kerpelman

Author

Over 300 content creators. Join our team.