Creating a Cross-Platform Multi-Player Game in Unity — Part 2
In the second part of this tutorial, you’ll write the code to have users connect with each other and then get a race started! 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 2
55 mins
- An Introduction to Matchmaking
- Invites, or Auto-Match?
- Adding Auto Matching
- Running on a Second Device
- Running on Android
- Running on iOS
- Adding a Simple Waiting Room UI
- Adhering to the Delegate Pattern
- Starting the Multiplayer Game
- Assigning Player Colors
- Making the Cars Move
- Sending Message Data
- Reliable or Unreliable?
- Receiving Message Data
- Where to Go From Here?
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.
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!