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
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

Trick #2: Extrapolation

Rather than halting the car until the next update comes, you can take advantage of the velocity information you've received and keep the car moving until you get the next update. This is a cheap and easy trick that smoothes over network lag issues.

Open OpponentCarController and add the following variable:

private Vector3 _lastKnownVel;

This variable will hold the Vector location of the last known velocity of the car.

Add the following line near the end of SetCarInformation, right before you set _lastUpdateTime:

_lastKnownVel = new Vector3 (velX, velY, 0);

This sets the last known velocity based off the velocity that was used to set the car's information.

Finally, modify the if block in Update() as follows:

if (pctDone <= 1.0) {
    // Previous code
    // ...
} else {
    // Guess where we might be
    transform.position = transform.position + (_lastKnownVel * Time.deltaTime);
}

This sets the position based off the velocity value.

Build and run your game again; only update one of your devices to compare the motion between the two versions of the game. You should see a lot less lurching around than before.

Note: While your game certainly looks better, it's actually less accurate than before. Why? When you receive an update from another player, you're seeing their position at some point in the past.

Adding this extrapolation means it takes you longer to bring the player to the point where they were a few milliseconds ago! And if your opponent's updates take too long to arrive, their car will calmly drive off the edge of the playing field.

This loss of accuracy probably doesn't matter much in this game; if you needed to be more accurate, you could try to guess the position of your opponent's car — and you might end up with some slightly more accurate car positions.

This would probably involve calculating some accurate ping times for your clients, as well as adding a bit of AI. This is beyond the scope of this tutorial, but it would make for some great experience with AI and game physics if you wanted to tackle it on your own!

Note: While your game certainly looks better, it's actually less accurate than before. Why? When you receive an update from another player, you're seeing their position at some point in the past.

Adding this extrapolation means it takes you longer to bring the player to the point where they were a few milliseconds ago! And if your opponent's updates take too long to arrive, their car will calmly drive off the edge of the playing field.

This loss of accuracy probably doesn't matter much in this game; if you needed to be more accurate, you could try to guess the position of your opponent's car — and you might end up with some slightly more accurate car positions.

This would probably involve calculating some accurate ping times for your clients, as well as adding a bit of AI. This is beyond the scope of this tutorial, but it would make for some great experience with AI and game physics if you wanted to tackle it on your own!

So you've got some smooth looking car action — but you still don't have a way to end the game. Time to fix that!

Finishing a Game

In this game, the race is won once a competitor completes three laps. At that point, you need to do three things:

  1. Stop the local player's car from moving.
  2. Send a "game over" message to all the other players once you've finished and note your final time.
  3. Show the game over screen once you've received a "game over" message from all other players.

Why do you need to send your final time in step 2 — why not just mark the order that you receive a "game over" message? Like everything in multiplayer games, it comes down to network lag. Imagine a situation where Player 1 finishes first, but by the time she sends a "game over" message to Player 2, Player 2 has already finished his game, so he thinks he's finished first.

And since all players in your networked game don't necessarily start at the exact same time, a faster player could receive a "game over" message by the slower player before they finish!

Crazy Time!

You can't rely on the order of messages to carry meaning in a networked game. You can only guarantee a fair resolution to the match by letting each client report their finishing times to all other clients.

Sending a Game Over Call

Open GameController find the following code in Update():

    if (_multiplayerGame) {
        // TODO: Probably need to let the game know we're done.
    } else {

...and replace it with the following code:

  if (_multiplayerGame) {
    // 1
    myCar.GetComponent<CarController>().Stop();
    // 2
    MultiplayerController.Instance.SendMyUpdate(myCar.transform.position.x,
                                                myCar.transform.position.y,
                                                new Vector2(0,0),
                                        myCar.transform.rotation.eulerAngles.z);
    // 3
    MultiplayerController.Instance.SendFinishMessage(_timePlayed);

  } else {

Looking in depth at the logic you added above:

  1. First, you tell the car controller to stop moving, at which point it will ignore all further updates from the interface.
  2. Next, you send an update to all other players with your current position, and a velocity of 0,0. This ensures your car stops at the finish line on all game clients.
  3. Finally, you call SendFinishMessage, which is a yet-to-be-written method in MultiplayerController which tells all of your opponents you've finished.

Open MultiplayerController and add the following private variable near the beginning of your class to track the length of the message:

// Byte + Byte + 1 float for finish time
private int _finishMessageLength = 6;

Now create the following method just after SendMyUpdate():

public void SendFinishMessage(float totalTime) {
    List<byte> bytes = new List<byte>(_finishMessageLength); 
    bytes.Add (_protocolVersion);
    bytes.Add ((byte)'F');
    bytes.AddRange(System.BitConverter.GetBytes(totalTime));  
    byte[] messageToSend = bytes.ToArray ();
    PlayGamesPlatform.Instance.RealTime.SendMessageToAll (true, messageToSend);
}

This is quite similar to SendMyUpdate(); the 'F' character lets other players know this is an "I'm done and here's my final time" call, followed by a float value containing the total elapsed time.

A big difference is the true argument in the SendMessageToAll call, which sends this message reliably, instead of via the unreliable mechanism you've used all along. Why? This time it's extra-super-important that all players know your car has finished. If this message were lost, your game would never end.

Even though it's important that your message is delivered, the timing of your message doesn't matter all that much. Even if this call was delayed by two or three seconds, it just means that the final screen wouldn't appear for a few seconds.

Todd Kerpelman

Contributors

Todd Kerpelman

Author

Over 300 content creators. Join our team.