Welcome back to the second part of this series on creating a cross-platform multi-player game in Unity. You’ll need to tackle the first part of this tutorial before you take on this one — otherwise this tutorial won’t make a whole lot of sense! :]
Up to this point, you’ve spent a lot of time adding frameworks and filling out forms to get to the very first step of any multiplayer game — signing in and out. Now you can move on to the real work of making the game happen.
In this tutorial part, you are going to learn the following:
- Some general game networking theory
- How to to connect Google Play Services
- How to create a game app using Google Development console.
- The code necessary to have devices talk with each other.
- Mad driving skills! :]
You can download the completed version for part one over here. You can read the first part over here.
An Introduction to Matchmaking
Matchmaking is one of the main functions of Google Play game services and other multiplayer frameworks. When your app tells Google Play’s servers that the player is interested in a multiplayer game, Google Play then looks for other people in the world who are also interested in playing that same game at that same time.
Matchmaking is a somewhat complex operation. the Google Play servers don’t just pick the next person they find; if one player is in Helsinki and the other player is in Sydney, the network connection between them would be terribly laggy. Unfortunately, even the best network programmers haven’t yet found a way to increase the speed of light. :]
So Google Play may choose to wait for better options to come up for our Helsinki player and only match them against the player in Sydney if no better option comes along. Fortunately, you don’t have to worry about any of that logic; you just need to know that the underlying service does a pretty good job at connecting people within a reasonable amount of time.
Many multiplayer games, such as MMOs, require that all clients connect to a server in a traditional client-server model like the one illustrated below:
However, many other games, like yours, will pair up players then connect their devices to each other through a peer-to-peer mesh network, where all players’ devices talk directly to each other as shown below:
Invites, or Auto-Match?
Traditionally, there are two ways that players are matched with opponents:
- Players can request to be matched up with specific friends.
- Players can ask to be auto-matched, which tells Google Play to find other people looking to play this game.
I find that vast majority of games use auto-matching; players tend to be impatient and just want to start a game instead of waiting around for their friend to see the invitation notification and decide whether or not to accept it.
Invitations typically happen when two players are in the same physical location and spontaneously decide to play a game, or they’ve scheduled a gameplay session ahead of time. In most other cases, players tend to stick with auto-matching.
For these reasons, you’ll implement auto-matching in your game.
Adding Auto Matching
Open your Circuit Racer project in Unity, then open up your MultiplayerController
script from the Scripts folder.
Create the following private instance variables in your class:
private uint minimumOpponents = 1; private uint maximumOpponents = 1; private uint gameVariation = 0; |
minimumOpponents
is the minimum number of opponents to match your player against. You’ll create a two-player game for now, so set this to1
.maximumOpponents
is the maximum number of opponents to match your player against. Since it’s a two-player game, set this to1
as well.gameVariation
specifies which particular multiplayer variation of your game that you wish to play. If Circuit Racer had a racing mode and a destruction derby mode, you wouldn’t want the racing players to end up auto-matched with people playing in the destruction derby!
The variables are all unsigned integers, meaning that they cannot hold a negative value. After all, you wouldn’t to play a game with negative players. Would that mean that you would cease to exist? :]
You could specify a variation of 1
for racing players, and a variation of 2
for destruction derby players to keep them separated. Using too many variants of your game segments the players and means there’s fewer players in each pool. In this tutorial, you only have the one variant, so use 0
as the default.
Next, add the following method to your MultiplayerController
class: (MonoDevelop will complain that your argument is invalid because you’ve declared that MultiplayerController
will be your RealTimeMultiplayerListener
, and you haven’t set it up that way — but that’s an easy fix.)
private void StartMatchMaking() { PlayGamesPlatform.Instance.RealTime.CreateQuickGame (minimumOpponents, maximumOpponents, gameVariation, this); } |
You will notice that you’ve passed in all your private instance values. The final argument is the RealTimeMultiplayerListener
instance that receives any messages from the Google Play games service about the status of your multiplayer game. To keep all multiplayer logic contained in the same class, you use this
to indicate that the MultiplayerController
class is your listener.
Now fix the compile error by changing the following line at the top of your class:
public class MultiplayerController { |
…to the following
public class MultiplayerController : RealTimeMultiplayerListener { |
Here you declare that MultiplayerController
conforms to the RealTimeMultiplayerListener
interface, which is a list of methods a class promises to implement; this is very similar to a protocol in Objective-C.
Look at the SignInAndStartMPGame()
; you’ll see there are two places in the code where the comments say you can start a multiplayer game like so:
// We could start our game now |
Replace those comments with the following method call:
StartMatchMaking(); |
Head back to Unity, and you’ll see…a bunch of new errors!
Unity is complaining that you aren’t conforming to the interface as you promised. The RealTimeMultiplayerListener
interface says any conforming class must implement the OnRoomConnected(bool success)
method, among others.
To get rid of the errors, you can create a few mostly stubbed-out methods for the time being.
You’ll replace MonoDevelop’s boilerplate exception handling code with something that just prints out simple debug messages for now.
First, add the following utility method in MultiplayerController that prints out status messages from your matchmaking service:
private void ShowMPStatus(string message) { Debug.Log(message); } |
Now you can tackle each interface method in turn.
Add the following method, or alternately replace the contents of your stub method if you auto-generated it in the previous step:
public void OnRoomSetupProgress (float percent) { ShowMPStatus ("We are " + percent + "% done with setup"); } |
OnRoomSetupProgress
indicates the progress of setting up your room. Admittedly, it’s pretty crude; on iOS in particular, I think it jumps from 20% to 100%. But hey, it’s better than nothing! :]
Some of you may be wondering what is a “room”? In Google Games terminology, a room is a virtual place that players gather to play real time games.
Add the following method, or replace the contents of your stub method:
public void OnRoomConnected (bool success) { if (success) { ShowMPStatus ("We are connected to the room! I would probably start our game now."); } else { ShowMPStatus ("Uh-oh. Encountered some error connecting to the room."); } } |
OnRoomConnected
executes with success
set to true
when you’ve successfully connected to the room. This would normally be the point where you’d switch to a multiplayer game.
Add or replace the following method:
public void OnLeftRoom () { ShowMPStatus ("We have left the room. We should probably perform some clean-up tasks."); } |
OnLeftRoom
tells you that your player has successfully exited a multiplayer room.
Add or replace the following method:
public void OnPeersConnected (string[] participantIds) { foreach (string participantID in participantIds) { ShowMPStatus ("Player " + participantID + " has joined."); } } |
You will receive an OnPeersConnected
message whenever one or more players joins the room to which your local player is currently connected. You’ll learn more about participantIds
later, but for now all you need to know is that they’re unique IDs for a specific player in this gameplay session.
Add or replace the following method:
public void OnPeersDisconnected (string[] participantIds) { foreach (string participantID in participantIds) { ShowMPStatus ("Player " + participantID + " has left."); } } |
OnPeersDisconnected
is similar to OnPeersConnected
but it signals that one or more players have left the room.
Now for the last interface method! Add or replace the following method:
public void OnRealTimeMessageReceived (bool isReliable, string senderId, byte[] data) { ShowMPStatus ("We have received some gameplay messages from participant ID:" + senderId); } |
You call OnRealTimeMessageReceived
whenever your game client receives gameplay data from any player in the room; this handles all of your multiplayer traffic.
Once you’ve added all of the above methods, head back to Unity and check that all your compiler errors have resolved. If so, hit Command-B to export and run your project in Xcode. When the game starts, click the Multiplayer button and check the console log, where you should see something similar to the following:
DEBUG: Entering internal callback for RealtimeManager#InternalRealTimeRoomCallback DEBUG: Entering state: ConnectingState We are 20% done with setup |
That means you’re in a two-player multiplayer lobby, waiting for another player to join. Success! :]
However, you’re the only person on earth who can play this game, so you might be waiting a looong time for someone else to join. Time to fix that.
Running on a Second Device
It’s quite difficult to test a Unity multiplayer game on a real device and the iOS simulator at the same time, so to make your life easy you’ll use two physical devices to test your multiplayer functions.
It’s safe to assume that since you’re reading this on RayWenderlich.com (where the iOS tutorials outnumber the Android ones by about 60 to 1), your second device also runs iOS. In that case, you can skip the section below. For those of you who want (or need) to run this on an Android device, read on!
Running on Android
If you’ve never run an Android app before, I recommend you read through the excellent “Make Your First Android App” series by Matt Luedke, as this section only presents a brief summary of the steps required.
Download and install Android Studio from http://developer.android.com/sdk/installing/index.html?pkg=studio. Click the Check for updates now text at the bottom of the welcome screen to ensure you have the latest version installed.
Next, get the latest version of the SDK: at the welcome dialog, click on Configure\SDK manager. Make sure you have the latest version of:
- Android SDK Tools
- Android SDK Platform-tools
- Android SDK Build-tools
- The Android API which corresponds to the device you have. Not sure what device you have? Go to Settings\About phone and look for the Android Version number.
- Android Support Library
- Google Play Services
If the status of any of these items is Not installed or Update available, simply check the box next to the item and click Install xx packages… as shown below:
Accept all the licenses as you’re prompted, and you’re good to go! Now that you’ve installed the requisite SDKs, you can quit Android Studio.
Make sure USB debugging is turned on for your device, as noted in Matt’s tutorial:
If you have a device, you don’t need any silly provisioning profiles. You just need to turn on USB debugging for your device. Sometimes the checkbox option is available just by going to Settings > Developer Options on your device. Check these instructions for more details.
Other times, you have to do some weird shenanigans. I can’t make this up! A direct quote from Android: “On Android 4.2 and newer, Developer options is hidden by default. To make it available, go to Settings > About phone and tap Build number seven times. Return to the previous screen to find Developer options.”
Head back to Unity and hook up your device to your computer. Select Unity\Preferences then select External Tools from the dialog that appears. Ensure the Android SDK location is pointing to the right place: on my machine, it’s in /Applications/AndroidStudio/sdk/
but your setup might differ.
Select Google Play Games\Android Setup…; in the dialog box that appears, you may already see the Application ID for your app. If it’s not there, re-enter it:
If your Application ID isn’t in the dialog box, and you don’t remember what is, here’s how to find it again:
- Go back to the Play Developer console at https://play.google.com/apps/publish/
- Click on Game Services (the little controller icon) on the left
- Click on your game
- Look at the name of your game at the top of the screen. Next to it should be an 11-or-12 digit number:
- Copy-and-paste this value and then continue with the tutorial!
Click Setup and after a moment or two you should see a confirmation dialog that everything has been set up correctly. Next, select File\Build Settings\Android\Switch Platform to make this the default platform.
Click Build and Run, and Unity will prompt you for a filename for your Android apk
file; choose whatever you’d like, but CircuitRacerAndroid is a good suggestion. Unity then compiles the app, transfers it to your device, and launches it!
If you followed the above steps, you should see Circuit Racer running on your Android device. Try out the single player version of the game!
Open up a terminal window and type:
adb logcat | grep Unity |
This reports back any debug output from your device. You don’t even need to restart your app to view the logs!
I/Unity ( 5914): [Play Games Plugin DLL] 11/21/14 11:04:20 -08:00 DEBUG: Starting Auth Transition. Op: SIGN_IN status: ERROR_NOT_AUTHORIZED I/Unity ( 5914): I/Unity ( 5914): (Filename: ./artifacts/AndroidManagedGenerated/UnityEngineDebug.cpp Line: 49) I/Unity ( 5914): I/Unity ( 5914): [Play Games Plugin DLL] 11/21/14 11:04:20 -08:00 DEBUG: Invoking user callback on game thread I/Unity ( 5914): |
Return to the main menu of the game and tap on the multiplayer option. You’ll likely be prompted to sign in, but the attempt will probably fail in a few moments:
Don’t panic — you simply haven’t set up your Client ID for Android as you did for iOS in Part 1 of this tutorial.
Go to the Play Developer console (http://play.google.com/apps/publish/) and select the Game Services icon. Select your app, then click on Linked Apps. Click Link another app then select Android:
Give this a name such as Circuit Racer Debug. Most developers create two Client IDs: one for debug releases, and one for production. Under Package Name, use the sample BundleID you’re using for the iOS version. Developers in the real world often like to add “.debug” to the end of this, but you’ll just keep things simple for the sake of this tutorial.
Turn on Real-time multiplayer, just as you did for the iOS version. Your screen should look like this:
Click Save, then Continue, and finally click Authorize your app now.
Next you’re prompted for your package name, which should be filled out for you, and a signing certificate fingerprint. Hmm, that’s new; how do you get that?
Execute the following command in Terminal:
keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore -list -v |
When prompted for your password, enter android, which admittedly is a terrible password, but good enough for the purposes of this tutorial. :]
You’ll see the following output:
Certificate fingerprints: MD5: (Lots of hex numbers) SHA1: (Even more hex numbers) SHA256: (Yet more hex numbers) Signature algorithm name: SHA1withRSA Version: 3 |
Copy the hex string next to the SHA1
entry and paste it into the Signing certificate fingerprint field in the dialog box as shown:
The above steps tell Google Play’s servers that “I am the owner of com.<mycompany>.CircuitRacer
, and here is a giant random number associated with me that basically guarantees it.” At this point, nobody else can claim to own a Client ID for com.<mycompany>.CircuitRacer
.
Finalyl, click Create Client and you’re done!
Build and run your game again; note that in the Android world, you can select Replace when saving your apk
, since there are no additional post-process steps as there are the Xcode world.
This time around, you should be able to sign in to your multiplayer game; make sure you’re using a different Google account than the one you’re signed into on your iOS device, and that has also been listed as a Tester account. You’ll soon join the multiplayer lobby for your game.
Running on iOS
Here’s how to get your game running on two iOS devices:
- Stop the app in XCode.
- Plug your second device in to your computer.
- Run the app again in Xcode on your second device. You don’t need to re-export from Unity; just hit Run in Xcode.
- Once your game is running, sign in to your second iOS device with a different Google account than the one you are using on your first device, and one that has been listed as a Tester account under the Game Services section of the Play Developer console.
- Once you’ve signed in, start a multiplayer game; the debug messages will tell you that you’ve joined the multiplayer lobby for your game.
- Go back to your first device, start up your Circuit Racer app and request to join a multiplayer game.
After a few moments, you’ll see that your two accounts are connected with some resultant console output like the following:
DEBUG: New participants connected: p_CKq_go_Vq4CMchAB,p_CJXhuOmO0se-BRAB DEBUG: Fully connected! Transitioning to active state. DEBUG: Entering state: ActiveState DEBUG: Entering internal callback for RealTimeEventListenerHelper#InternalOnRoomStatusChangedCallback We are connected to the room! I would probably start our game now. |
As far as the Google Play library is concerned, you’re in a real multiplayer game — you just can’t see your opponent’s car or actually interact with the game in any way. Boo. In fact, if you click on the Multiplayer button again, it won’t work because the service thinks you’re still in a game. You’ll need to kill both apps and restart them again to join another multiplayer game.
The important thing is that the Google Play games service has matched up the two accounts on your devices and, in theory, you could start sending messages between the devices.
Since there aren’t any magazines lying around the multiplayer waiting room of your game to read while you wait for another player to join, It might be nice to add a little UI to your game that shows the progress of connecting to the multiplayer game.
Adding a Simple Waiting Room UI
The Google Play Games library you’ve incorporated into your Xcode build has some built-in support for a creating multiplayer waiting room UI. For some reason though, the Unity plug-in doesn’t take advantage of it. I suspect that most real game developers would prefer to build their own waiting room interface with their own themed graphics, which is the approach you’ll follow in this tutorial.
Open MainMenuScript.cs
and add the following public variable.
public GUISkin guiSkin; |
This variable holds the image that will be the background of your dialog box. You will set this image using the Unity inspector.
Now add the following two instance variables:
private bool _showLobbyDialog; private string _lobbyMessage; |
These variables track whether you should display the lobby dialog box and what message it should contain.
Now, add the following setter method to update _lobbyMessage
:
public void SetLobbyStatusMessage(string message) { _lobbyMessage = message; } |
Next, add the method below:
public void HideLobby() { _lobbyMessage = ""; _showLobbyDialog = false; } |
The above method simply instructs the Main Menu to stop showing the lobby interface.
Next, open MultiplayerController.cs
and add the following public variable:
public MainMenuScript mainMenuScript; |
The mainMenuScript
variable holds a reference to your mainMenuScript. By doing so, you can call the public methods that you just added.
Replace ShowMPStatus()
with the following::
private void ShowMPStatus(string message) { Debug.Log(message); if (mainMenuScript != null) { mainMenuScript.SetLobbyStatusMessage(message); } } |
With the changes above, instead of just printing debug messages to the console, you’re telling your mainMenuScript
that it should be showing message
as a lobby status message.
Now you need to display these messages. Go back to MainMenuScript.cs
and replace the following lines in OnGUI()
:
} else if (i == 1) { RetainedUserPicksScript.Instance.multiplayerGame = true; MultiplayerController.Instance.SignInAndStartMPGame(); } |
…with the following code:
} else if (i == 1) { RetainedUserPicksScript.Instance.multiplayerGame = true; _lobbyMessage = "Starting a multi-player game..."; _showLobbyDialog = true; MultiplayerController.Instance.mainMenuScript = this; MultiplayerController.Instance.SignInAndStartMPGame(); } |
Here you set _showLobbyDialog
to true
, which indicates you’re ready to show a dialog box; you also tell MultiplayerController
that this is the class to which it should send lobby status messages.
Next, at the top of OnGUI()
wrap that entire for
loop inside another if
block as follows:
if (!_showLobbyDialog) { for (int i = 0; i < 2; i++) { // Lots of GUI code goes here,,, } } |
This hides the menu buttons [when the lobby dialog box appears. Even though the dialog box will cover the buttons, they’ll still remain clickable — which could lead to some unexpected behavior from your game! :]
Add the following code to the end of OnGUI()
:
if (_showLobbyDialog) { GUI.skin = guiSkin; GUI.Box(new Rect(Screen.width * 0.25f, Screen.height * 0.4f, Screen.width * 0.5f, Screen.height * 0.5f), _lobbyMessage); } |
This displays the dialog box on the screen.
Head back to Unity and go to the MainMenu scene. Select MainMenuGameObject
, click the little target next to Gui Skin
, and then select GameGuiSkin
from the Assets dialog:
This sets the skin of your game using the public guiSkin
variable you declared at the start of this section.
Build and run your game; this time you should see your online connection status as a nice series of dialog boxes:
That’s a little more comforting to a player who can’t view the Xcode console log! :]
However, you’re still stuck with this dialog box that never goes away. As nice as the dialog is, playing an actual game would be a lot more fun.
Adhering to the Delegate Pattern
Some of you may have noticed that the code above violates the principle of encapsulation. MultiplayerController
directly communicates with MainMenuScript
, which means the two classes are highly coupled. If you wrote some really awesome code in MultiplayerController
and wanted to reuse it in your new exciting boat racing game “Circuit Sailor”, you’d be in a hard spot.
In much the same way that you use the delegate pattern in Objective-C to logically separate one class from another, you can create an interface in C# to do the same thing.
Head back to Unity and open the Scripts folder in your assets panel. Right-click the panel and select Create\C# Script. Name the script MPInterfaces
, then open up the file for editing.
Replace the entire file (yep, the whole thing) with the following code:
public interface MPLobbyListener { void SetLobbyStatusMessage(string message); void HideLobby(); } |
Here you declare an interface, which as you’ll recall is sort of like a contract. Any class that implements this interface promises that it will add any requisite methods.
Go back to MultiplayerController
and modify the following variable:
public MainMenuScript mainMenuScript; |
…to the following:
public MPLobbyListener lobbyListener; |
You’re replacing mainMenuScript
, which was tied directly to a MainMenuScript
class, with an instance of lobbyListener
instead. You’re saying you don’t care what kind of class this is, so long as it implements the MPLobbyListener
interface.
Modify ShowMPStatus
so that mainMenuScript
variable is replaced by a lobbyListener
variable:
private void ShowMPStatus(string message) { Debug.Log(message); if (lobbyListener != null) { lobbyListener.SetLobbyStatusMessage(message); } } |
The following code decouples the current object from the MainMenuScript
since the code now works upon an interface as opposed to an instance.
Now open MainMenuScript.cs
and modify the class declaration at the beginning of the file as follows:
public class MainMenuScript : MonoBehaviour, MPLobbyListener { |
This change indicates that the class implements the MPLobbyListener
interface.
Now find the following line in OnGUI()
:
MultiplayerController.Instance.mainMenuScript = this; |
..and modify it as follows:
MultiplayerController.Instance.lobbyListener = this; |
This last bit of code removes the last reference of the MainMenuScript and uses the new interface instead.
Since MainMenuScript
already implements the required methods, it’s fulfilled its contract with the MPLobbyListener
interface.
If you were to build and run your app at this point, it would still look and run the same, But you could feel confident developing it further knowing that you have some nicely encapsulated code under the hood! :]
Starting the Multiplayer Game
Now that you’ve cleaned up your code, you can add some multiplayer game logic to your project.
Add the following code to the if (success)
block of OnRoomConnected
in MultiplayerController.cs
:
lobbyListener.HideLobby(); lobbyListener = null; Application.LoadLevel("MainGame"); |
This hides the lobby dialog, clears out your mainMenuScript
and then loads up your MainGame
scene. If you were to run your app at this point, you’d see that you’re taken into the game as soon as you’re connected.
Having only one car on the screen somewhat limits the multiplayer experience. Your next task is to add the opponent’s car.
Browse out your assets folder; you’ll see that there’s an existing Prefab for an OpponentCar
object. Click on it, then click Add Component and add a new Script Component. Name it OpponentCarController, then click Create and Add.
Finally, double-click on the newly created script to edit it. OpponentCarController
will represent the other players in the game; it doesn’t need to be very smart, since your human opponents will do all the driving.
At the top of your class definition, add the following variable:
public Sprite[] carSprites; |
This variable is going to hold the sprites of various car images.
Next, add the following method into the script:
public void SetCarNumber (int carNum) { GetComponent<SpriteRenderer>().sprite = carSprites[carNum-1]; } |
This lets you store your collection of car sprites in an array (yellow, blue, and red); you can then set the car sprite for a specific player simply by calling SetCarNumber
on it.
Go back to Unity, then click on the OpponentCar
Prefab. Find the Car Sprites entry in the script component, and change its size to 3
. Then select car_1
for element 0
, car_2
for element 1
, and car_3
for element 2
as shown below:
Assigning Player Colors
At this point, you’re left with an interesting problem: how do you determine the color to assign to each player? If it was simply the player’s choice, all players could choose the yellow car. Or one player could think her car is red and her opponent’s car is blue, while her opponent thinks the exact opposite. How can you get all game clients to agree on the state of the world?
At first, you might think “Who cares?” But it quickly becomes evident that this sort of thing does matter in many cases. In a poker game, all clients need to decide who should deal first. In a real-time strategy game, you need to decide who should start at which base.
In your racing game, you need to decide which lane each client will start in; therefore this problem applies to you. Other game tutorials on this site usually solve this problem by generating random numbers; the highest number goes first, deals first, gets the yellow car and so on.
Instead of this approach, you can take advantage of the participant ID entity in Google Play Games.
Each player is assigned a participant ID by the game service when they join a game. This participant ID is different than a traditional player ID as it is randomly generated and only exists for the duration of that gameplay session. It’s a nice way for strangers to refer to each other in a game without having to use any data that could be traced back to the actual player.
You can easily sort out the game configuration by sorting the list of participant IDs and assigning qualities to players based on their order in this list; the yellow car goes to the first listed participant, the blue car to the second, etc. In a poker game, the player listed first could be the dealer, and so on.
Note: Google Play engineers have asked me to point out that these participant IDs are only mostly random; it’s possible for one player to get a lower-ordered participant ID more often than other players.
If your game design confers a major gameplay advantage to the player listed first, you might want to sort the list of participant IDs, then randomly choose the player who gets to be dealer, gets the sniper rifle, or gets the inside lane. In your case, the yellow car isn’t all that special, so you don’t need to do anything beyond sorting the list.
Open MultiplayerController.cs
and add the following line to the top of the file:
using System.Collections.Generic; |
This ensures that List
is defined.
Next, add the following method:
public List<Participant> GetAllPlayers() { return PlayGamesPlatform.Instance.RealTime.GetConnectedParticipants (); } |
This simply gets a list of all participants in the room. It’s worth noting here that the library already sorts this list for you by participantId
, so you don’t need to sort it yourself.
Add the following method to get the ParticipantID
of the local player:
public string GetMyParticipantId() { return PlayGamesPlatform.Instance.RealTime.GetSelf().ParticipantId; } |
Open GameController.cs
and add the following imports:
using System.Collections.Generic; using GooglePlayGames.BasicApi.Multiplayer; |
Now add the following variables near the top of the class definition:
public GameObject opponentPrefab; private bool _multiplayerReady; private string _myParticipantId; private Vector2 _startingPoint = new Vector2(0.09675431f, -1.752321f); private float _startingPointYOffset = 0.2f; private Dictionary<string, OpponentCarController> _opponentScripts; |
You’ll be using all these variables in the next method. Add the following code to the empty implementation of SetupMultiplayerGame()
:
// 1 _myParticipantId = MultiplayerController.Instance.GetMyParticipantId(); // 2 List<Participant> allPlayers = MultiplayerController.Instance.GetAllPlayers(); _opponentScripts = new Dictionary<string, OpponentCarController>(allPlayers.Count - 1); for (int i =0; i < allPlayers.Count; i++) { string nextParticipantId = allPlayers[i].ParticipantId; Debug.Log("Setting up car for " + nextParticipantId); // 3 Vector3 carStartPoint = new Vector3(_startingPoint.x, _startingPoint.y + (i * _startingPointYOffset), 0); if (nextParticipantId == _myParticipantId) { // 4 myCar.GetComponent<CarController> ().SetCarChoice(i + 1, true); myCar.transform.position = carStartPoint; } else { // 5 GameObject opponentCar = (Instantiate(opponentPrefab, carStartPoint, Quaternion.identity) as GameObject); OpponentCarController opponentScript = opponentCar.GetComponent<OpponentCarController>(); opponentScript.SetCarNumber(i+1); // 6 _opponentScripts[nextParticipantId] = opponentScript; } } // 7 _lapsRemaining = 3; _timePlayed = 0; guiObject.SetLaps(_lapsRemaining); guiObject.SetTime(_timePlayed); _multiplayerReady = true; |
Taking each numbered comment in turn:
- This gets the local player’s participant ID and stores it as a local variable.
- This grabs the list of sorted participants from the multiplayer controller so you can iterate through them.
- This calculates the start point for each car based on its order in the list.
- If the
participantID
of the current player matches the local player, then instruct theCarController
script to set the car number, which determines its color. - Otherwise, instantiate a new opponent car from the Prefab object and set its car number and color as well.
- You’re storing this carController in a dictionary with the participant ID as the key. Since you’ll be getting a lot of messages in the form “Participant ID xxx said so-and-so”, storing your opponents in a dictionary is an easy way to refer to them later by their participant ID.
- Finally, you initialize a few other game constants and set
_multiplayerReady
totrue
, signalling that you’re ready to receive multiplayer message. Since there’s no guarantee all clients will start at precisely the same time, weird things may happen if you begin to receive game messages while you’re still setting up. The_multiplayerReady
flag will protect against that, as you’ll see later.
You might be thinking “Hey, isn’t it overkill to create a dictionary to store all of my OpponentCarControllers
, when there’s really only one? Why not just have a simple opponentCar
variable and be done with it?” Although you’re only testing the game with two players at the moment, you’ll want the model to be flexible enough to handle more than two players in the future, and the dictionary is a good way to prepare for that scenario.
Go back to Unity and open the MainGame
scene. Select GameManager
and set the OpponentPrefab
variable in the inspector to be your OpponentCar
prefab object, then save the scene as shown below:
Build and run your game again; start a multiplayer game and you’ll see that one player has been assigned the yellow car and the other has been assigned the blue — and each player can drive themselves around the track.
Drive each of the cars around the track, and you’ll notice some strange gameplay: your car’s progress isn’t reported on your opponent’s device and vice versa. Time to get these devices talking to each other!
Making the Cars Move
You’ll move the opponent’s cars by reporting the location of each car to all players in the room at frequent intervals. When your game receives an update from another client, you can move the appropriate OppponentCar
to its current location.
Here’s the big question: how often are these “frequent intervals”? A typical PC game sends updates about 10-30 times per second. That’s a lot! However, mobile device games need to worry about battery life and data usage limits, so you will definitely want to send updates less often than that.
You’ll start by sending a network call every Update()
— which is definitely too often, but works as a good starting point — and you can work on optimizing your network calls in Part 3 of this tutorial series.
Sending Message Data
Add the following code to the end of DoMultiplayerUpdate()
in GameController.cs:
MultiplayerController.Instance.SendMyUpdate(myCar.transform.position.x, myCar.transform.position.y, myCar.rigidbody2D.velocity, myCar.transform.rotation.eulerAngles.z); |
Here you send all information the other players need to display the local player’s car appropriately: their x and y coordinates, z-axis rotaion, and the car’s current velocity.
At this point, Unity is probably complaining because you haven’t defined SendMyUpdate
. You’ll do that in just a moment, once you add a bit of supporting code.
Add the following private variables near the top of your MultiplayerController
class definition:
private byte _protocolVersion = 1; // Byte + Byte + 2 floats for position + 2 floats for velcocity + 1 float for rotZ private int _updateMessageLength = 22; private List<byte> _updateMessage; |
You’ll see how you use each of these variables shortly.
Add the following code to the beginning of private MultiplayerController()
to create your _updateMessage
list:
_updateMessage = new List<byte>(_updateMessageLength); |
Then add SendMyUpdate
to your MultiplayerController
class:
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 (posX)); _updateMessage.AddRange (System.BitConverter.GetBytes (posY)); _updateMessage.AddRange (System.BitConverter.GetBytes (velocity.x)); _updateMessage.AddRange (System.BitConverter.GetBytes (velocity.y)); _updateMessage.AddRange (System.BitConverter.GetBytes (rotZ)); byte[] messageToSend = _updateMessage.ToArray(); Debug.Log ("Sending my update message " + messageToSend + " to all players in the room"); PlayGamesPlatform.Instance.RealTime.SendMessageToAll (false, messageToSend); } |
First, you put your message in a byteArray
. The easiest, if not terribly efficient, way to do this is to build the message using a List
of bytes. You’ll re-use the _updateMessage
variable you created in the previous step, which is set at 22 bytes — just enough for everything you need to send. In Unity, floats are represented as 4 bytes.
The very first byte you add is a single byte to represent the protocol version. You can think of this as the version number of the message itself. You’ll see why this is important later in this tutorial.
The next byte is the instruction to send. It’s possible for a client to send all sorts of messages to the other players, so it’s helpful to first include a character as one of your first bytes to describe what’s going to be in the rest of the message. In your case, you’re using U to declare that you are sending an Update.
After that, you add the passed-in values of the position, velocity, and rotation of the car. AddRange
is one way to add a number of bytes to a list, and the System.BitConverter.GetBytes()
method reinterprets each of these float values as a series of bytes.
Next, you convert this list into a byteArray
using your list’s toArray()
method. Finally, you send this message to all the other players using the Google Play library’s SendMessageToAll
method, which takes two arguments:
- Whether or not this message should be sent reliably
- The actual message to send, as a byteArray
The second argument is pretty self-explanatory, but what does it mean to send a message “reliably”?
Reliable or Unreliable?
Typically there are two ways a client can send messages to other clients in a multi-player game.
The unreliable way is via the UDP network protocol. When you send messages over UDP, a small percentage of them don’t always make it to the target device. And if they are received, there’s no guarantee they’ll be received in the order they were sent — especially in your game, since you’re sending updates so frequently.
The reliable method uses the TCP network protocol. It guarantees that a message will always be received — eventually — by the target device and, even better, all messages will be received in the order they were sent.
So…why wouldn’t you choose the reliable method, you ask? The answer is that all this convenience with reliable network messaging comes at a considerable cost in speed. Reliable messages are significantly slower than unreliable ones, and the amount of slowdown can vary a lot.
Think about it this way: since reliable messages are guaranteed to be received in order, what happens if one message doesn’t make it to its target? That’s right, all of your other messages will be delayed and not received by the other player until the first one is re-sent and received, which can take a considerable amount of time:
The general rule for most game developers is to use reliable messages only when speed isn’t important, such as in the theoretical poker game discussed earlier. In fact, many developers of action games try to not use reliable messages at all, and instead implement some lightweight logic as needed on top of the UDP protocol to add little bits of reliability as needed.
Build and run your game now; join a multiplayer game and you’ll see from your debug logs that you’re sending dozens of messages per second:
Sending my update message System.Byte[] to all players in the room Sending my update message System.Byte[] to all players in the room Sending my update message System.Byte[] to all players in the room Sending my update message System.Byte[] to all players in the room |
There’s no need to clutter up the debug log with these messages; remove the call to Debug.Log
message from SendMyUpdate()
to streamline the debuglog.
You know that your client is sending out update messages, but now you need to do something when your game receives those messages from an opponent, like, say, move their car around the track! :] You’ll tackle that next.
Receiving Message Data
In MultiplayerController.cs, replace the code of OnRealTimeMessageReceived
with the following:
// We'll be doing more with this later... byte messageVersion = (byte)data[0]; // Let's figure out what type of message this is. char messageType = (char)data[1]; if (messageType == 'U' && data.Length == _updateMessageLength) { float posX = System.BitConverter.ToSingle(data, 2); float posY = System.BitConverter.ToSingle(data, 6); float velX = System.BitConverter.ToSingle(data, 10); float velY = System.BitConverter.ToSingle(data, 14); float rotZ = System.BitConverter.ToSingle(data, 18); Debug.Log ("Player " + senderId + " is at (" + posX + ", " + posY + ") traveling (" + velX + ", " + velY + ") rotation " + rotZ); // We'd better tell our GameController about this. } |
You’re basically doing the opposite of what SendMyUpdate
does; instead of putting data into a ByteArray
, you’re extracting it into useful values that you can use in your game. System.BitConverter.ToSingle()
takes a ByteArray
and an offset, and converts those bytes into a native data type.
Now that you have the information you need, you can do something useful with it. Add the following interface definition to MPInterfaces.cs:
public interface MPUpdateListener { void UpdateReceived(string participantId, float posX, float posY, float velX, float velY, float rotZ); } |
Next, add the following public variable to the top of your MultiplayerController
class:
public MPUpdateListener updateListener; |
Then, add the following to the end of the if
block in OnRealTimeMessageReceived
, right after the We'd better tell our GameController about this
comment:
if (updateListener != null) { updateListener.UpdateReceived(senderId, posX, posY, velX, velY, rotZ); } |
Go back to GameController.cs
and declare that it satisfies the MPUpdateListener
interface as follows:
public class GameController : MonoBehaviour, MPUpdateListener { |
Then add the following method somewhere inside that same class:
public void UpdateReceived(string senderId, float posX, float posY, float velX, float velY, float rotZ) { if (_multiplayerReady) { OpponentCarController opponent = _opponentScripts[senderId]; if (opponent != null) { opponent.SetCarInformation (posX, posY, velX, velY, rotZ); } } } |
Here you’re checking that _multiplayerReady
is true
, as there’s always a chance you could receive a message before you’re done your setup; if you don’t protect against this you could end up with some fun and exciting null pointer exceptions.
If everything checks out, you then call SetCarInformation()
on the OpponentCarController
corresponding to the participantID
of the sender.
Now open OpponentCarController.cs
and add the following method:
public void SetCarInformation(float posX, float posY, float velX, float velY, float rotZ) { transform.position = new Vector3 (posX, posY, 0); transform.rotation = Quaternion.Euler (0, 0, rotZ); // We're going to do nothing with velocity.... for now } |
There’s nothing too surprising in SetCarInformation
; you simply set this car’s position and rotation to the values reported by its game client.
Finally, go back to GameController
and add the following line to the top of SetupMultiplayerGame()
:
MultiplayerController.Instance.updateListener = this; |
In the code above, GameController
tells MultiplayerController
that “I am the class you should talk to when you get an update from another player in a multiplayer game.” So MultiplayerController
will call UpdateReceived()
in your GameController
, which will then call SetCarInformation()
on the appropriate OpponentCarController
object.
Build and run your app on both devices; you should now be able to move your car on one screen and see its movement reflected on the other screen. Move both cars on both devices at the same time, and hey — you’ve got yourself a racing game! :]
If everything hasn’t gone well, I’d suggest you try the following two things:
- Make sure you’re running the latest version of the game on both devices. Sometimes I’ll update my game on only one device and forget that I need to load it on the other as well.
- Kill the app on both devices and try again. Sadly, the “try it a second time and maybe it will magically work this time” solution seems to work for multiplayer games more times than I’d care to admit.
Where to Go From Here?
You can download the completed project over here.
Your multiplayer racing game is off to a really good start, but there still are a few problems that you’ll need to address:
- A biggie: the game doesn’t end! You’ll need a way to declare a winner and leave the room.
- You’re making network calls more frequently than you need to. It’d be nice if your game was a little more considerate of your user’s battery and data plan.
- Depending on your network connection, the cars’ movements might look a little jittery. You need a way to smooth them out and make the animation look fluid.
Luckily, these are all relatively easy issues to fix — and you’ll address them all in Part 3 of this tutorial series. As always, if you have any questions or comments, please feel free to join in the discussion below!
Creating a Cross-Platform Multi-Player Game in Unity — Part 2 is a post from: Ray Wenderlich
The post Creating a Cross-Platform Multi-Player Game in Unity — Part 2 appeared first on Ray Wenderlich.