Creating a Cross-Platform Multiplayer Game in Unity — Part 4

In this final part of the series, you will deal with clients sending messages out of order and the various ways on how to deal with them. By Todd Kerpelman.

Leave a rating/review
Save for later
Share

You’re not quite done; to put some polish on your app you’ll have to get your cross-platform app running properly on Android and let more than two people play at once among other things! However, they aren’t huge issues to fix — and you’ll take care of all of them in this part of the tutorial.

The most pressing problem to fix, however, is dealing with your unreliable messaging protocol and handling the inevitable out-of order messages.

In this tutorial, you will be learning the following:

  • How to respond to messages that are received out of order
  • How to cancel auto matchmaking
  • A whole lot of game theory regarding network connectivity

You can download the starter project over here.

If you are starting this project with the starter project, you will need to configure based on the first part of the tutorial series.

Handling Out of Order Messages

Since you’re likely testing with your devices on a fast internal network, there’s a good chance most of your messages arrive in order; throttling back on your updates as you did in the previous chapter also means that statistically, you’ll experience fewer out-of-order messages.

However, real users play games while on public transportation that meanders in and out of good cell coverage, and some players just have cruddy mobile connections all the time. You can easily imagine what might happen if you receive an update message out of order. Think about the following car traveling around the track with messages #1 through #6 received in order:

Cars in order

But what if message #3 wasn’t received until sometime after message #5? You’d end up with a zig-zagging car as the following image illustrates:

Zig Zagging Car!

The car will appear to teleport all over the place — what a mess! Here’s a very simple strategy you can use to manage this issue:

  • Anytime you send out an update message, start the message with a number that you automatically increment.
  • Anytime you receive an update message from another player, look at the auto-increment number; if it’s smaller than the number from the previous message you received, then it’s an out-of-order message, and you simply discard it.

In the example above, you would simply discard car location #3, since you received it after message #5. Sure, you’d be missing a location update, but the car would continue on with the same velocity from message #2 and recover when it received location #4:

Skipping in out of order message

Re-open the Circuit Racer project, then open MultiplayerController.cs in the scripts folder. Add the following variable to track your own auto-increment message number:

private int _myMessageNum;

Next, update _updateMessageLength, so that you have space for this new 4-byte integer.

// Byte + Byte + 1 int for message num + 2 floats for position + 2 floats for velcocity + 1 float for rotZ
private int _updateMessageLength = 26;

Add the following code to the OnRoomConnected callback, right before your Application.LoadLevel call:

_myMessageNum = 0;

This simply initializes _myMessageNum to a starting value.

Now, modify SendMyUpdate as follows to include the new message number:

public void SendMyUpdate(float posX, float posY, Vector2 velocity, float rotZ) {
    _updateMessage.Clear ();
    _updateMessage.Add (_protocolVersion);
    _updateMessage.Add ((byte)'U');
    _updateMessage.AddRange(System.BitConverter.GetBytes(++_myMessageNum)); // THIS IS THE NEW LINE
    ...

You won’t need to add any kind of message number to SendFinishMessage; that’s because this message is sent reliably and is guaranteed to be received in order. Also, you’re only sending one of these messages, which means sending it out of order is mathematically impossible. :]

Now that you’re sending this message number, you need to handle it on the receiving end.

if (messageType == 'U' && data.Length == _updateMessageLength) { 
        int messageNum = System.BitConverter.ToInt32(data, 2);
	float posX = System.BitConverter.ToSingle(data, 6);
	float posY = System.BitConverter.ToSingle(data, 10);
	float velX = System.BitConverter.ToSingle(data, 14);
	float velY = System.BitConverter.ToSingle(data, 18);
	float rotZ = System.BitConverter.ToSingle(data, 22);

    if (updateListener != null) {
        updateListener.UpdateReceived(senderId, messageNum, posX, posY, velX, velY, rotZ);
    }
} else if ...

This change means you’ll have to adjust your method’s signature in MPUpdateListener.

Open your MPUpdateListener interface in MPInterfaces.cs and change UpdateReceived as follows:

void UpdateReceived(string participantId, int messageNum, float posX, float posY, float velX, float velY, float rotZ);

Next, open your GameController class and change UpdateReceived as follows:

public void UpdateReceived(string senderId, int messageNum, float posX, float posY, float velX, float velY, float rotZ) {
    if (_multiplayerReady) {
        OpponentCarController opponent = _opponentScripts[senderId];
        if (opponent != null) {
            opponent.SetCarInformation(messageNum, posX, posY, velX, velY, rotZ);
        }
    }
}

You’re now passing messageNum to SetCarInformation() of OpponentCarController.

Open OpponentCarController.cs, and add the following variable:

private int _lastMessageNum;

This variable contains that last message number that it received. That way, you can determine if the next message is an older or newer one.

Now initialize that variable in Start():

_lastMessageNum = 0;

Now change as follows:

public void SetCarInformation(int messageNum, float posX, float posY, float velX, float velY, float rotZ) {
    if (messageNum <= _lastMessageNum) {
        // Discard any out of order messages
        return;
    }
    _lastMessageNum = messageNum;

    // The rest of the method is the same as before.
}

Whenever you receive a message number from an opponent, you compare it to the lastMessageNum you have stored for that player. If it's a smaller number than what you've received in the past, you know this message has been received out of order and you toss it.

That was a wide-ranging change; build and run your game to make sure everything is still working. But just for kicks, update your game on only one device and see what happens...

Your clients are no longer talking to each other! In fact, pretty soon your players will time out on each other's devices because no messages are getting through. Can you guess why?

[spoiler title="Why are our clients giving each other the silent treatment?"]
The code that breaks all of your communication is the following line in OnRealTimeMessageReceived:

if (messageType == 'U' && data.Length == _updateMessageLength) { 

Your two devices now have different message lengths, so they're both discarding each other's messages as invalid.
[/spoiler]

In a way, you got lucky in this scenario. Ignoring each other's messages is probably the best outcome. What would have happened if you didn't bother checking the message length, or if you removed a few bytes elsewhere in the message so that it ended up the same length as before?

You could have created a situation where one client thinks it's sending an integer representing the message number, but the other client is interpreting those 4 bytes as a float representing the car's x position.

It's not a theoretical problem — this could totally happen in real life. When you release a new version of your game, not all players will update right away. Your client has no way of knowing it's using an older message protocol...unless you had a way of sending a version number across with every message.

Wait — you do have a way to do that. Remember that _protocolVersion variable you set way back in the last lesson? The one Unity keeps complaining that we're not using? It's time to put it to work.

We could not secure the rights to Dr. Zoidberg, but that isn't going to stop you from reading this in his voice.

We could not secure the rights to Dr. Zoidberg, but that isn't going to stop you from reading this in his voice.

First, you should change your version number, since your message format has changed.

Change the value of _protocolVersion near the top of MultiplayerController as follows:

private byte _protocolVersion = 2;

Next, you will modify OnRealTimeMessageReceived so you check the message version before you do anything else.

Add the following code to the begining of OnRealTimeMessageReceived, right underneath the line byte messageVersion = (byte)data[0];

if (messageVersion < _protocolVersion) {
    // Our opponent seems to be out of date.
    Debug.Log("Our opponent is using an older client.");
    return;
} else if (messageVersion > _protocolVersion) {
    // Our opponents seem to be using a newer client version than our own! 
    // In a real game, we might want to prompt the user to update their game.
    Debug.Log("Our opponent has a newer client!");
    return;
}

Build your game and update it on the one device you updated earlier; launch a multiplayer game and you'll see that your two clients still won't talk to each other — but at least now you know why! :]

Your newer client recognizes that its protocol version is out of sync with the other clients and it's rejecting the message for that reason instead of having to rely on just the message length. You're only displaying this in your console log for now, but in a real game you could display this message to the player and give them a better understanding of why their two games aren't talking to each other.

This is a rather simple strategy for dealing with out of sync clients. If you wanted to be more flexible, you could add some degree of backwards compatibility into your clients. For instance, you could still accept messages based on protocol version 1; you simply wouldn't check for an auto-incrementing message number.

With backwards compatibility built into your game, your clients could declare at the start of a match what protocol version they're using and how backwards compatible they are.

Agreeing on Protocol, part 1

If there's a "lowest common protocol" version that all of your clients support, they could agree to use that one, and your different-versioned games could still play with one another!

Agreeing on Protocol, Part 2

Supporting multiple protocols can be a lot of work; sometimes the benefits of a breaking change outweigh the need for backward compatibility. It's up to you and your game's design to decide if this approach makes sense to you.

Another alternative is to set the game's auto-match variant equal to your protocol version, which would ensure that only players of the same client version could be auto-matched to play with each other. This would certainly minimize this problem — but it would also decrease the number of possible competitors in each pool. No one ever said this multiplayer thing was easy! :]

Load the newest version of your game onto both devices; everything should be working just about the same as before, although the game will now smartly discard any message received out of order — and you're also prepared for a world where people might have different client versions.

Todd Kerpelman

Contributors

Todd Kerpelman

Author

Over 300 content creators. Join our team.