CardConquest GameDev Blog #14: Hosting a Lobby for Multiplayer using Mirror

Disclaimer: I am neither a professional game developer nor am I a professional programmer. I doubt I am following best practices. This is just what worked for me at the time. Hopefully it works for you if you’re playing along while reading!

Version of Unity used: 2019.4.13f1 Personal

The previous posts in this series can be found under the CardConquest category on this blog. The github for the project can be found here. The zip archive for this specific post can be found here.

In my last post, I mentioned that I wanted to start adding multiplayer to CardConquest. After searching around for how to add multiplayer to Unity games, I decided to use Mirror. There are a lot of different frameworks to do multiplayer in Unity, but I decided to go with Mirror because it is free, has a lot of documentation and free learning resources available, and it is based on the older uNet Unity feature so I thought maybe even some older/out-of-date resources could still be helpful for it while learning.

After settling on Mirror, I decided to create a rudimentary “Lobby” system that would allow players to create and host a game, and then for another player to join that lobby based on IP. I wanted to make it so that the player hosts/joins a lobby all through my game’s UI. Mirror does provide you with a “Networking GUI” overlay that can allow you to quickly host and join games, but that is meant more for quick testing than as something to use in a finished game.

This post will only be about creating the lobby system for the game. I still have not made any of my game functionality work over multiplayer (yet). So, onto making a lobby system using Mirror!

Where I Learned/Stole All This From

But before I go on and say how I did this, I really need to credit where I learned how to do this all, and where I ended up stealing like 90% of my code from. I watched a Mirror tutorial series from Dapper Dino on Mirror Multiplayer. The whole playlist can be found here. Most of this post will be from three of Dapper Dino’s “lobby” videos: Main Menu, Readying Up, and Start Game. I would recommend starting at the beginning of the tutorial series with Client-Server – Mirror Networking and working your way through up to the lobby tutorials, as that’s what I did. Also, remember to like and subscribe to Dapper Dino and their videos.

Dapper Dino also has a website for commission submissions and learning resources, and a Patreon page. I decided to subscribe to the $5 subscription on Patreon for while I continue to use their resources to make my game multiplayer. Thanks Dapper Dino!

Getting Started with Mirror

With the credit given were credit was due, time to get started with the tutorial. The first thing you’ll want to do is to actually add Mirror to your Unity project. When you start up Unity, you should see an “Asset Store” tab at the top of your scene view.

If for some reason you don’t see the asset store tab, you should be able to find it by going to Window -> Asset Store.

After you are in the Asset Store, search for “Mirror.” Scroll down until you see Mirror in the store and click on it.

Unity will probably then make you “purchase” Mirror to your Unity account. Mirror is free, so it shouldn’t cost you anything. You may need to “Update” or “Download” Mirror as well after purchasing it. After that’s all taken care of, click on “Import” to import the Mirror package into your project.

This will bring up the “Import Unity Package” window. You can simply click on “All” and then “Import” to import everything in the Mirror package. Not everything will be necessary for your project, though. There is an “Examples” folder that is a bunch of scenes that showcase different functionality of Mirror. You can uncheck this box if you want. It doesn’t matter too much, though, as the whole package is only takes up ~7mb of space in your project even with the examples. For simplicity’s sake, just click on “Import” to add Mirror to the project.

The import process may take a few minutes, but once it’s done, you should see the “Mirror” directory under your project’s Assets.

You can now begin to use Mirror in the project. Yay!

The Necessary UI Changes

The goal of this post is for the player to go through the title screen UI to then either create a lobby or join a lobby. This will require the following components in the title screen UI:

  • “Start Game” button to begin the process of join a lobby
  • Have the player set their username
  • Player decides to either “Host” a lobby/game or “Join” a game
    • If the player is joining a game, they will then enter an IP address or host name to join on

This will all be done in the “TitleScreen” scene, so make sure you double click on that scene to load it in the Unity editor.

If you remember all the way back to post #8, TitleScreen has a very simple UI that allows a player to either start the game and load the Gameplay scene, or to exit the game. The lobby creation/joining will make this UI slightly more complicated/involved.

Adjusting the “Main Menu” UI

The current UI in TitleScreen has an initial UI that allows the player to “start” the game. That will be left as is for. Later, the function the “StartGame” button is tied to will be changed.

There will be one minor change made to the initial UI that is mostly just to make organization easier. Right now in the hierarchy, the UI looks like this:

Later, elements of the UI will be activated and deactivated through scripting. To make that easier, different UI elements will be made children of “panels” so you can easily activate and deactivate the elements by making a reference to the single panel object instead of each object individually.

Right click on the “Canvas” object in the hierarchy, go to UI, then select Panel

Rename the panel to “MainMenuPanel.” Then, select the panel in the hierarchy, and in the Inspector, remove the “Image” component. This will get rid of that grey overlay that appears when you add a panel.

You could probably use an empty gameobject here instead of a panel, but I preferred to use a panel as panels can give you some more options in terms of how child UI elements are handled.

With MainMenuPanel created, select the “StartGameButton” and “ExitGameButton” objects and drag them to MainMenuPanel. This will make those two buttons children of MainMenuPanel.

The “TitleText” object is left outside of the MainMenuPanel so that the game’s title is displayed throughout all of TitleScreen’s various UIs.

Before moving on to creating the next UI element, you can deactivate MainMenuPanel by unchecking it in the inspector.

Setting the Player’s Name

The next UI to create will be to allow the player to set their name. The name will be displayed later in the lobby, and can be used when referencing the player.

To begin, create a new panel that will be the parent of the rest of the UI. Rename the panel to “PlayerNamePanel” and remove its image component.

The PlayerNamePanel will have two UI elements: A text input field for the player to enter their name, and a button to confirm the name they entered.

To create the text input field, right click on “PlayerNamePanel”, go to UI, then “Input Field – TextMeshPro.”

You could probably use a regular input field for this instead of the TextMeshPro (TMP) one, but I chose the TMP input field for whatever reason and I’m sticking to it. Rename the input field to “InputPlayerName.”

Select InputPlayerName in the hierarchy and then view it in the Inspector. Set all the position values to “0”, and then set Width to 300 and Height to 75.

Under the “Image” section, uncheck the “Fill Center” check box.

Back in the hierarchy, expand InputPlayerName. This should reveal “Text Area.” Expand the text area to see “Placeholder” and “Text.” Select “Placeholder.”

Placeholder is what will be displayed before the player has entered and saved a name. Under the TextMEshPro – Text (UI) section in the Inspector, set the following:

  • Text Input: Enter Name…
  • Font Size: 50
  • Vertex Color: White

The next thing to do would be to set the font to ThaleahFat to match the rest of the UI. Unfortunately, TMP requires a “Font Asset” be used to se the font for Placeholder, and currently ThaleahFat is not set as a font asset. Time to make a font asset then!

Making ThaleahFat a Font Asset.

I read this from unity.learn to figure out how to make a TMP font asset. To begin, go to Window, then “TextMeshPro,” then “Font Asset Creator.”

This will open the “Font Asset Creator” window. The first option you should see is the “Source Font File” option. This will be where you specify what you will be making the font asset from. Since I want to add ThaleahFat as a font asset, it will be from the Thaleah_PixelFont directory in my project. Under your project, expand “Thaleah_PixelFont” and go into the “Materials” directory. Then, select “ThaleahFat_TTF” and drag and drop it onto “Source Font File” in the Font Asset Creator window.

The rest of the settings in Font Asset Creator can be left as is. I honestly don’t know what they do so I didn’t bother messing with them. Now you can click on “Generate Font Atlas.” Once that completes, click on “Save as…”

When I saved the font asset, I created a new directory under “Assets” called “Font Assets.” That then saved the “ThaleahFat_TTF SDF” asset file.

Back to the Player Name UI…

Go back to the Placeholder object. you should now see the ThaleahFat font asset as an option when setting the font asset.

Back in the hierarchy, select the “Text” object below InputPlayerName’s Text Area. You will make similar changes to the Text that were made to Placeholder. The “Text” object will be what is displayed after the player starts typing into the input field. Set the following:

  • The “Text” should be nothing/blank
  • Font asset: ThaleahFat
  • Check “Auto Size” under font size. This will automatically adjust the size of the text based on how long of a name the user enters.
  • Vertex Color: R:51,G:255,B:0

That should be everything you need for InputPlayerName. The next step will be to add a confirmation button. Right click on PlayerNamePanel, go to UI, then select Button.

Rename the button to ConfirmPlayerName. Select ConfirmPlayerName in the hierarchy and make the following changes:

  • Under Rect Transform:
    • Pos X: 0
    • Pos Y: -100
    • Width: 300
    • Height: 75
  • Under Image
    • Uncheck “Fill Center”

Expand ConfirmPlayerName in the hierarchy and select its Text object. Then, set the following:

  • Text: Confirm Name
  • Font: ThaleahFat
  • Font Size: 50
  • Color: R:51,G:255,B:0

With all that set, the UI should look like this in the scene:

Uncheck the PlayerNamePanel in the Inspector to deactivate it and prepare for the next UI to make!

Host or Join a Lobby UI

The next UI to create will be the UI the player uses to either host a lobby, or continue on to join a lobby. First, create a new panel in the Canvas. Rename it to HostOrJoinPanel and remove the image from the panel.

This UI will be very simple. It will have two buttons: “Host Game” and “Join Game.” To create these buttons quickly, you can select the StartGameButton and ExitGameButton from the MainMenuPanel and then copy and paste them. Select the new, pasted buttons, and then drag them under HostOrJoinPanel to make them children of that panel. It should look like this after you’re done.

Rename the buttons to HostGameButton and JoinGameButton. Then, go into each button and change their Text so they are “Host Game” and “Join Game,” respectively.

The UI should look like this in the scene:

One thing to double on each button is anything left in the “On Click ()” section from when you copied it. Functions will be added to this buttons later, but right now make sure to remove the “StartGame()” and “ExitGame()” functions from the buttons if they are still there when you copied the buttons. The functions can be removed by clicking on the “-” sign under “On Click ().”

Deactivate the PlayerNamePanel in the Inspector and move onto making the next UI.

Join Game UI

The Join Game UI will have the player enter an IP address or hostname to join. A text input field and a button will be needed for this. Begin by creating a new UI panel, rename it EnterIPAddressPanel, then remove its Image.

EnterIPAddressPanel will have the same UI elements as PlayerNamePanel did, a text input field and a button. To create these quickly, copy and paste InputPlayerName and ConfirmPlayerName from PlayerNamePanel, then drag and drop them under EnterIPAddressPanel. The pasted objects should look like this:

Rename the input field to “IPAddress” and the button to “JoinOnIP.”

Select IPAddress in the hierarchy and expand out its child objects. Select Placeholder and remove the text. There won’t need to be any placeholder text for this UI. Back in the hierarchy, select the IPAddress object, and then set its Text to “localhost.” For the purposes of this post, a lobby will be created locally so a local player can join from the same machine. “Localhost” is a hostname that will allow a local player to join. So, for now the IPAddress text can just be set to localhost so you don’t have to enter it in every time when you are testing.

In the JoinOnIP button, the text can be set to “Connect to ip.” Once you have everything set, the UI should look like this:

With the EnterIPAddressPanel’s UI completed, you can deactivate it in the Inspector.

One Last Thing – Return to Main Menu Button

There’s one last UI element to add, and that’s a button to return to the main menu. This button will persist through multiple different UI’s so it can be created outside of any of the other panels that have already been created.

To create the new button, I copied and pasted the JoinOnIP button, then dragged and dropped the pasted button onto the Canvas at the top of the hierarchy. This made the new button a child of none of the panels, but only the Canvas.

Rename the new button to ReturnToMainMenu. Select ReturnToMainMenu in the hierarchy and go to the Inspector. This button will sit at the bottom of the UI, below the usual two other elements that are on the screen in any given UI. To position ReturnToMainMenu, set the Pos Y to -200.

Then, select ReturnToMainMenu’s text object and set the text to “Main Menu.”

If you activate any of the other panels, such as the HostOrJoinPanel, the UI with the ReturnToMainMenu button will look like this:

Everything for the UI has been created! You can now deactivate everything except for the Canvas and the TitleText. Time to get going with the scripting!

TitleScreenManager.cs Script

Currently, the TitleScreenManager.cs does two things: load the Gameplay scene and exit the game. This will now be modified so it navigates through the various UIs that were just created to host/join a lobby.

To begin, the script will need to import the necessary libraries to use TMP and Unity’s UI engine. The code for that is show below:

using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;
using UnityEngine.UI;

Next, a lot of new variables will be declared. Most of these will be used to attach various UI elements to TitleScreenManager.

public static TitleScreenManager instance;

[Header("UI Panels")]
[SerializeField] private GameObject mainMenuPanel;
[SerializeField] private GameObject PlayerNamePanel;
[SerializeField] private GameObject HostOrJoinPanel;
[SerializeField] private GameObject EnterIPAddressPanel;

[Header("PlayerName UI")]
[SerializeField] private TMP_InputField playerNameInputField;

[Header("Enter IP UI")]
[SerializeField] private TMP_InputField IpAddressField;

[Header("Misc. UI")]
[SerializeField] private Button returnToMainMenu;

private const string PlayerPrefsNameKey = "PlayerName";

Quick note: The [Header] tags aren’t variables or anything. They simply help label and organize things in the Unity editor. If you save TitleScreenManager.cs you will see the following in the Inspector when you have the TitleScreenManager object selected.

Most of the variables should be pretty self explanatory. The variables that end in “panel” are the UI panels that will be attached to the script. There are also other UI elements such as input fields and buttons to be added. The input fields are there to grab the entered text from them, and the button is the ReturnToMenuButton that will be added/removed as necessary.

The non-UI variables are TitleScreenManager instance and const string PlayerPrefsNameKey = "PlayerName". The instance variable will be used to create an instance of TitleScreenManager.cs for other objects to access, as has been done with other objects in the game. The PlayerPrefsNameKey string will be used to reference a PlayerPref. A PlayerPref is a saved piece of data that persists between game sessions. This will be used to save the player’s name so after they enter it once, it doesn’t need to be re-entered by the user if they don’t want to change it.

In the Unity editor, add all the UI elements to their corresponding variables in TitleScreenManager. They should look like this:

With all that added, it’s time to code!

Initial Menu Setup

In the scene right now, all UI elements are deactivated. To setup the initial menu, a function called ReturnToMainMenu will be created. The code is shown below.

public void ReturnToMainMenu()
{
	mainMenuPanel.SetActive(true);
	PlayerNamePanel.SetActive(false);
	HostOrJoinPanel.SetActive(false);
	EnterIPAddressPanel.SetActive(false);
	returnToMainMenu.gameObject.SetActive(false);
}

ReturnToMainMenu goes through each UI panel and and deactivates everything except for the MainMenuPanel.

In addition to ReturnToMainMenu, the code for the MakeInstance function that will create an instance can be created. It’s the same as all the other objects this has been done on.

void MakeInstance()
{
	if (instance == null)
		instance = this;
}

Both MakeInstance and ReturnToMainMenu should then be called in the Awake() function of TitleScreenManager.cs

The ReturnToMainMenu function will also be what you want the ReturnToMainMenu button to execute when it is clicked by the user. Save TitleScreenManager.cs and then go into the Unity editor to add the ReturnToMainMenu() function to the ReturnToMainMenu button’s “On Click ().”

“StartGame” and Saving the Player’s Name

In the old game, StartGame would load the Gameplay scene. Now, it will be changed so it transitions to the next UI where the player enters their name.

The player name will be saved to a PlayerPref key called “PlayerName.” When the PlayerName UI is loaded, TitleScreenManager will check if the PlayerName key exists. If the key exists, it will populate the text of the input field with that key. Then, in the PlayerName UI, if the player clicks on “Confirm Name,” whatever is in the input field will be saved as the player name to use. The code for all this is shown below.

public void StartGame()
{
	//SceneManager.LoadScene("Gameplay");
	mainMenuPanel.SetActive(false);
	PlayerNamePanel.SetActive(true);
	GetSavedPlayerName();
	returnToMainMenu.gameObject.SetActive(true);
}
private void GetSavedPlayerName()
{
	if (PlayerPrefs.HasKey(PlayerPrefsNameKey))
	{
		playerNameInputField.text = PlayerPrefs.GetString(PlayerPrefsNameKey);
	}
}
public void SavePlayerName()
{
	string playerName = null;
	if (!string.IsNullOrEmpty(playerNameInputField.text))
	{
		playerName = playerNameInputField.text;
		PlayerPrefs.SetString(PlayerPrefsNameKey, playerName);
		PlayerNamePanel.SetActive(false);
		HostOrJoinPanel.SetActive(true);
	}
}

StartGame will deactivate the MainMenuPanel then activate the PlayerNamePanel. It will also activate the ReturnToMainMenu button. It then calls the GetSavedPlayerName function.

GetSavedPlayerName checks if there is a playerPref key for “PlayerName.” If that key exists, it will put the key as PlayerNameInputField’s text.

SavePlayerName will be called when the player clicks the “Confirm Name” button. It will check to see if the text in PlayerNameInputField is empty or not. If it is not empty, it will save the text to the PlayerName key. It will then deactivate the PlayerNamePanel and Activate the HostOrJoinPanel.

Save TitleScreenManager.cs and go back to the Unity Editor. Double check to make sure that StartGameButton still calls StartGame in its “On Click ()”

Then go to the ConfirmPlayerName button under PlayerNamePanel. Set its On Click function to TitleScreenManager’s SavePlayerName

Host/Join a Game

The next UI’s are for the user to choose whether to host a game lobby or to join a lobby. Hosting a game will kick off some networking stuff that will be covered more later in this post. Joining a lobby will activate the EnterIPAddressPanel. The code for the two is shown below:

public void HostGame()
{
	Debug.Log("Hosting a game...");
	//networkManager.StartHost();
	HostOrJoinPanel.SetActive(false);
	returnToMainMenu.gameObject.SetActive(false);
}
public void JoinGame()
{
	HostOrJoinPanel.SetActive(false);
	EnterIPAddressPanel.SetActive(true);
}

The commented out code in HostGame() is what starts the hosted lobby. I’ll get to that later, I promise. Other than that, the code is pretty self-explanatory. UI is deactivated and activated as necessary.

Save TitleSceneManger.cs and then add the functions to the appropriate buttons. For HostGameButton, add the HostGame function

Then, for the JoinGameButton, add the JoinGame function

Joining a Lobby

The final UI to code out is trhe EnterIPAddressPanel UI where the player enters an IP/hostname to join. There is a text input field and a button. The text from the input field will be submitted as the game to join when the button is clicked. The code for the ConnectToGame function is shown below.

public void ConnectToGame()
{
	if (!string.IsNullOrEmpty(IpAddressField.text))
	{
		Debug.Log("Client will connect to: " + IpAddressField.text);
		//networkManager.networkAddress = IpAddressField.text;
		//networkManager.StartClient();
	}
	EnterIPAddressPanel.SetActive(false);
	returnToMainMenu.gameObject.SetActive(false);
}

Note again that two lines are commented out. That is the network code to join a lobby. That will all be discussed next! For now, though, save TitleScreenManager.cs and go back to the Unity editor. Add the ConnectToGame function to the JoinOnIP button.

Here’s a quick video of the UI doing its thing!

The NetworkManager

Now begins the descent into the dark arts of “Multiplayer.” Mirror uses a “NetworkManager” to handle server and client communications for your game. There is a default NetworkManager component that can be added to an object, but in this a custom NetworkManager will be created. It will simply be a script that inherits the NetworkManager from Mirror and does a bunch of overrides of functions and stuff.

To get started, create a new script in the TitleScreenScripts directory called NetworkManagerCC (the “CC” is for “CardConquest,” the name of this game).

Next, in the hierarchy, create a new empty game object and name it NetworkManager. Then, drag and drop the NetworkManagerCC script to NetworkManager to attach the script.

After the script has been attached to the object, open NetworkManagerCC.cs in VisualStudio. In the script, first add the following libraries so they are imported by the script.

using Mirror;
using System;
using System.Linq;
using UnityEngine.SceneManagement;

The most significant of these will be “Mirror,” which is where all the multiplayer and networking functionality will come from.

After the libraries/headers/whatever they are actually called have been imported, replace NetworkManagerCC : MonoBehaviour with NetworkManagerCC : NetworkManager. This will cause NetworkManagerCC to inherit from Mirror’s NetworkManager class, giving you access to all of that classes functionalities for server and client communication.

If you save NetworkManagerCC.cs now and go back to the Unity Editor, you’ll see that the NetworkManager has a bunch of new options now.

These are all options related to how NetworkManager will do network related…stuff. Things will be added to here as needed, but for now it can be left as is.

Player Prefabs

The next thing that will be required will be to make some player prefabs that NetworkManager will spawn as the game is created. NetworkManager does have a default “Player Prefab,” which an empty player object will be added to.

First, in the prefabs directory of your project, create a new directory called PlayerPrefabs.

Then in the hierarchy, rightclick and create a new empty gameobject. Rename it player. In the Inspector, you can add the default “Player” tag but it isn’t really important and you can leave it untagged. The important thing, though, for the NetworkManager’s player prefab is to make sure it has a “Network Identity” component. This will be required for all objects that are used over the network. If you’ve imported Mirror to your project, you should be able to search for “Network Identity” when adding a component

After Network Identity has been added, you can save the Player object as a prefab and then delete “Player” from the scene.

Next, you will want to add Player as the Player Prefab in Network Manager. Select Network Manager in the hierarchy and then drag the Player prefab to the “Player Prefab” variable.

LobbyPlayer and GamePlayer

In addition to the default Player Prefab, i will be creating two other prefabs for NetworkManager to spawn: LobbyPlayer and GamePlayer. As their names state, one prefab will be used when the players are in the lobby, and the other will be used when they are actually in the game. The main reason I am separating the two is because Dapper Dino did in their videos, and also because it will help keep things separate from the lobby and game that don’t need to be in both. For example, later in this post a lobby UI will be attached to LobbyPlayer, which won’t be needed GamePlayer in the next Gameplay scene, so it can be easier to just destroy the LobbyPlayer and create a separate, different GamePlayer.

First, create a new directory in the Scripts directory called PlayerScripts.

Then, create two new scripts called GamePlayer and LobbyPlayer.

Open both the GamePlayer and LobbyPlayer scripts in Visual Studio. They will both need the following headers/libraries added.

using UnityEngine.UI;
using Mirror;
using System.Linq;

Then, again for both, change “MonoBehaviour” with “NetworkBehaviour.” This will allow for client/server communications using Mirror. NetworkBehaviour inherits from MonoBehaviour, so all of the regular Unity functionality will also be present in NetworkBehaviour.

Save the scripts and then go back to Unity. In the hierarchy, create two new empty game objects. Name one LobbyPlayer and the other GamePlayer. Attach the corresponding scripts. You will notice in the Inspector that after you attach the script, the Network Identity component will automatically be added. This is from the NetworkBehaviour.

Both LobbyPlayer and Gameplayer can now be saved as prefabs and removed from the scene.

Finally, to add both LobbyPlayer and GamePlayer to NetworkManager, open NetworkManagerCC.cs in visual studio. The following variables can be added to the beginning of the script.

[SerializeField] public int minPlayers = 2;
[SerializeField] private LobbyPlayer lobbyPlayerPrefab;
[SerializeField] private GamePlayer gamePlayerPrefab;
public List<LobbyPlayer> LobbyPlayers { get; } = new List<LobbyPlayer>();
public List<GamePlayer> GamePlayers { get; } = new List<GamePlayer>();

minPlayers isn’t really necessary right now, but it will be used later so I figured I’d add it. The other variables are setting the LobbyPlayer and GamePlayer prefabs, and then create a list of each that will be used as players are added/removed from the game.

Save NetworkManagerCC.cs and return to Unity. Select the NetworkManager object, then add the LobbyPlayer and GamePlayer prefabs.

Creating the Lobby and Connecting

Now that the NetworkManager has been created and the player prefabs have been added, you can now have the game create a lobby/connect to a lobby.

This will be done first in the TitleScreenManager.cs script. In TitleScreenManager, first add a new variable that will be used to attach the NetworkManager object.

[SerializeField] private NetworkManagerCC networkManager;

Back in Unity, you can attach the NetworkManager object to TitleScreenManager.

Back in the TitleScreenManager.cs script, you can uncomment the lines of code in the HostGame and ConnectToGame functions that begin with networkManager. as they are what makes the necessary calls/connections for the lobby stuff.

In HostGame, networkManager.StartHost(); will basically create or “host” the game as a server for other players to join. The game will be hosted locally on port 7777 as specified in NetworkManager’s settings.

In ConnectToGame networkManager.networkAddress = IpAddressField.text; first specifies the IP/hostname that will be used for the connection. Then, networkManager.StartClient(); will start and try and establish a connection to that IP/hostname.

With all this set, you can now start to try and test out the hosting and joining a lobby. to simulate two players joining a game, you can first choose to “Build and Run” by going to File > Build and Run.

This will launch an .exe of the game in a new window. Then, you can just press play in the scene for the second player. I hosted the game from Unity and had the build version join. After the second player joined, you should see that there are two “Player” objects in your scene.

Adding LobbyPlayer Objects to the Scene

As the test showed, when the clients host or join the lobby, the Player object is spawn. However, the Player object is really just an empty object. The actual player object that we’re concerned about in the lobby is the LobbyPlayer prefab that was created. In order to spawn the LobbyPlayer prefab, some code will need to be added to NetworkManager.cs.

First, some debuging code will be added. This is just to help see in the console when clients are connecting and disconnecting from the lobby.

public override void OnStartClient()
{
	Debug.Log("Starting client...");
}
public override void OnClientConnect(NetworkConnection conn)
{
	Debug.Log("Client connected.");
	base.OnClientConnect(conn);
}
public override void OnClientDisconnect(NetworkConnection conn)
{
	Debug.Log("Client disconnected.");
	base.OnClientDisconnect(conn);
}

These functions are all overrides of functions from NetworkManager. These simply spit out some debug logs when the client first starts, when the client connects to the lobby/server, and then when the client disconnects from the lobby/server.

The next function to add will be an override of OnServerConnect that adds some logic to determine if the client is allowed to join the lobby.

public override void OnServerConnect(NetworkConnection conn)
{
	Debug.Log("Connecting to server...");
	if (numPlayers >= maxConnections) // prevents players joining if the game is full
	{
		Debug.Log("Too many players. Disconnecting user.");
		conn.Disconnect();
		return;
	}
	if (SceneManager.GetActiveScene().name != "TitleScreen") // prevents players from joining a game that has already started. When the game starts, the scene will no longer be the "TitleScreen"
	{
		Debug.Log("Player did not load from correct scene. Disconnecting user. Player loaded from scene: " + SceneManager.GetActiveScene().name);
		conn.Disconnect();
		return;
	}
	Debug.Log("Server Connected");
}

OnServerConnect makes two checks: 1.) if the number of players exceeds the maximum number of players (default is 4), do not let the client connect, and 2.) if the client is joining and their active scene is not “TitleScreen”, don’t let them connect.

Now, before actually adding the LobbyPlayer object, some changes are going to be made to LobbyPlayer.cs first.

Some New Variables for LobbyPlayer.cs

In LobbyPlayer.cs, add the following variables to the beginning of the script.

[SyncVar] public string PlayerName;
[SyncVar] public int ConnectionId;

[Header("Game Info")]
public bool IsGameLeader = false;

private const string PlayerPrefsNameKey = "PlayerName";

You may be wondering what the [SyncVar] tag means. SyncVars are a functionality of Mirror that allows for your game to “sync” a value/variable between the server and its clients. By setting PlayerName and ConnectionId as SyncVars, you should ensure that both the server and all clients have the same values for those variables.

The boolean variable IsGameLeader will be used to track who is the “leader” of the game, or the player who is hosting the lobby. This will be set when NetworkManagerCC.cs adds the player to the server.

The string constant PlayerPrefsNameKey is used later when setting the player’s name from their PlayerPref key value that was set before the player joined the lobby.

Back to NetworkManagerCC.cs

With these variables added, the LobbyPlayer can be added to the server by NetworkManager.cs. A new function that is an override of OnServerAddPlayer will be created.

public override void OnServerAddPlayer(NetworkConnection conn)
{
	Debug.Log("Checking if player is in correct scene. Player's scene name is: " + SceneManager.GetActiveScene().name.ToString() + ". Correct scene name is: TitleScreen");
	if (SceneManager.GetActiveScene().name == "TitleScreen")
	{
		bool isGameLeader = LobbyPlayers.Count == 0; // isLeader is true if the player count is 0, aka when you are the first player to be added to a server/room

		LobbyPlayer lobbyPlayerInstance = Instantiate(lobbyPlayerPrefab);

		lobbyPlayerInstance.IsGameLeader = isGameLeader;
		lobbyPlayerInstance.ConnectionId = conn.connectionId;

		NetworkServer.AddPlayerForConnection(conn, lobbyPlayerInstance.gameObject);
		Debug.Log("Player added. Player name: " + lobbyPlayerInstance.PlayerName + ". Player connection id: " + lobbyPlayerInstance.ConnectionId.ToString());
	}
}

OnServerAddPlayer does the following:

  • Checks if the player is connecting from the TitleScreen scene
    • If the LobbyPlayer list has a count of 0, set the IsGameLeader value to true. Else, set to false
      • The player who hosts the game will be the first player to join, so the list will be empty when they first join. So, this should only ever be true when the host joins
    • Instantiate the lobbyPlayerInstance object using the LobbyPlayer prefab
    • Set the IsGameLeader value
    • set the ConnectionId value
    • use NetworkServer.AddPlayerForConnection to add the lobbyPlayerInstance object as being under the Authority of the client that just connected
      • The “client” is the “conn” parameter that is passed – the client’s connection that was just made to the server
      • Authority will be covered later! But it is important!

Setting the Player Name on the Player Object and Adding them to The List

The LobbyPlayer object is now being spawned when the client connects to the server. However, a few more things need to be done to make this work correctly. First, setting the player name.

Right now, when the player confirms their name, BEFORE hosting or joining a lobby, that name is saved as a PlayerPref. However, right now, these are not actually set on the LobbyPlayer objects.

The next step will be to set the PlayerName value from the PlayerPrefs key “PlayerName.” Two new functions will need to be created. The first is an override of OnStartAuthority and then a [Command] function called CmdSetPlayerName.

public override void OnStartAuthority()
{
	CmdSetPlayerName(PlayerPrefs.GetString(PlayerPrefsNameKey));
}
[Command]
private void CmdSetPlayerName(string playerName)
{
	PlayerName = playerName;
	Debug.Log("Player display name set to: " + playerName);
}

OnStartAuthority is a function that will run in a similar manner to Unity’s Start() function that has been used before, but instead will only run if the client has Authority over the object. Authority is basically who owns the gameobject, specifically which connection owns the object which could be either the server or one of the clients.

When an object is spawn by the server it is by default owned by the server, or the server has authority over that object. However, since NetworkMAnagerCC used NetworkServer.AddPlayerForConnection to instantiate the LobbyPlayer, the client that was specified will have authority over that LobbyPlayer. So, OnStartAuthority will on execute on that specific client’s game. The LobbyPlayer object will exist on all clients, but OnStartAuthority will only execute on the client with the correct Authority.

The CmdSetPlayerName function is a Mirror Command. A command is essentially a request from your client sent to the server, and then that function is executed on the server.

So, what happens is this:

  • The LobbyPlayer object on your client calls the command CmdSetPlayerName and passes the value of the “PlayerName” PlayerPrefs key
  • On the server, CmdSetPlayerName sets the PlayerName value of the LobbyPlayer object on the server
  • Because PlayerName is a SyncVar, the change to PlayerName’s value of the LobbyPlayer object on the server is then sync’d back to your client’s LobbyPlayer object

Alright, now, with the PlayerName value set on the server and client, it’s time to add the new LobbyPlayer to NetworkManagerCC’s LobbyPlayers list. To do this, first you will create a singleton of NetworkManagerCC called “Game.” This will allow you to access the NetworkManagerCC instance by the name “Game.”

private NetworkManagerCC game;
private NetworkManagerCC Game
{
	get
	{
		if (game != null)
		{
			return game;
		}
		return game = NetworkManagerCC.singleton as NetworkManagerCC;
	}
}

Then, two new override functions will be created for OnStartClient and OnStopClient. These functions are for when a clients and disconnects from the server, respectively.

public override void OnStartClient()
{
	Game.LobbyPlayers.Add(this);
	Debug.Log("Added to GamePlayer list: " + this.PlayerName);
}
public override void OnStopClient()
{
	Debug.Log(PlayerName + " is quiting the game.");
	Game.LobbyPlayers.Remove(this);
	Debug.Log("Removed player from the GamePlayer list: " + this.PlayerName);
}

The important thing these functions are doing is adding and removing the LobbyPlayer object from the LobbyPlayers list on NetworkManagerCC.

You can now run the lobby test again by using Build and Run to execute a .exe, and starting another game in the Unity editor. I hosted the game from my Unity game and set the player name to Player1. I set the player name in the second game to player2 and connected to the lobby.

In the hierarchy I can see two LobbyPlayer objects.

On the player1 object, I can see that it was set to the Game Leader

And player2 has their name set and is not the game leader.

Creating the Lobby UI

So, players can join a lobby now. Great! But, there isn’t anything in the lobby for them to do or see. It’s just a blank screen.

To make the “lobby” that the player actually sees, some more UI will need to be added. The lobby UI should do the following:

  • Display each player’s name
  • Display each player’s “Ready” status
  • Have a button to ready up
  • have a button to quit the lobby
  • For the GAME LEADER ONLY, have a button to start the game

If you watched the Dapper Dino video on creating a lobby, you can see that they have all the lobby UI elements attached to the player prefab. I tried to avoid this and just have the Lobby UI as a child object in my main scene’s canvas, but I couldn’t seem to get it to work. So, I decided to just do what Dapper Dino did and have the lobby UI attached to the LobbyPlayer prefab.

Open up the LobbyPlayer prefab by double clicking it from the PlayerPrefabs directory. Then, add a Canvas as a child of LobbyPlayer by right clicking on LobbyPlayer, going to UI, then Canvas. Rename the Canvas to LobbyPlayerCanvas.

Configure LobbyPlayerCanvas so it matches the settings from the scene’s canvas. That would be following settings:

  • Render Mode: Screen Space – Camera
  • Reference Resolution: 1280×720
  • Screen Match Mode: Match Width or Height
  • Match: 0.5

Next, create a new panel object as a child of LobbyPlayerCanvas. Rename the panel to LobbyPanel and delete the image component.

Now, make another panel object, this time as a child of LobbyPanel. Rename it as Player1ReadyPanel, but leave it’s Image component.

Select Player1ReadyPanel and go to the Inspector. Make sure its anchor is “stretch”, and then set the following values:

  • Left: 100
  • Top: 200
  • Right: 100
  • Bottom

You should see a narrow grey box in your scene view:

Back in the hierarchy, create two new text objects that are children of Player1ReadyPanel. Rename them Player1Name and Player1ReadyText.

Select Player1Name in the hierarchy, and in the Inspector, make sure the anchor is set to Middle Stretch.

Then, set the following values for Player1Name

  • Left: 0
  • Right: 300
  • Height: 100
  • Text: Player Name
  • Font: ThaleahFat
  • Font Size: 100
  • Alignment: Middle
  • Color: R:51,G:255,B:0

Then, select Player1ReadyText and make sure its anchor is also Middle Stretch. Then, set the following:

  • Left: 800
  • Right: 0
  • Height: 100
  • Text: Not ready
  • Font: ThaleahFat
  • Font Size: 65
  • Alignment: Middle
  • Color: R:255,G:0,B:0

In your scene, you should not see the following:

Go ahead and copy and paste Player1ReadyPanel. This should duplicate everything including the text. Then, rename everything referencing “Player1” to “Player2.”

Select Player2ReadyPanel. In the Inspector, set the following:

  • Left: 100
  • Top: 350
  • Right: 100
  • Bottom: 250

This should have “lowered” Player2ReadyPanel in the scene. The scene should look like this:

Now it’s time to make some buttons. Add a button object as a child of LobbyPanel and rename it to ReadyUpButton.

Select ReadyUpButton and in the inspector, set the following values:

  • Pos X: -390
  • Pos Y: -200
  • Width: 300
  • Height: 75
  • Uncheck “Fill Center”

Select the Text child object of ReadyUpButton in the hierarchy. In the inspector, set the following values:

  • Text: Ready Up
  • Font: ThaleahFat
  • Font Size: 60
  • Color: R:55, G:255, B:0

After ReadyUpButton has been set up, go back to the hierarchy and copy and past it twice. Rename the second button StartGameButton and the third button QuitGameButton.

For StartGameButton, set its Pos X value to 0, and its text to “Start Game.”

For QuitGameButton, set its Pos X value to 390 and its text to “Quit Game.”

You should now see the following as the LobbyPlayer UI:

That should be all of the necessary UI elements, as I will only be testing with 2 players for the forseeable future. Maybe someday I can add a third or fourth! But not right now…

Before moving onto the next step, deactivate the Player1ReadyPanel, Player2ReadyPanel, and the StartGameButton. It will look like this in the hierarchy.

Then, deactivate LobbyPanel. Next, some scripting will be done to activate and deactivate the Panels and buttons as necessary.

Adding the UI Elements to the LobbyPlayer Script

Now, the various Lobby UI elements can be added to LobbyPlayer.cs. In the variables section, add the following:

[Header("UI")]
[SerializeField] private GameObject PlayerLobyUI;
[SerializeField] private GameObject Player1ReadyPanel;
[SerializeField] private GameObject Player2ReadyPanel;
[SerializeField] private GameObject startGameButton;
[SerializeField] private Button readyButton;

Save LobbyPlayer.cs and then go back to Unity. Open the LobbyPlayer prefab again, and add the UI objects to their corresponding variables on the LobbyPlayer object.

Activating the Lobby UI

To activate the Lobby UI when the player joins a lobby, go to LobbyPlayer.cs and add the following if statement in the OnStartAuthority function.

if (!PlayerLobyUI.activeInHierarchy)
	PlayerLobyUI.SetActive(true);

This will simply activate the LobbyPanel object if it is not active when the player joins. Because it is done in OnStartAuthority, it will only occur on the LobbyPlayer object that the client has authority over.

Updating the UI when Players Join

With the Lobby UI active, it will need to be updated whenever a player joins or leaves or otherwise changes something (like their ready status).

To begin, you will need a way for the client to determine which LobbyPlayer object is the one they have authority over. This will be needed so that only the player’s object’s UI is updated.

One quick way I found to locate the client’s LobbyPlayer object, is to change its name when it’s spawn. In OnStartAuthority, I added the following line to rename the LobbyPlayer object to “LocalLobbyPlayer.”

gameObject.name = "LocalLobbyPlayer";

Then, in the future when I want to find the LobbyPlayer the client has authority over, you can use the following line of code:

GameObject localPlayer = GameObject.Find("LocalLobbyPlayer");

Updating the Lobby UI

Now it’s time to code out the functions that will update the lobby UI when players join/leave and when their statuses change. The first function will be UpdateLobbyUI

public void UpdateLobbyUI()
{
	Debug.Log("Updating UI for: " + this.PlayerName);
	GameObject localPlayer = GameObject.Find("LocalLobbyPlayer");
	if (localPlayer != null)
	{
		localPlayer.GetComponent<LobbyPlayer>().ActivateLobbyUI();
	}
}

UpdateLobbyUI first finds the LobbyPlayer object the client has authority over by searching for the LobbyPlayer that was renamed to LocalLobbyPlayer. Then, if that LobbyPlayer was found, it will call ActivateLobbyUI, which will be coded next!

Below is the code for ActivateLobbyUI

public void ActivateLobbyUI()
{
	Debug.Log("Activating lobby UI");
	if (!PlayerLobyUI.activeInHierarchy)
		PlayerLobyUI.SetActive(true);
	if (Game.LobbyPlayers.Count() > 0)
	{
		Player1ReadyPanel.SetActive(true);
		Debug.Log("Player1 Ready Panel activated");
		Player2ReadyPanel.SetActive(false);
	}
	else
	{
		Debug.Log("Player1 Ready Panel not activated. Player count: " + Game.LobbyPlayers.Count().ToString());
	}
	if (Game.LobbyPlayers.Count() > 1)
	{
		Player2ReadyPanel.SetActive(true);
		Debug.Log("Player2 Ready Panel activated");
	}
	else
	{
		Debug.Log("Player2 Ready Panel not activated. Player count: " + Game.LobbyPlayers.Count().ToString());
	}
	UpdatePlayerReadyText();
}

ActivateLobbyUI does the following:

  • Checks if the LobbyPanel is active. If not, activates it
  • Checks Game.LobbyPlayers to see if the count is greater than 0. If true:
    • Activate the Player1ReadyPanel
    • Deactivate the Player2ReadyPanel
      • This is just to make sure that the Player2ReadyPanel gets deactivated if the player count drops from 2 to 1
  • Checks Game.LobbyPlayers to see if the count is greater than 1. If true:
    • Activate Player2ReadyPanel
  • Calls UpdatePlayerReadyText

UpdatePlayerReadyText will update the “not ready”/”ready” status of a player based on a boolean variable that will need to be added, IsReady. IsReady will be a SyncVar so that all clients receive ready status updates of the different clients.

[Header("Game Info")]
public bool IsGameLeader = false;
[SyncVar] public bool IsReady = false;

The code for UpdatePlayerReadyText is shown below:

public void UpdatePlayerReadyText()
{
	if (Player1ReadyPanel.activeInHierarchy && Game.LobbyPlayers.Count() > 0)
	{
		foreach (Transform childText in Player1ReadyPanel.transform)
		{
			if (childText.name == "Player1Name")
				childText.GetComponent<Text>().text = Game.LobbyPlayers[0].PlayerName;
			if (childText.name == "Player1ReadyText")
			{
				bool isPlayerReady = Game.LobbyPlayers[0].IsReady;
				if (isPlayerReady)
				{
					childText.GetComponent<Text>().text = "Ready";
					childText.GetComponent<Text>().color = Color.green;
				}
				else
				{
					childText.GetComponent<Text>().text = "Not Ready";
					childText.GetComponent<Text>().color = Color.red;
				}
			}
		}
	}
	if (Player2ReadyPanel.activeInHierarchy && Game.LobbyPlayers.Count() > 1)
	{
		foreach (Transform childText in Player2ReadyPanel.transform)
		{
			if (childText.name == "Player2Name")
				childText.GetComponent<Text>().text = Game.LobbyPlayers[1].PlayerName;
			if (childText.name == "Player2ReadyText")
			{
				bool isPlayerReady = Game.LobbyPlayers[1].IsReady;
				if (isPlayerReady)
				{
					childText.GetComponent<Text>().text = "Ready";
					childText.GetComponent<Text>().color = Color.green;
				}
				else
				{
					childText.GetComponent<Text>().text = "Not Ready";
					childText.GetComponent<Text>().color = Color.red;
				}
			}
			Debug.Log("Updated Player2 Ready panel with player name: " + Game.LobbyPlayers[1].PlayerName + " and ready status: " + Game.LobbyPlayers[1].IsReady);
		}
	}
}

UpdatePlayerReadyText does the following

  • Checks if Player1ReadyPanel is active AND if Game.LobbyPlayers’ count is greater than 0. If true:
    • Iterates through each child object of Player1ReadyPanel
      • Updates Player1Name’s text to the PlayerName of the first LobbyPlayer object in Game.LobbyPlayer
      • Updates the text of Player1Ready text as follows:
        • If IsReady is true:
          • Set text to “Ready” and the color to green
        • if IsReady is false:
          • set text to “Not Ready” and color to red
  • Repeats the above but for Player2ReadyPanel and its child objects

Allowing the Player to Ready Up

So, right now, the game has a way to update the lobby UI to reflect a player’s ready status, but it doesn’t actually have a way to let the player change their ready status. We’ll need to make a function that is called when the ReadyUpButton is clicked by the user.

The function will be pretty simple. Similar to updating the player’s PlayerName, this will be a [Command] that is executed on the server. The code for the CmdReadyUp command function is shown below

[Command]
public void CmdReadyUp()
{
	IsReady = !IsReady;
	Debug.Log("Ready status changed for: " + PlayerName);
}

All CmdReadyUp does is flip the boolean value of IsReady for the LobbyPlayer. Because IsReady is a SyncVar, the change will then be synced to the clients.

Back in Unity, open the LobbyPlayer prefab and make sure that the ReadyUpButton has CmdReadyUp added to its OnClick()

Update the UI on PlayerName and IsReady changes

If you were to run the game right now to test out the UI, you would notice that while the Player(1/2)ReadyPanels are being activated correctly, they are not filling out the PlayerNames in the UI, and clicking ready doesn’t change the “Not Ready” text. If you look at the LobbyPlayer objects in the Inspector of the game running in Unity, you will see that the PlayerName and IsReady values are being updated and synchronized correctly. Why isn’t the UI updating?

Well, it’s because that while the SyncVar values are updating, the game doesn’t know to update the UI when that value changes. When a player connects to the lobby, the debug output should show you that the PlayerReadyPanel is added when the PlayerName is blank. So, basically, because the UI is updated before the SyncVar values are actually set, the UI is showing “incorrect” or out-of-date data.

To fix this, you can add “hooks” to the SyncVar variables. A SyncVar hook is a way to tell Unity “hey, when this variable is update, do this thing,” where that “thing” will be a function to call.

So, hooks will need to be added to PlayerName and IsReady. The hooks will be called HandlePlayerNameUpdate and HandlePlayerReadyStatusUpdate for their respective variables.

[SyncVar(hook = nameof(HandlePlayerNameUpdate))] public string PlayerName;
[SyncVar(hook = nameof(HandlePlayerReadyStatusUpdate))] public bool IsReady = false;

The hook functions will both do the same thing. They will call UpdateLobbyUI to update the UI of the LocalLobbyPlayer. As UpdateLobbyUI works through all it’s stuff, the playername values and ready statuses will update appropriately.

public void HandlePlayerNameUpdate(string oldValue, string newValue)
{
	Debug.Log("Player name has been updated for: " + oldValue + " to new value: " + newValue);
	UpdateLobbyUI();
}
public void HandlePlayerReadyStatusUpdate(bool oldValue, bool newValue)
{
	Debug.Log("Player ready status has been has been updated for " + this.PlayerName + ": " + oldValue + " to new value: " + newValue);
	UpdateLobbyUI();
}

Look, the names and ready statuses update!

After making that video, I decided to make a change to the UpdatePlayerReadyText function to change the text of the “Ready Up” button. If IsReady is true, it will change the text to “Unready.” If IsReady is false, the text will be set to “ReadyUp.”

if (IsReady)
{
	readyButton.GetComponentInChildren<Text>().text = "Unready";
}
else
{
	readyButton.GetComponentInChildren<Text>().text = "Ready Up";
}

Checking if All Players are Ready and Starting the Game

Now that players can ready up, there should be something that checks if all players are ready, and if they are all ready, allow the host of the game (or GameLeader) to start the game.

First, the function CheckIfAllPlayersAreReady will be created. If all players are ready, it will activate the StartGameButton on the GameLeader’s/host’s UI. The code for CheckIfAllPlayersAreReady is below:

public void CheckIfAllPlayersAreReady()
{
	Debug.Log("Checking if all players are ready.");
	bool arePlayersReady = false;
	foreach (LobbyPlayer player in Game.LobbyPlayers)
	{
		if (!player.IsReady)
		{
			Debug.Log(player.PlayerName + "is not ready.");
			arePlayersReady = false;
			startGameButton.SetActive(false);
			break;
		}
		else
		{
			arePlayersReady = true;
		}

	}
	if (arePlayersReady)
		Debug.Log("All players are ready");

	if (arePlayersReady && IsGameLeader && Game.LobbyPlayers.Count() >= Game.minPlayers)
	{
		Debug.Log("All players are ready and minimum number of players in game. Activating the StartGame button on Game leader's UI.");
		startGameButton.SetActive(true);
	}
	else
	{
		startGameButton.SetActive(false);
	}

}

CheckIfAllPlayersAreReady will do the following:

  • Creates a bool variable arePlayersReady
  • Iterates through each LobbyPlayer in Game.LobbyPlayers
    • if one of the LobbyPlayer’s has IsReady set to false:
      • set areReadyPlayers to false
      • deactivate the StartGameButton
      • break out of the foreach loop
        • This ensures that if just one LobbyPlayer has IsReady set to false, arePlayersReady will be false as well
    • Otherwise, set arePlayersReady to true
  • If arePlayersReady is true, AND the client is the game leader, AND the count of Game.LobbyPlayers is greater than or equal to the minimum player count (set to 2 right now)
    • Activate the StartGameButton
  • If the above is false, deactivate the StartGameButton

The above should make sure that the StartGameButton is only activated when all players are ready. If a player unready’s, the startgamebutton should deactivate, and if a player leaves the game so there are fewer than 2 players, the startGameButton will also deactivate.

Starting The Game

Now the host should be able to start the game. “Starting the game” will mean that the server changes the level from the TitleScreen scene to the GamePlay scene. In order to make sure that all clients change to the correct level, a few things will need to be done on the server.

  • Verify that the game can start: That all players are ready and that there are enough players
    • This is just a server validation in case a client somehow triggered a “start game” call and bypassed the requirements to start the game
  • Call “ServerChangeScene” with the specified scene to change to
    • When the scene is changed, the LobbyPlayer objects will be destroyed. ServerChangeScene will also then be used to spawn the GamePlayer prefabs to replace the destroyed LobbyPlayer objects

All this code will be added on NetworkManagerCC.cs. To start, below is the code for CanStartGame which will do the server validation that the game can start.

private bool CanStartGame()
{
	if (numPlayers < minPlayers)
		return false;
	foreach (LobbyPlayer player in LobbyPlayers)
	{
		if (!player.IsReady)
			return false;
	}
	return true;
}

This is basically just a repeat of the client side verification to start a game. Check if there enough players and check if all players are ready. If either is untrue, return false.

The next function is StartGame, code shown below.

public void StartGame()
    {
        if (CanStartGame() && SceneManager.GetActiveScene().name == "TitleScreen")
        {
            ServerChangeScene("Gameplay");
        }
    }

StartGame checks if CanStartGame returns true and if the current scene is TitleScreen. If those are both true, call ServerChangeScene with the “Gameplay” scene.

A override of ServerChangeScene will be created now, shown below:

public override void ServerChangeScene(string newSceneName)
{
	//Changing from the menu to the scene
	if (SceneManager.GetActiveScene().name == "TitleScreen" && newSceneName == "Gameplay")
	{
		for (int i = LobbyPlayers.Count - 1; i >= 0; i--)
		{
			var conn = LobbyPlayers[i].connectionToClient;
			var gamePlayerInstance = Instantiate(gamePlayerPrefab);

			NetworkServer.Destroy(conn.identity.gameObject);
			NetworkServer.ReplacePlayerForConnection(conn, gamePlayerInstance.gameObject, true);
		}
	}
	base.ServerChangeScene(newSceneName);
}

ServerChangeScene makes a sanity check that the current scene is TitleScreen, and that the new scene will be Gameplay. If those are both true, ServerChangeScene will do the following:

  • Iterate through each LobbyPlayer
    • get the connection of a client
    • spawn a new GamePlayer object from the gamePlayerPrefab
    • Destroy the LobbyPlayer object
    • Replace the client’s “player” with the spawned GamePlayer object
      • This associates the client’s connection with the new GamePlayer object. This gives the client Authority over that GamePlayer
  • Then the normal ServerChangeScene functions are executed

Now, the player hosting the game will need to be able to call StartGame. In LobbyPlayer.cs a new command function called CmdStartGame will be created. Code below

[Command]
public void CmdStartGame()
{
	Game.StartGame();
}

Then, in Unity, open the LobbyPlayer prefab and add CmdStartGame to the StartGameButton.

Here’s a video of a player starting the game!!!

Quiting the Game Mid-Lobby

You may remember that a “Quit Game” button had been created. This will let a player leave the lobby before a game starts.

The first thing to do is, in LobbyPlayer.cs, create a new function called QuitLobby. Code below:

public void QuitLobby()
{
	if (hasAuthority)
	{
		if (IsGameLeader)
		{
		
			Game.StopHost();
		}
		else
		{
		
			Game.StopClient();
		}
	}
}

Really all QuitLobby is doing is checking if the player is a leader or not, and then calling the appropriate function from NetworkManagerCC to have that player to leave the game. For the leader, it is StopHost. For the client, it is StopClient.

If you were to run the game as is and quit a lobby, the Lobby UI would disappear and you’d be left with a blank screen. So, to reload the main menu UI, you can use OnDestroy to call TitleScreenManager’s ReturnToMainMenu. Code below:

private void OnDestroy()
{
	if (hasAuthority)
		TitleScreenManager.instance.ReturnToMainMenu();
}

OnDestroy works here because when the host or client leaves the lobby, the LobbyPlayer object will be destroyed. So, when the LobbyPlayer the client has authority over is destroyed, ReturnToMainMenu from TitleScreenManager will be called to return to the main menu UI.

One thing I thought about with using OnDestroy is that when the game is started, LobbyPlayer is also destroyed. Based on my console logs, the call to ReturnToMainMenu is made when the game starts, but since the scene is immediately changed, the player never sees it. Hopefully that doesn’t cause any issues!

Cleanup on NetworkManagerCC

When players quit the lobby, it’s important to make sure some things on NetworkManagerCC, namely, the LobbyPlayers list. Its important to make sure LobbyPlayers are removed when they quit the lobby, and that the list is cleared when the host quits the lobby and a new one has to be created.

This will be done by creating overrides of OnServerDiscconect and OnStopServer. Code below

public override void OnServerDisconnect(NetworkConnection conn)
{
	if (conn.identity != null)
	{
		LobbyPlayer player = conn.identity.GetComponent<LobbyPlayer>();
		LobbyPlayers.Remove(player);
	}
	base.OnServerDisconnect(conn);
}

public override void OnStopServer()
{
	LobbyPlayers.Clear();
}

Initializing the GamePlayer

Everything works! Or at least I think it all works. A player can choose to either host a game or join a game, and after everyone is ready, they can start the game and load the Gameplay scene.

But wait! If you pay attention to the hierarchy in the game you are running in Unity, you will see that the GamePlayers aren’t there. Why? They should have been spawned by ServerChangeScene, right?

Well, the GamePlayers were spawned by ServerChangeScene, but they were spawned in the previous scene. When the scene changed, everything from the previous scene was destroyed. Except the NetworkManager. Why wasn’t the NetworkManager destroyed? Well, because it is marked as “DontDestroyOnLoad.”

So, to have the GamePlayers not be destroyed when the scene changes, they need to be marked as DontDestroyOnLoad as well. To get this all setup, I’m just going to initialize a bunch of stuff for the GamePlayer. Notice that DontDestroyOnLoad(gameObject); is called in OnStartClient. All the code for GamePlayer.cs is shown below:

public class GamePlayer : NetworkBehaviour
{
    [SyncVar] public string PlayerName;
    [SyncVar] public int ConnectionId;

    private NetworkManagerCC game;
    private NetworkManagerCC Game
    {
        get
        {
            if (game != null)
            {
                return game;
            }
            return game = NetworkManagerCC.singleton as NetworkManagerCC;
        }
    }
    public override void OnStartAuthority()
    {
        gameObject.name = "LocalGamePlayer";
        Debug.Log("Labeling the local player: " + this.PlayerName);
    }
    public override void OnStartClient()
    {
        DontDestroyOnLoad(gameObject);
        Game.GamePlayers.Add(this);
        Debug.Log("Added to GamePlayer list: " + this.PlayerName);
    }
    public override void OnStopClient()
    {
        Debug.Log(PlayerName + " is quiting the game.");
        Game.GamePlayers.Remove(this);
        Debug.Log("Removed player from the GamePlayer list: " + this.PlayerName);
    }
    [Server]
    public void SetPlayerName(string playerName)
    {
        this.PlayerName = playerName;
    }
    [Server]
    public void SetConnectionId(int connId)
    {
        this.ConnectionId = connId;
    }
}

So, when the GamePlayer object is created, it will be set as DontDestroyOnLoad. The GamePlayer the client has authority over will be renamed to LocalGamePlayer. There are also functions to set the playername and connectionid.

To set the PlayerName and ConnectionId of the GamePlayer, and to make sure they match what was there for the LobbyPlayer, two lines of code are added to NetworkManagerCC.cs’s ServerChangeScene function.

gamePlayerInstance.SetPlayerName(LobbyPlayers[i].PlayerName);
gamePlayerInstance.SetConnectionId(LobbyPlayers[i].ConnectionId);

Now, when you start a game, you should see the GamePlayer objects under the DontDestroyOnLoad section of the hierarchy.

Yay!

Next Steps

The players can create and join a lobby and start the game. Great! They can’t, however, play any of the game, at least not as multiplayer. In CardConquet’s current state, they’re both just connected to a game and playing singleplayer games. That sounds awfully lonely…

So, now to figure out how to convert all that stuff I made in previous posts to a multiplayer game! I have no idea where to even begin!

Well, actually, I think I am going to begin by going through M.S. Farzan‘s youtube tutorial “How to Create a 2D Card Game in Unity” series where they first create a card game, then convert it to a multiplayer game. It’s only like 8 hours of videos. It can’t take me that long.

Anyway, good luck (to myself)!