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 4 of 5 of this article. Click here to view the first page.

Leaving the Game (Less Normally)

It's a fact of life that all players won't see a game through to completion. This can happen intentionally, where a player rage-quits, has to go to an appointment, or just gets fed up with an opponent who's not playing fair; or unintentionally, where your game crashes, the player loses their network connection, or their battery dies. Your job as a game designer is to handle these unexpected quits in a clean manner.

Take the case where a player consciously leaves the game. Do you really want to encourage players to rage-quit? No, but there are lots of valid reasons someone might leave a game; many events in the real world often require you to put your device away for a moment, such as saying your wedding vows. :] To that end, you'll add a "Leave Game" button in your UI.

Open GameController.cs and add the following code to the beginning of OnGUI(), just outside of the if block:

if (_multiplayerGame) {
    if (GUI.Button (new Rect (0.0f, 0.0f, Screen.width * 0.1f, Screen.height * 0.1f), "Quit")) {

        // Tell the multiplayer controller to leave the game
        MultiplayerController.Instance.LeaveGame();
    } 
}

This calls LeaveGame() in MultiplayerController, which in turn calls LeaveRoom() in the Google Play platform. Once your player has left the room, the platform reports this back to your game in the OnLeftRoom() callback, which will then call LeaveGameConfirmed() in your Game Controller, just as in the diagram shown earlier.

Note: This is actually a pretty terrible way for a player to quit the game. At the very least, you'd want to put up a confirmation dialog so that a player can't accidentally leave the game with a single tap on the screen.

But what about the players remaining in the game? They need to know that this player has left, so they're not sitting around waiting for him or her to finish. Well, if you've been carefully studying your console log (which is what I like to do for fun on a Saturday night), you likely saw a line like this when your opponent left the game:

Player p_CL3Ay7mbjLn16QEQAQ has left.

This means this event is being captured in your OnPeersDisconnected() listener; therefore you just need to pass that information back to the game.

Note: At the time of this writing, there is an "undocumented feature" (okay, fine, a bug) in the library where the room is considered destroyed if your opponent leaves the room and you're the only one left. You'll also receive an OnLeftRoom callback instead of an OnPeersDisconnected call. Hopefully that "feature" has been dealt with by the time this is published! :]

First, add the following line to your MPUpdateListener interface in MPInterfaces.cs file:

void PlayerLeftRoom(string participantId);

MultiplayerController will call this method when it receives a notice that somebody left the room.

Next, modify OnPeersDisconnected in MultiplayerController.cs, as follows:

public void OnPeersDisconnected (string[] participantIds)
{
    foreach (string participantID in participantIds) {
        ShowMPStatus("Player " + participantID + " has left.");
        if (updateListener != null) {
            updateListener.PlayerLeftRoom(participantID);
        }
    }
}

This loops through each player and calls your new interface method on your listener. Open GameController.cs and add that method now:

public void PlayerLeftRoom(string participantId) {
    if (_finishTimes[participantId] < 0) {
        _finishTimes[participantId] = 999999.0f;
        CheckForMPGameOver();
    }
}

When a player leaves a room, you record their finish time as 999999.0; this ensures CheckForMPGameOver counts this player as "finished" since the finish time is positive. Using such a large value means a player can't cheat their way to first place by quitting a game early.

The call to CheckForMPGameOver() is necessary since the disconnected player won't ever send a "game over" message; if they were the only player remaining in the game you'd want the game to end at this point.

Note: Depending on your game design, you might want to end the game early if there is only one player remaining. In Circuit Racer, it's still fun to drive around a track by yourself, so you let the local player finish off the race. On the other hand, a first person shooter would be much less fun if there were nobody left to shoot, so leaving the game early would be a better choice.

Note: Depending on your game design, you might want to end the game early if there is only one player remaining. In Circuit Racer, it's still fun to drive around a track by yourself, so you let the local player finish off the race. On the other hand, a first person shooter would be much less fun if there were nobody left to shoot, so leaving the game early would be a better choice.

Build and run your game; start a game and end one client early by tapping the Quit button. The other player will be able to finish their game and start a new one instead of waiting around forever.

It looks a little odd to see your disconnected opponent's dead car sitting in the middle of the track, but it's easy enough to add some code to remove it from the game.

Open OpponentController.cs and add the following method:

public void HideCar() {
    gameObject.renderer.enabled = false;
}

In PlayerLeftRoom, add the following code just before CheckForMPGameOver():

if (_opponentScripts[participantId] != null) {
    _opponentScripts[participantId].HideCar();
}

Build and run your game; start a two-player game and quit a game prematurely. The opponent's car will eventually vanish from the screen, so you know your opponent is truly gone and not just being difficult by sulking in the middle of the road. :]

So are we done now?

Close — but not yet. You've handled the situation where a player intentionally leaves the room and the Google Play game services library calls LeaveRoom on behalf of that player. Sometimes, though, the service doesn't have the chance to make a clean exit — and that's the next scenario you'll need to handle.

Leaving the Game (Abnormally)

If your player's battery dies, their game crashes, lose their cell service in the subway, or drop their phone in the toilet (it's been known to happen), your game won't have an opportunity to leave the room properly.

Try this yourself — no, don't go and drop your phone in the toilet. :] You can turn on airplane mode on one of your devices while in the middle of the game, which will kill your network connectivity, not give the Google Play games services library a chance to call LeaveRoom(), and leave the other player waiting forever for the other player to either send a "game over" message or leave the room — neither of which will happen.

The most common way to detect these scenarios is through timeouts. You're receiving approximately six updates per second for your opponent; if those calls stopped for long enough, you could probably assume something terrible has happened to the other player. Or that their game has crashed. One of the two.

Our time-out strategy

Your strategy for detecting timeouts will be pretty straightforward. Each CarOpponent will keep track of the last time they received a game update. You'll check at intervals to see if this update is longer than your timeout threshold. If it is, treat the opponent as you would if they left the game voluntarily.

Aha! OpponentCarController is already storing the last time it received an update from the network in _lastUpdateTime. You just need a way to access it.

Add the following line to OpponentCar, right beneath the spot where you declare _lastUpdateTime:

public float lastUpdateTime { get { return _lastUpdateTime; } }

That code might look a little odd to you, but it's not all that different from creating a readonly property in Objective-C. You simply create a lastUpdateTime property with only a getter (hence making it read-only), and define the getter to return the value of the private variable _lastUpdateTime.

This lets you retrieve the value of _lastUpdateTime from GameController, while only OpponentCar can set the value.

You need to make sure this threshold is realistic; you don't want players timing out right away just because _lastUpdateTime was initialized to 0!

Add the following code to Start():

_lastUpdateTime = Time.time;

This sets the time when an opponent is created.

Now you can add the logic to check for timeouts. Add the following variables to GameController:

public float timeOutThreshold = 5.0f;
private float _timeOutCheckInterval = 1.0f;
private float _nextTimeoutCheck = 0.0f;

The timeOutThreshold is the how many seconds before the player is considered gone. _timeOutCheckInterval is how often the system should check for a timeout. _nextTimeoutCheck holds the time plus the interval so it can be easily checked (versus calculating the total every iteration).

Next, add the following method to GameController:

void CheckForTimeOuts() {
    foreach (string participantId in _opponentScripts.Keys) {
        // We can skip anybody who's finished.
        if (_finishTimes[participantId] < 0) {
            if (_opponentScripts[participantId].lastUpdateTime < Time.time - timeOutThreshold) { 
                // Haven't heard from them in a while!
                Debug.Log("Haven't heard from " + participantId + " in " + timeOutThreshold + 
                          " seconds! They're outta here!");
                PlayerLeftRoom(participantId);
            }
        }
    }
}

First, you iterate through all opponent participantIDs by looking at the keys of the _opponentScripts dictionary. You then check the last time you heard from each player that hasn't finished; if you haven't heard from them in more than 5.0 seconds treat them as if they had left the game.

Note: A five-second value is great for testing, but in real life you'd want to use a value around 10 or 15 seconds, particularly if you're planning on releasing your game internationally to markets where flaky 2G networks are still common.

Finally, you need to call this method from within DoMultiplayerUpdate(). It's probably overkill to call it in every frame, so you'll call it once per second instead.

Add the following code to the beginning of DoMultiplayerUpdate().

if (Time.time > _nextTimeoutCheck) {
    CheckForTimeOuts();
    _nextTimeoutCheck = Time.time + _timeOutCheckInterval;
}

Build and run your game one last time — and ensure you've turned off airplane mode to save pulling of hair and much gnashing of teeth. :] Start a game, then enable airplane mode on one of your devices. After a five-second pause, you should see a note in the console log that this player is gone, and their car should disappear from the track.

Are we done now?!?

Well, you're done with all the ways a player can exit the game, so you're done for this part of the tutorial at least! :]

Todd Kerpelman

Contributors

Todd Kerpelman

Author

Over 300 content creators. Join our team.