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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Creating a Cross-Platform Multi-Player Game in Unity — Part 3
35 mins
- Reducing Network Traffic
- An Aside on Compression Strategies
- Decreasing Update Frequency
- Trick #1: Interpolation
- Trick #2: Extrapolation
- Finishing a Game
- Sending a Game Over Call
- Receiving the Game Over Call
- Leaving a Game (Normally)
- Leaving the Game (Less Normally)
- Leaving the Game (Abnormally)
- Where to Go from Here?
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.
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.
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. :]
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.
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.
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.
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! :]