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. Zip archives for specific posts can be found here.
Now that CardConquest allows players to join a lobby and then a game, it is time to start making some of the actual gameplay (or what I have so far that can be called gameplay) multiplayer as well.
When players start a game, they load the “Gameplay” scene and are in the “Unit Placement” phase where they decide where to place their units to start the game. As of the previous post in the series, the players are essentially playing a single player game while connected to one another. Not really useful as far as a multiplayer game is concerned.
After going through and beginning to convert the gameplay to multiplayer, it became quite apparent that a lot of my previous code will basically be re-written. Oh Well! The previous code gave me a solid foundation and reference on which to write the new code!
Anyway, onto making this game multiplayer!
Giving Everything a Network Identity
In order for objects to be used in a “multiplayer” game using Mirror, those objects will need a “Network Identity.” This was very briefly touched on before with the LobbyPlayer and GamePlayer objects that were used when hosting/joining the lobby to start a game. The Network Identity is how the multiplayer server/Mirror tracks each networked object in the game. When you start the game in the Unity editor and go through the lobby process, if you select the “GamePlayer” object in the hierarchy, you will see the network identity information in the bottom right corner of Unity.

The “Network ID” is a number that acts as a unique identifier for the object in the game. As more objects are added to the game, the number increases. You can also see here other information such as “IS Client” and “Is Server,” which tells you if you are a client or the server (since this is from a game the unity editor is hosting, it is both a client and the server), and also tells you if you have authority over the object or if the object is the local player object and so on.
Any object that will be used in the game, and controlled in part by the server or will be affected in someway by the “multiplayer” aspect of the game, will need a network identity as well. For the most part, everything in the “Gameplay” scene will need to have a network identity attached.
I started by going through my prefabs and figuring out which objects would need to be networked and have the Network Identity added. In my project, I created a new directory called OfflinePrefabs to store the prefabs that didn’t need to be networked.

The main prefabs that I have that seemed like they wouldn’t need to be networked are the “outline” prefabs used for units and land, as well as the “don’t place here” markers used during Unit Placement. These prefabs are really only in the game to visual indicate to the player about what they have selected and where they can and cannot place their own units. These don’t need to be visible to any other players, only the local player. So, these will not need network identities.
I created two sub directories in OfflinePrefabs called MapPrefabs and UnitPrefabs.

In MapPrefabs, I added “base-marker-cant-place-here” and “land-outline”

In UnitPrefabs, I added “soldier-outline” and “tank-outline”

The remaining prefabs should all be the “online” prefabs that will require network identities. To help organize the prefabs folder, I created new MapPrefabs and UnitPrefabs sub directories in the Prefabs directory.

In the MapPrefabs, I added the following:

And then in UnitPrefabs:

With all that sorted, you can begin adding a network identity to everything under the regular “Prefabs” directory. Using “soldier-green” under UnitPrefabs as an example, open the prefab and then click “Add component”

Search for “Networkidentity” and select the NetworkIdentity component to add it.

Once the NetworkIdentity has been added, you should see the following:

And now you get to add the NetworkIdentity to everything in the prefabs directory! It’s really fun, especially when you have to do this twice because I decided to maintain a local copy of CardConquest as well as a version just for this blog. Enjoy!
Creating Some New Prefabs
Two existing objects in the Gameplay scene that I decided to make networked prefabs are MouseClickManager and GameplayManager. This probably isn’t necessary, but it is what I decided to do! Make sure to add the NetworkIdentity component, and then save them to the prefabs folder.

I then wanted to add some prefabs of “holder” objects. These will be objects that hold the player’s cards, their units, and then all the tiles of the map.
First, create a new empty gameobject and name it PlayerCardHand. If you still have the older PlayerCardHand in the scene, it can be deleted before you do this (delete all the child card objects as well). Make sure to tag PlayerCardHand with the “PlayerHand” tag, then add the NetworkIdentity component and add it to the CardPrefabs directory.

For the player unit holder, first delete the currently exist “PlayerUnitHolder” and then create a new empty objects called “PlayerUnitHolder.” Add the PlayerUnitHolder tag and the NetworkIdentity Component, and add it to the UnitPrefabs directory.

The reason that the older objects are being deleted and new, empty objects are being created as prefabs is that when the game is played, the server will add the GamePlayer objects, and then spawn the unit holder and card holder objects, then the subsequent units and cards, and then assign the proper authority to the correct player. It will hopefully make more sense later.
For the “map” holder, create a new empty game object called “2PlayerMap.” Make sure to reset its transform to 0,0,0, add the network identity, and save it in MapPrefabs.

I also saved my EscMenuManager object in my offline prefabs. Because this is offline, you will not need to add the network identity.

Units for the Second Player
Since this game will not have a whole two! (2!) players, some new unit prefabs will be added for the second player.
The tank prefab for the second player will be the easiest. Go to the UnitPrefabs directory, select “tank-pixel,” then use the “ctrl + d” keyboard shortcut to duplicate the tank-pixel prefab. You should now see a “tank-pixel 1” prefab.

In the game, Player1 will spawn from the left side, with their units facing toward the right. So, Player2 will spawn from the right side with their units facing toward the left. To make the Player2 tank face the left, open the tank-pixel 1 prefab and add a “-” to the X scale value to make it -0.4. You should see in the preview that the tank “flipped” directions along the x-axis.

Rename the new tank prefab to tank-player-2.
One thing to remember about the tank prefab is that its script has the tank “outline” attached to it. Because the outline is really just spawning a new prefab, a new outline will need to be created for tank-player-2. The same process will be done. Duplicate the tank-outline prefab in OfflinePrefabs\UnitPrefabs and make the X scale value negative.

Then, go back to the tank-player-2 prefab in Prefabs\UnitPrefabs and set the outline to tank-outline-player-2. Note: If you click the little padlock in the way upper right of the inspector, it will “lock” the inspector to tank-player-2 while you navigate to the outline prefab to drag and drop it on. Just remember to unlock it when you’re done!

Now for the Player2 infantry. This one will have a different sprite that is blue. The sprite can be found below:

Add the sprite to your project’s Sprites directory. A similar process to the tank will be followed, though. Go to Prefabs\UnitPrefabs and duplicate soldier-green.

Open the soldier-green 1 prefab and do the following:
- Rename to “soldier-blue”
- Set the X scale to -0.25

Then, under Sprite Renderer, click the little circle symbol and select the “soldier-blue” sprite.

In the prefab preview, you should see the blue soldier facing to the left.

Now make the outline for the player2 infantry. Go to OfflinePrefabs\UnitPrefabs, duplicate soldier-outline, rename it to solider-outline-player-2 and set its X scale to -0.25.

Then, attach soldier-outline-player-2 to the soldier-blue unit script.

One last thing to do is to make sure that the “base-marker-blue” object in the scene has the “PlayerBase” tag. Mine didn’t to start out so likely yours doesn’t either!

Now, make sure to save the scene, go back to the “TitleScreen” scene, and build and run to test it all out. Ahh! Everything is broken and the game crashes!
Some Quick Fixes to get back to testing…
If you look at the error message in the Unity Editor, it has to do with GameplayManager.cs not being able to reference some stuff. Open up GameplayManager.cs and make the following changes.
- Change the “Awake” function to “Start” function.
- In the Start function, do the following:
- Comment out the call to the “PutUnitsInUnitBox()” function
- Comment out the call to the “LimitUserPlacementByDistanceToBase()” function

Save GameplayManager.cs, rebuild and run, and when the game starts, you should see this…

So, basically, nothing! No units or anything, because, well, they haven’t been spawned by the server yet! We’ll get to that soon.
Registering Prefabs to be Spawned
In order for the unit and card prefabs to be spawned for the player, those prefabs will need to be “registered” as prefabs by the NetworkManager. If you go to the TitleScreen scene right now and look at the NetworkManager, you should see that the “LobbyPlayer” and “GamePlayer” prefabs are listed under “Registered Spawnable Prefabs.”

You could use the “+” sign and start adding all the prefabs manually, but there are a lot of prefabs that will need to be spawned by the server. That will take a while, and any time a new networked prefab is created, you’d have to remember to add it manually, which is likely to end in you forgetting to do that and then beating your head against your desk for an hour before you realize that’s why your game isn’t working as expected.
But, fortunately, prefabs can be registered as spawnable by the server programmatically! Yay! This will be done by having the NetworkManagerCC.cs script use Unity’s “Resources.LoadAll” functionality to load all the prefabs from a directory. This will require a “Resources” directory to be created in your project.

Next, drag the “Prefabs” directory into the Resources directory.

Now, open up NetworkManagerCC.cs in VisualStudio. Create a new function called public override void OnStartServer()
and add the following:
public override void OnStartServer()
{
spawnPrefabs = Resources.LoadAll<GameObject>("Prefabs").ToList();
}

This tells the server to load all the prefabs in the “Prefabs” directory of the Resources directory into the spawnPrefabs variable. The spawnPrefabs variable is used by the NetworkManager for the registered spawnable prefabs.
The next bit of code will be added to the override of OnStartClient. OnStartClient should already exist in NetworkManagerCC.cs, so just add the new code to it. All it did before this was have a Debug.log line. The code is shown below:
public override void OnStartClient()
{
Debug.Log("Starting client...");
List<GameObject> spawnablePrefabs = Resources.LoadAll<GameObject>("Prefabs").ToList();
Debug.Log("Spawnable Prefab count: " + spawnablePrefabs.Count());
foreach (GameObject prefab in spawnablePrefabs)
{
ClientScene.RegisterPrefab(prefab);
Debug.Log("Registering prefab: " + prefab);
}
}

Save NetworkManagerCC.cs, then build and run. When the Game starts, you should see the following under “Registered Spawnable Prefabs”

What to Do Next…
There are a LOT of things that need to be changed to get the Unit Placement phase working over multiplayer. The following list should be, at a high level, what will need to be updated for multiplayer:
- Spawn units for each player from the server
- Spawn cards for each player from the server
- Determine where each player can place units
- The server will determine where each player’s base is, and then calculate what tiles each player can place on
- The player’s will spawn the “cannot place here” markers locally, since those are just visual elements
- Update unit movement/placement so it is verified by the server
- Also want to make it so the player only sees where their units are moving. During unit placement, they shouldn’t see the opponent’s units at all
- Have a “Ready Up” system for advancing to the next phase. The phase won’t advance until all players are ready
- Advance to the next phase and sync where each player’s units were placed
So, lets get started spawning units!
Spawning Units for the Players
In the old game, the “units” never needed to be spawned because they already existed in the scene. This won’t work for the multiplayer version, as each player will need to have their own units. I guess I could just have a bunch of units in the scene and then assign them to players after they enter the game, but then assigning authority and all that becomes annoying (and it might be impossible to assign authority to already existing objects? idk). So, instead, I will have the server spawn them as the players connect.
The way I decided to go about doing this was to have GameplayManager.cs make calls to GamePlayer.cs. GamePlayer.cs running on a client would then make requests to the server to spawn the prefabs for the units/cards/whatever, and then tell the clients about the new units.
To start this process, I first want GameplayManager.cs to find the “LocalGamePlayer”, or the gameplayer object that the client has authority over. This will help in making sure that you are only making calls to the GamePlayer that you actually have authority over. If you don’t have authority over the gameplayer object, you won’t be able to make requests to the server from it.
First, in the Global variables section, I added the LocalGamePlayer and LocalGamePlayerScript variables that will be used to hold the local gameplayer gameobject and gameplayer.cs script, respectively.
[Header("GamePlayers")]
[SerializeField] private GameObject LocalGamePlayer;
[SerializeField] private GamePlayer LocalGamePlayerScript;

Then, I created a new function called GetLocalGamePlayer that finds the LocalGamePlayer object and assigns it and its script to the variables.
void GetLocalGamePlayer()
{
LocalGamePlayer = GameObject.Find("LocalGamePlayer");
LocalGamePlayerScript = LocalGamePlayer.GetComponent<GamePlayer>();
}

Then, call GetLocalGamePlayer() from the Start function.

Save GameplayManager.cs and then build and run the game. After you join the game from the lobby, you should see the LocalGamePlayer/Script variables populated.

Now, GameplayManager.cs should be ready to make function calls from the LocalGamePlayer object/script.
Preparing the GamePlayer.cs Script
So I will need to backtrack a bit as this will be useful later. Open up LobbyPlayer.cs and add a new playerNumber int variable that will be a syncvar.

Then, add the same to GamePlayer.cs

“playerNumber” will be used to track who is player 1, who is player 2, etc. This value will need to be assigned by the server in NetworkManagerCC.cs. So go to NetworkMAnagerCC.cs and under the OnServerAddPlayer function, add the following line of code:
lobbyPlayerInstance.playerNumber = LobbyPlayers.Count + 1;

Back in GamePlayer.cs add a new server function called SetPlayerNumber
[Server]
public void SetPlayerNumber(int playerNum)
{
this.playerNumber = playerNum;
}

Then, back in NetworkManagerCC.cs, add the following line of code to the ServerChangeScene function.
gamePlayerInstance.SetPlayerNumber(LobbyPlayers[i].playerNumber);

One more thing, the GamePlayer that is your local player will need to have the “LocalGamePlayer” tag added to it. In OnStartAuthority, add the following line of code:
gameObject.tag = "LocalGamePlayer";

Them, in the Unity Editor, create two tags for “GamePlayer” and “LocalGamePlayer.”

Assign (only) the GamePlayer tag to the GamePlayer prefab.

Ok, phew, that should be taken care of now!
Adding Unit Prefabs
In GamePlayer.cs, add new globabl variables that will be used to store the prefabs for the units and the unit holder.
[Header("Player Unit Prefabs")]
[SerializeField] GameObject PlayerUnitHolder;
[SerializeField] GameObject Player1Inf;
[SerializeField] GameObject Player1Tank;
[SerializeField] GameObject Player2Inf;
[SerializeField] GameObject Player2Tank;

Then, in the Unity Editor, open the GamePlayer prefab and add the correct prefabs to the variables. Remember, green soldier = player 1, blue soldier = player 2.

Updating Unit Scripts
After the units have been spawned, they will need to be able to easily be “tracked” and for it to be easy to determine which unit belongs to which player.
First, open UnitScript.cs, and then add the following so it can use Mirror and inherit from NetworkBehavior instead of MonoBehavior. This will allow UnitScript.cs to run Mirror functions.
using UnityEngine;
using Mirror;
public class UnitScript : NetworkBehaviour

Then, add the following to store information about which player “owns” the unit.
[Header("Player Owner info")]
[SyncVar] public string ownerPlayerName;
[SyncVar] public int ownerConnectionId;
[SyncVar] public int ownerPlayerNumber;

When the unit is spawned by the server, the server will set the owner playername, conenctionid, and playernumber to the appropriate GamePlayer that owns the unit.
Next, the PlayerUnitHolder prefab will need similar information added for it. Start by creating a new PlayerUnitHolder.cs script in your project.

Open PlayerUnitHolder.cs in VisualStudio, and add the same new variables and import mirror as you did for unitscript.cs.
using Mirror;
public class PlayerUnitHolder : NetworkBehaviour
{
[SyncVar] public string ownerPlayerName;
[SyncVar] public int ownerConnectionId;
[SyncVar] public int ownerPlayerNumber;

Save PlayerUnitHolder.cs, go back to the Unity Editor, and then attach the script to the PlayerUnitHolder prefab.

Time to Code the Unit Spawning…
The code to spawn the Units will be done in GamePlayer.cs. The first thing to add to GamePlayer.cs is a bool variable HaveSpawnedUnits to keep track of which gameplayers have already had their units spawned yet.
[Header("Player Statuses")]
[SyncVar] public bool HaveSpawnedUnits = false;

Then, three new functions will be created. For right now, they will be empty. Create them now just to have them their and to help understand the flow a bit. The functions are the following:
- A public function called SpawnPlayerUnits
- A command called CmdSpawnPlayerUnits
- A ClientRpc called RpcShowSpawnedPlayerUnits
public void SpawnPlayerUnits()
{
}
[Command]
public void CmdSpawnPlayerUnits()
{
}
[ClientRpc]
void RpcShowSpawnedPlayerUnits()
{
}

SpawnPlayerUnits()
The SpawnPlayerUnits() units function will be pretty simple. All it will do is check if the bool variable HaveSpawnUnits is false. If it is false, it will then call CmdSpawnPlayerUnits() on the server.
public void SpawnPlayerUnits()
{
if (!this.HaveSpawnedUnits)
{
Debug.Log("SpawnPlayerUnits() for: " + this.PlayerName + " with player number: " + this.playerNumber);
CmdSpawnPlayerUnits();
}
}

CmdSpawnPlayerUnits()
The CmdSpawnPlayerUnits function will be a bit more involved. CmdSpawnPlayerUnits is a Command function, meaning that a client requests this function, and then a server runs the function. The code will only be executed on the server.
The first thing CmdSpawnPlayerUnits will do is find the GamePlayer script of the client that made the request to CmdSpawnPlayerUnits. This is done using the “connectionToClient” function of mirror by first getting the requesting client’s networkidentity, and then using that network identity to get their GamePlayer script.
public void CmdSpawnPlayerUnits()
{
Debug.Log("Running CmdSpawnPlayerUnits on the server.");
NetworkIdentity networkIdentity = connectionToClient.identity;
GamePlayer requestingPlayer = networkIdentity.GetComponent<GamePlayer>();
if (requestingPlayer.playerNumber == 1 && !requestingPlayer.HaveSpawnedUnits)

Next, the requesting player’s playernumber will be checked to see what units to spawn for them. The code to spawn all the units for player 1 is shown below.
if (requestingPlayer.playerNumber == 1 && !requestingPlayer.HaveSpawnedUnits)
{
//Instantiate the unit holder
GameObject playerUnitHolder = Instantiate(PlayerUnitHolder, transform.position, Quaternion.identity);
//Get the unit holder's script to set the owner variables
PlayerUnitHolder script = playerUnitHolder.GetComponent<PlayerUnitHolder>();
script.ownerPlayerName = requestingPlayer.PlayerName;
script.ownerConnectionId = requestingPlayer.ConnectionId;
script.ownerPlayerNumber = requestingPlayer.playerNumber;
//Spawn the unity holder on the network and assign owner/authority to the requesting client
NetworkServer.Spawn(playerUnitHolder, connectionToClient);
//Spawn the player1 infantry units
for (int i = 0; i < 6; i++)
{
GameObject playerInfantry = Instantiate(Player1Inf, transform.position, Quaternion.identity);
UnitScript unitScript = playerInfantry.GetComponent<UnitScript>();
unitScript.ownerPlayerName = requestingPlayer.PlayerName;
unitScript.ownerConnectionId = requestingPlayer.ConnectionId;
unitScript.ownerPlayerNumber = requestingPlayer.playerNumber;
NetworkServer.Spawn(playerInfantry, connectionToClient);
}
//Spawn player1 tanks
for (int i = 0; i < 4; i++)
{
GameObject playerTank = Instantiate(Player1Tank, transform.position, Quaternion.identity);
UnitScript unitScript = playerTank.GetComponent<UnitScript>();
unitScript.ownerPlayerName = requestingPlayer.PlayerName;
unitScript.ownerConnectionId = requestingPlayer.ConnectionId;
unitScript.ownerPlayerNumber = requestingPlayer.playerNumber;
NetworkServer.Spawn(playerTank, connectionToClient);
}
requestingPlayer.HaveSpawnedUnits = true;
//Tell all clients to "show" the PlayerUnitHolder - set the correct parent to all the unity holders and run GameplayManager's PutUnitsInUnitBox
RpcShowSpawnedPlayerUnits(playerUnitHolder);
Debug.Log("Spawned Player1UnitHolder.");
}

The code above does the following:
- Instantiates the PlayerUnitHolder object
- Gets the PlayerUnitHolder script
- Assigns the owner variables to the PlayerUnitHolder object
- Uses NetworkServer.Spawn to “spawn” the PlayerUnitHolder object on the server, and then uses connectionToClient to make sure the requesting player has authority over the PlayerUnitHolder
- The Units are then spawned. First, the infantry units
- The for loop makes sure that 6 infantry are spawned
- The owner variables are assigned for each infatry
- NetworkServer.Spawn spawns the infantry object and gives the requesting player authority
- Tanks are spawned, same as the infantry, but only 4 of them
- The requesting player’s HaveSpawnedUnits variable is set to true
- RpcShowSpawnedPlayerUnits is called to inform the clients that units/unitholder has been spawned for a player. The PlayerUnitHolder is passed as an argument.
The reason that each unit and the unit holder are spawned individually instead of as just one prefab with a bunch of children, is because when you do it as one object with children, the player doesn’t have “authority” over the child objects. This was very annoying for me, so spawning them individually allows for the player to have authority over each infantry, tank, and the unit holder. Then, later, RpcShowSpawnedPlayerUnits will be used to make the units children of the unit holder for organization purposes.
The code for player 2 is the same, just substituting the corresponding prefabs for player 2. All the code is shown below for copy/paste:
[Command]
public void CmdSpawnPlayerUnits()
{
Debug.Log("Running CmdSpawnPlayerUnits on the server.");
NetworkIdentity networkIdentity = connectionToClient.identity;
GamePlayer requestingPlayer = networkIdentity.GetComponent<GamePlayer>();
if (requestingPlayer.playerNumber == 1 && !requestingPlayer.HaveSpawnedUnits)
{
//Instantiate the unit holder
GameObject playerUnitHolder = Instantiate(PlayerUnitHolder, transform.position, Quaternion.identity);
//Get the unit holder's script to set the owner variables
PlayerUnitHolder script = playerUnitHolder.GetComponent<PlayerUnitHolder>();
script.ownerPlayerName = requestingPlayer.PlayerName;
script.ownerConnectionId = requestingPlayer.ConnectionId;
script.ownerPlayerNumber = requestingPlayer.playerNumber;
//Spawn the unity holder on the network and assign owner/authority to the requesting client
NetworkServer.Spawn(playerUnitHolder, connectionToClient);
//Spawn the player1 infantry units
for (int i = 0; i < 6; i++)
{
GameObject playerInfantry = Instantiate(Player1Inf, transform.position, Quaternion.identity);
UnitScript unitScript = playerInfantry.GetComponent<UnitScript>();
unitScript.ownerPlayerName = requestingPlayer.PlayerName;
unitScript.ownerConnectionId = requestingPlayer.ConnectionId;
unitScript.ownerPlayerNumber = requestingPlayer.playerNumber;
NetworkServer.Spawn(playerInfantry, connectionToClient);
}
//Spawn player1 tanks
for (int i = 0; i < 4; i++)
{
GameObject playerTank = Instantiate(Player1Tank, transform.position, Quaternion.identity);
UnitScript unitScript = playerTank.GetComponent<UnitScript>();
unitScript.ownerPlayerName = requestingPlayer.PlayerName;
unitScript.ownerConnectionId = requestingPlayer.ConnectionId;
unitScript.ownerPlayerNumber = requestingPlayer.playerNumber;
NetworkServer.Spawn(playerTank, connectionToClient);
}
requestingPlayer.HaveSpawnedUnits = true;
//Tell all clients to "show" the PlayerUnitHolder - set the correct parent to all the unity holders and run GameplayManager's PutUnitsInUnitBox
RpcShowSpawnedPlayerUnits(playerUnitHolder);
Debug.Log("Spawned Player1UnitHolder.");
}
else if (requestingPlayer.playerNumber == 2 && !requestingPlayer.HaveSpawnedUnits)
{
GameObject playerUnitHolder = Instantiate(PlayerUnitHolder, transform.position, Quaternion.identity);
PlayerUnitHolder script = playerUnitHolder.GetComponent<PlayerUnitHolder>();
script.ownerPlayerName = requestingPlayer.PlayerName;
script.ownerConnectionId = requestingPlayer.ConnectionId;
script.ownerPlayerNumber = requestingPlayer.playerNumber;
NetworkServer.Spawn(playerUnitHolder, connectionToClient);
for (int i = 0; i < 6; i++)
{
GameObject playerInfantry = Instantiate(Player2Inf, transform.position, Quaternion.identity);
UnitScript unitScript = playerInfantry.GetComponent<UnitScript>();
unitScript.ownerPlayerName = requestingPlayer.PlayerName;
unitScript.ownerConnectionId = requestingPlayer.ConnectionId;
unitScript.ownerPlayerNumber = requestingPlayer.playerNumber;
NetworkServer.Spawn(playerInfantry, connectionToClient);
}
//Spawn player1 tanks
for (int i = 0; i < 4; i++)
{
GameObject playerTank = Instantiate(Player2Tank, transform.position, Quaternion.identity);
UnitScript unitScript = playerTank.GetComponent<UnitScript>();
unitScript.ownerPlayerName = requestingPlayer.PlayerName;
unitScript.ownerConnectionId = requestingPlayer.ConnectionId;
unitScript.ownerPlayerNumber = requestingPlayer.playerNumber;
NetworkServer.Spawn(playerTank, connectionToClient);
}
requestingPlayer.HaveSpawnedUnits = true;
RpcShowSpawnedPlayerUnits(playerUnitHolder);
Debug.Log("Spawned Player2UnitHolder.");
}
else
{
Debug.Log("NO PlayerUnitHolder spawned.");
}
}
RpcShowSpawnedPlayerUnits
You probably noticed in the screenshot above that there is a red squiggly line under the calls to RpcShowSpawnedPlayerUnits. That’s because I included a gameobject, the spawned unitholder, as an argument to RpcShowSpawnedPlayerUnits. So, first, add the argument to the function.
void RpcShowSpawnedPlayerUnits(GameObject playerUnitHolder)

So RpcShowSpawnedPlayerUnits will take the playerUnitHolder object that the server just created using CmdSpawnPlayerUnits. It will then make some checks against that playerUnitHolder object to determine who owns it and take actions based on the owner.
First, though, you will need to add a new global GameObject variable called myUnitHolder. This will be assigned to in RpcShowSpawnedPlayerUnits.
[Header("Player Base/Units")]
public GameObject myUnitHolder;
The first section of code for RpcShowSpawnedPlayerUnits will check if the UnitHolder is owned by you or not.
GameObject[] infantryUnits = GameObject.FindGameObjectsWithTag("infantry");
GameObject[] tankUnits = GameObject.FindGameObjectsWithTag("tank");
if (playerUnitHolder.GetComponent<NetworkIdentity>().hasAuthority && hasAuthority)
{
Debug.Log("You: " + this.PlayerName + " have authority over: " + playerUnitHolder);
playerUnitHolder.SetActive(true);
playerUnitHolder.transform.SetParent(gameObject.transform);
PlayerUnitHolder unitHolderScript = playerUnitHolder.GetComponent<PlayerUnitHolder>();
myUnitHolder = playerUnitHolder;
foreach (GameObject inf in infantryUnits)
{
UnitScript infScript = inf.GetComponent<UnitScript>();
if (unitHolderScript.ownerConnectionId == infScript.ownerConnectionId)
{
inf.transform.SetParent(playerUnitHolder.transform);
}
}
foreach (GameObject tank in tankUnits)
{
UnitScript tankScript = tank.GetComponent<UnitScript>();
if (unitHolderScript.ownerConnectionId == tankScript.ownerConnectionId)
{
tank.transform.SetParent(playerUnitHolder.transform);
}
}
//GameplayManager.instance.PutUnitsInUnitBox();
}
else
{
Debug.Log("You: " + this.PlayerName + " DO NOT have authority over: " + playerUnitHolder);

The code above does the following
- Finds all infantry units by tag and stores them in infantryUnits
- Finds all tank units by tag and stores them in tankUnits
- if statement that checks if you have authority over the returned playerUnitHolder and if you have authority over the GamePlayer object running this code
- .hasAuthority on a networkidentity should return true or false on whether you have authority on that object (I think so, at least)
- this check makes it so you only run the next bit of code if it is your gameplayer object, aka LocalGamePlayer
- If the if statement returns true, do the following
- make sure that the playerUnitHolder is set to active in the hierarchy
- sets the unitPlayerHolder as a child of your gameplayer object
- Gets the PlayerUnitHolder script of playerUnitHolder and stores it in unitHolderScript
- stores the returned unitPlayerHolder in myUnitPlayerHolder
- Iterates through every infantry in infantryUnits
- gets the UnitScript of the infantry
- checks if the ownerConnectionId of the infantry and playerUnitHolder match
- if they match, set the infantry as a child of the playerUnitHolder
- Iterates through each tank object in tankUnits
- gets the UnitScript of the infantry
- checks if the ownerConnectionId of the infantry and playerUnitHolder match
- if they match, set the infantry as a child of the playerUnitHolder
- Finally there is a commented out call to GameplayManager’s PutUnitsInUnitBox, which will put your units in the correct spot for unit placement. PutUnitsInUnitBox needs to be updated before it will work, though, so for now it is commented out
The next section of code in RpcShowSpawnedPlayerUnits will be used to look for all other UnitPlayerHolder objects in the game and set them as childern of the proper GamePlayer. The code is shown below:
GameObject[] PlayerUnitHolders = GameObject.FindGameObjectsWithTag("PlayerUnitHolder");
foreach (GameObject unitHolder in PlayerUnitHolders)
{
PlayerUnitHolder unitHolderScript = unitHolder.GetComponent<PlayerUnitHolder>();
GameObject[] gamePlayers = GameObject.FindGameObjectsWithTag("GamePlayer");
foreach (GameObject gamePlayer in gamePlayers)
{
GamePlayer gamePlayerScript = gamePlayer.GetComponent<GamePlayer>();
if (gamePlayerScript.ConnectionId == unitHolderScript.ownerConnectionId)
{
foreach (GameObject inf in infantryUnits)
{
UnitScript infScript = inf.GetComponent<UnitScript>();
if (gamePlayerScript.ConnectionId == unitHolderScript.ownerConnectionId && unitHolderScript.ownerConnectionId == infScript.ownerConnectionId)
{
inf.transform.SetParent(unitHolder.transform);
inf.transform.position = new Vector3(-1000, -1000, 0);
}
}
foreach (GameObject tank in tankUnits)
{
UnitScript tankScript = tank.GetComponent<UnitScript>();
if (gamePlayerScript.ConnectionId == unitHolderScript.ownerConnectionId && unitHolderScript.ownerConnectionId == tankScript.ownerConnectionId)
{
tank.transform.SetParent(unitHolder.transform);
tank.transform.position = new Vector3(-1000, -1000, 0);
}
}
unitHolder.transform.SetParent(gamePlayer.transform);
gamePlayerScript.myUnitHolder = unitHolder;
}
}
}

This code does the following:
- Gets all gameobjects with the “PlayerUnitHolder” tag and stores them in PlayerUnitHolders
- Iterates through every unitHolder in PlayerUnitHolder
- Gets the PlayerUnitHolder script for unitHolder
- Gets all gameobjects with the tag “GamePlayer” and stores in gamePlayers
- This should be every GamePlayer object that isn’t yours. Remember, your gameplayer object is tagged LocalGamePlayer
- Iterates through every gamePlayer in gamePlayers
- Gets the GamePlayer script for gamePlayer
- Checks if the gamePlayer connectionId matches the unitHolder ownerConnectionId (does this gameplayer own this unitholder?)
- If they match:
- Iterate through every infantry unit. If the infantry, gameplayer, and unitHolder all have the same ownerConnectionid, set the infantry to a child of the unitHolder
- Iterate through every tank unit. If the tank, gameplayer, and unitHolder all have the same ownerConnectionid, set the tank to a child of the unitHolder
- For both the infantry and tank units, set their transform.position to (-1000,-1000,0). This is just a stupid hack to make sure the units aren’t visibile on the screen
- set the unitHolder as a child object of the gameplayer
- Sets the gamePlayer’s myUnitHolder to the unitHolder
So once all of that is done, on each client the unitholders and units will all be spawned. The authority for the unit holders and the units will be assigned by the server. The clients then organize the unit holders and units so that they are children of the correct owner.
All that’s left is to actually call the SpawnPlayerUnits function to get the whole spawning units ball rolling. Go to GameplayManager.cs and create a new function called SpawnPlayerUnits.
void SpawnPlayerUnits()
{
Debug.Log("Spawn units for: " + LocalGamePlayerScript.PlayerName);
LocalGamePlayerScript.SpawnPlayerUnits();
}

SpawnPlayerUnits will use the LocalGamePlayerScript to call SpawnPlayerUnits from GamePlayer.cs on your local gameplayer object.
Then, in GameplayMAnager.cs’s Start function, make the call to SpawnPlayerUnits.
Save everything and build and run. When the game starts out of the lobby, you should see everything working in the hierarchy

In the middle of the screen you should only see your own units. Soon we will move them to the correct space, but for now they are just plopped down there in the middle. The other player’s units should be offscreen.
Now that spawning the unit holders and units appears to be working, onto the next thing…
Spawning Cards for the Players
Spawning cards for the players will follow the same basic logic of spawning units did. A PlayerCardHand will be spawned, then the five player cards are spawn, the correct player will be given authority over those cards, and then the clients will arrange them as children int he hierachy as the correct player.
In GamePlayer.cs, some new globaly variables will be declared. There will be the prefabs for the player card hand and the cards, as well a bool variable to track when/if that player has had their cards spawned. There will also be a new variable called myPlayerCardHand used to store the player’s hand gameobject.
[Header("Player Card Prefabs")]
[SerializeField] GameObject PlayerCardHand;
[SerializeField] GameObject[] Cards;
[Header("Player Base/Units")]
public GameObject myUnitHolder;
public GameObject myPlayerCardHand;
[Header("Player Statuses")]
[SyncVar] public bool HaveSpawnedUnits = false;
[SyncVar] public bool HaveSpawnedCards = false;

Open the GamePlayer prefab in the Unity editor. Attach the PlayerCardGand prefab to its variable. For the list of “Cards,” expand the list and manually set the size to “5.” This will create 5 elements for you to attach gameobjects. Attach the 5 card prefabs to the list.

Next, you can create the empty functions that will be used in GamePlayer to spawn the cards. They are:
- a public SpawnPlayerCards
- a Command CmdSpawnPlayerCards
- A ClientRpc RpcSpawnPlayerCards

SpawnPlayerCards()
The SpawnPlayerCards() function will be simple. It checks if HaveSpawnedCards is false, and if it is false, makes the request to the server to run CmdSpawnPlayerCards.
public void SpawnPlayerCards()
{
if (!this.HaveSpawnedCards)
{
Debug.Log("SpawnPlayerUnits() for: " + this.PlayerName + " with player number: " + this.playerNumber);
CmdSpawnPlayerCards();
}
}

CmdSpawnPlayerCards()
The CmdSpawnPlayerCards function will spawn the PlayerCardHand object and the cards that will be in the hand. Before that can be done, though, some changes need to be made to the PlayerHand.cs and the Card.cs scripts.
Updates to PlayerHand.cs
Open PlayerHand.cs and add the following to the top of the script to use Mirror, inherit NetworkBehavior, and create the owner information variables, all similar to what was done with the PlayerUnitHolder and UnitScript scripts.
using Mirror;
public class PlayerHand : NetworkBehaviour
{
[SyncVar] public string ownerPlayerName;
[SyncVar] public int ownerConnectionId;
[SyncVar] public int ownerPlayerNumber;

There will be a few more global variables added as well to PlayerHand.cs, shown below.
[SyncVar] public bool isHandInitialized = false;
public List<GameObject> Hand = new List<GameObject>();
public List<GameObject> DiscardPile = new List<GameObject>();
public SyncList<uint> HandNetId = new SyncList<uint>();
public SyncList<uint> DiscardPileNetId = new SyncList<uint>();
public bool isPlayerViewingTheirHand = false;

The new variables here are:
- isHandInitialized to keep track of if the PlayerHand has been “initialized” – to be used in later code
- Two new SnycLists called HandNetId and DiscardPileId
- As cards are added to the “hand” or “discard pile,” the network identity will be stored in a SyncList. This allows for the server and other clients to all know what specific cards are in each player’s hand/discard
Next, change the “Awake” function to “Start,” and then remove everything from the Start function (it should be empty. No “MakeInstance()” call or anything like that). You can also remove the MakeInstance function all together.

For now, leave PlayerHand.cs like this. The code to “initialize” the PlayerHand will be covered later.
After removing the “instance” of PlayerHand, you will get a bunch of errors in GameplayManager about the instance not having a definition. The lines e PlayerHand.instance.ShowPlayerHandOnScreen();
in ShowPlayerHandPressed and PlayerHand.instance.HidePlayerHandOnScreen();
in HidePlayerHandPressed can be commented out for now. For if (hidePlayerHandButton.activeInHierarchy && !PlayerHand.instance.isPlayerViewingTheirHand)
in ActivateUnitMovementUI, just change it to if (hidePlayerHandButton.activeInHierarchy)
by removing the PlayerHand reference.
You will also get errors in EscMenuManager.cs in the Update function. You can remove the reference to PlayerHand.instance in the first if statement, and then just comment out the else if statement that follows.

Updates to Card.cs
The updates to Card.cs are pretty simple. You just need to import Mirror, inherit NetworkBehaviour, and then add the syncvars for the player owner info.
using Mirror;
public class Card : NetworkBehaviour
{
[SyncVar] public string ownerPlayerName;
[SyncVar] public int ownerConnectionId;
[SyncVar] public int ownerPlayerNumber;

Back to CmdSpawnPlayerCards()…
With the updates to PlayerHand.cs and Card.cs made, the code for CmdSpawnPlayerCards can be added. The code is shown below.
[Command]
void CmdSpawnPlayerCards()
{
Debug.Log("Running CmdSpawnPlayerCards on the server.");
NetworkIdentity networkIdentity = connectionToClient.identity;
GamePlayer requestingPlayer = networkIdentity.GetComponent<GamePlayer>();
if (!requestingPlayer.HaveSpawnedCards)
{
//Instantiate the unit holder
GameObject playerHand = Instantiate(PlayerCardHand, transform.position, Quaternion.identity);
//Get the unit holder's script to set the owner variables
PlayerHand playerHandScript = playerHand.GetComponent<PlayerHand>();
playerHandScript.ownerPlayerName = requestingPlayer.PlayerName;
playerHandScript.ownerConnectionId = requestingPlayer.ConnectionId;
playerHandScript.ownerPlayerNumber = requestingPlayer.playerNumber;
//Spawn the unity holder on the network and assign owner/authority to the requesting client
NetworkServer.Spawn(playerHand, connectionToClient);
foreach (GameObject card in Cards)
{
GameObject playerCard = Instantiate(card, transform.position, Quaternion.identity);
Card playerCardScript = playerCard.GetComponent<Card>();
playerCardScript.ownerPlayerName = requestingPlayer.PlayerName;
playerCardScript.ownerConnectionId = requestingPlayer.ConnectionId;
playerCardScript.ownerPlayerNumber = requestingPlayer.playerNumber;
playerCard.transform.position = new Vector3(-1000, -1000, 0);
NetworkServer.Spawn(playerCard, connectionToClient);
}
requestingPlayer.HaveSpawnedCards = true;
//Tell all clients to "show" the PlayerUnitHolder - set the correct parent to all the unity holders and run GameplayManager's PutUnitsInUnitBox
RpcSpawnPlayerCards();
Debug.Log("Spawned PlayerHand and Player Cards.");
}
else
{
Debug.Log("NO PlayerHand or Player cards spawned.");
}
}

The code for CmdSpawnPlayerCards does the following:
- Gets the networkidentity of the player that requested CmdSpawnPlayerCards
- Gets the GamePlayer script of the requesting player
- Checks if the requesting player has HaveSpawnCards set to false. If it is false:
- Instantiate the PlayerCardHand prefab
- Get the PlayerCardHand’s PlayerHand script
- Set the player owner information for PlayerCardHand
- NetworkServer.Spawn is then used to spawn the PlayerHand and sets the requesting player as having authority
- Iterates through every Card in the Cards list of prefabs
- instantiates the card
- sets the owner information of the card
- sets the transform.position of each card to -1000,-1000,0 to make sure they are off the player’s screen
- NetworkServer.Spawn spawns the card and sets the authority of the card to the requesting player
- Sets the requesting player’s HaveSpawnedCards to true
- Calls RpcSpawnPlayerCards
RpcSpawnPlayerCards()
RpcSpawnPlayerCards will do the same as what RpcShowSpawnedPlayerUnits did for the units: After the server spawns a PlayerCardHand and the player’s Cards, it will find all the PlayerCardHands and all Cards and correlate them to the correct player owner.
The code for RpcSpawnPlayerCards is shown below:
[ClientRpc]
void RpcSpawnPlayerCards()
{
GameObject[] allPlayerCardHands = GameObject.FindGameObjectsWithTag("PlayerHand");
GameObject[] allCards = GameObject.FindGameObjectsWithTag("Card");
GameObject LocalGamePlayer = GameObject.Find("LocalGamePlayer");
GamePlayer LocalGamePlayerScript = LocalGamePlayer.GetComponent<GamePlayer>();
foreach (GameObject playerCardHand in allPlayerCardHands)
{
PlayerHand playerCardHandScript = playerCardHand.GetComponent<PlayerHand>();
if (playerCardHandScript.ownerConnectionId == LocalGamePlayerScript.ConnectionId)
{
playerCardHand.transform.SetParent(LocalGamePlayer.transform);
foreach (GameObject card in allCards)
{
Card cardScript = card.GetComponent<Card>();
if (cardScript.ownerConnectionId == LocalGamePlayerScript.ConnectionId)
{
card.transform.SetParent(playerCardHand.transform);
}
}
myPlayerCardHand = playerCardHand;
}
else
{
GameObject[] allGamePlayers = GameObject.FindGameObjectsWithTag("GamePlayer");
foreach (GameObject gamePlayer in allGamePlayers)
{
GamePlayer gamePlayerScript = gamePlayer.GetComponent<GamePlayer>();
if (playerCardHandScript.ownerConnectionId == gamePlayerScript.ConnectionId)
{
playerCardHand.transform.SetParent(gamePlayerScript.transform);
foreach (GameObject card in allCards)
{
Card cardScript = card.GetComponent<Card>();
if (cardScript.ownerConnectionId == gamePlayerScript.ConnectionId)
{
card.transform.SetParent(playerCardHand.transform);
}
}
gamePlayerScript.myPlayerCardHand = playerCardHand;
}
}
}
}
}

The code does the following:
- Finds all PlayerCardHands by the “PlayerHand” tag and stores it in allPlayerCardHands
- Finds all player cards by the “Card” tag and stores it in allCards
- Finds the LocalGamePlayer object by its name
- Gets the LocalGamePlayer object’s GamePlayer script and stores it in LocalGamePlayerScript
- Iterates through each playerCardHand in allPlayerCardHands
- Gets the PlayerHand script for playerCardHand
- checks if the playerCardHand ownerConnectionId matches the localGamePlayer’s connection ID. If it does:
- Sets the playerCardHand as a child of LocalGamePlayer
- Iterates through each card in allCards. If the card’s ownerConnectionId matches the LocalGamePlayer’s connectionId, make the card a child of playerCardHand
- Set themyPlayerCardHand to the playerCardHand
- If the playerCardHand’s connection ID DOES NOT match the LocalGamePlayer’s connectionId:
- Find All gameplayer objects
- check if the playerCardHand ownerConnection ID matches any of the GamePlayer connection Ids. Correlate the playerCardHand and Cards to any match GamePlayers
Calling SpawnPlayerCards
In GameplayManager.cs, create a new function called SpawnPlayerCards that calls SpawnPlayerCards from the LocalGamePlayerScript:
void SpawnPlayerCards()
{
Debug.Log("Spawn cards for: " + LocalGamePlayerScript.PlayerName);
LocalGamePlayerScript.SpawnPlayerCards();
}
Then call SpawnPlayerCards in GameplayManager.cs’s Start function.

Updating the Card and PlayerCardHand Prefabs
Back in the Unity editor you will need to update the PlayerCardHand prefab. Open the prefab, and make sure to add the PlayerHand script to it.

Next, create a new tag called “Card”

Then, add the Card tag to all the Card prefabs that were attached to GamePlayer to spawn.
Save everything. Build and run the game, and when the game launches from the lobby, you should see the PlayerCardHand and Cards spawned under the correct players.

Finding Each Player’s Base and Restricting Unit Placement
In the singleplayer version of the game, there was only one “real” player base to worry about and one player to restrict the movement of. For this, though, there will be multiplayers, and restrictions for multiple players. This makes things somewhat more complicated, but we can get through it!
To get things started off in GamePlayer.cs, add two new variables called myPlayerBase and GotPlayerBase
[Header("Player Base/Units")]
public GameObject myUnitHolder;
public GameObject myPlayerCardHand;
[SyncVar] public GameObject myPlayerBase;
[Header("Player Statuses")]
[SyncVar] public bool HaveSpawnedUnits = false;
[SyncVar] public bool HaveSpawnedCards = false;
[SyncVar] public bool GotPlayerBase = false;

myPlayerBase will store the gameobject of the actual base. This is a SyncVar, and you should be wary about making GameObject variables SyncVars. In my limited experience, SyncVars and GameObjects don’t mix well. Because the player bases will already “exist” when the server and client start the scene, this appears to work. Otherwise, don’t put a SyncVar on a GameObject variable.
The other variable, GotPlayerBase, is a boolean used to track when the player has had their base spawned.
Preparing the Player Bases
To be able to track which Player Base is which player’s, you will need to create a new script called “PlayerBaseScript.cs.”

PlayerBaseScript will be pretty simple. Here’s the entire script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Mirror;
public class PlayerBaseScript : NetworkBehaviour
{
[SyncVar] public string ownerPlayerName;
[SyncVar] public int ownerConnectionId;
[SyncVar] public int ownerPlayerNumber;
}

Really all the script does is allow for the owner info variables to exist and be referenced.
Open each player base prefab and add the PlayerBaseScript.

The PlayerBaseScript should be added to the base-marker-green and base-marker-blue prefabs. One thing you will need to do for the player bases differently than the units/cards is to manually assign the value for “ownerPlayerNumber.” The server will “assign” the palyer bases by checking to see what the player’s player number is, and then finding the base that belongs to that player number. base-marker-green will be for playerNumber 1, and base-marker-blue will be for player number 2.

After that save the scene and let’s get back to coding…
Finding the Player Base on the Server
GamePlayer.cs will be used to find the player’s base and assign it to the correct player. First, create some code to call the Command:
public void GetPlayerBase()
{
if (!GotPlayerBase)
{
Debug.Log("Calling CmdSetPlayerBase from: " + this.PlayerName);
CmdSetPlayerBase();
}
}
[Command]
void CmdSetPlayerBase()
{
}

GetPlayerBase simply checks if the GotPlayerBase bool is false. If it is, it requests CmdSetPlayerBase on the server.
In addition to CmdSetPlayerBase, there will be two more functions that will be created. A server function called CanPlayerPlaceOnLand that takes a GamePlayer and the playerBase as arguments, and then also a ClientRpc called RpcGetPlayerBase

CmdSetPlayerBase()
CmdSetPlayerBase will find all the player bases and check which player should be assigned to which base. The code is shown below.
[Command]
void CmdSetPlayerBase()
{
NetworkIdentity networkIdentity = connectionToClient.identity;
GamePlayer requestingPlayer = networkIdentity.GetComponent<GamePlayer>();
Debug.Log("Running CmdSetPlayerBase on the server for: " + requestingPlayer.PlayerName);
GameObject[] playerBases = GameObject.FindGameObjectsWithTag("PlayerBase");
foreach (GameObject playerBase in playerBases)
{
PlayerBaseScript playerBaseScript = playerBase.GetComponent<PlayerBaseScript>();
if (requestingPlayer.playerNumber == playerBaseScript.ownerPlayerNumber)
{
playerBaseScript.ownerPlayerName = requestingPlayer.PlayerName;
playerBaseScript.ownerConnectionId = requestingPlayer.ConnectionId;
CanPlayerPlaceOnLand(requestingPlayer, playerBase);
RpcGetPlayerBase();
break;
}
}
}

CmdSetPlayerBase does the following:
- Gets the requesting player GamePlayer script
- Gets all playerBases based on the PlayerBase tag
- iterates through all the playerBases
- Checks if the playerNumber of the playerBase matches the requesting player’s player number. If they match:
- Set the owner information for the playerBase
- Call CanPlayerPlaceOnLand, providing the requesting player and their playerBase as arguments
- set the requesting player’s GotPlayerBase to true
- set the requesting player’s myPlayerBase to the playerBase
- break out of the foreach loop
- Checks if the playerNumber of the playerBase matches the requesting player’s player number. If they match:
CanPlayerPlaceOnLand
CanPlayerPlaceOnLand is a “Server” function, which means that it can only be run on the server. Clients can not run this function locally. CanPlayerPlaceOnLand will calculate the distance of each land object from the provided playerBase, and determine if the provided player can place units on the land object. The code is shown below.
[Server]
void CanPlayerPlaceOnLand(GamePlayer gamePlayer, GameObject playerBase)
{
Debug.Log("Server: Running CanPlayerPlaceOnLand for: " + gamePlayer.PlayerName + " using this base: " + playerBase);
Vector3 playerBaseLocation = playerBase.transform.position;
GameObject LandTileHolder = GameObject.FindGameObjectWithTag("LandHolder");
foreach (Transform landObject in LandTileHolder.transform)
{
LandScript landScript = landObject.gameObject.GetComponent<LandScript>();
float disFromBase = Vector3.Distance(landObject.transform.position, playerBaseLocation);
Debug.Log(landScript.gameObject.name + "'s distance from player base: " + disFromBase.ToString());
if (disFromBase <= 6.0f)
{
landScript.PlayerCanPlaceHere = gamePlayer.playerNumber;
}
}
}

The code does the following:
- Gets the Vector3 coordinates of the playerBase from its transform.position.
- Finds the “LandTileHolder” object based on the tag “LandHolder”
- Iterates through each child land object of LandTileHolder
- Gets the landscript of the land object
- Uses Vector3.distance to determine the distance from the land object’s transform.position and the location of the player base
- If the distance is less than or equal to 6:
- Add the requesting player’s player number to the land object’s “PlayerCanPlaceHere” variable
One thing you might notice from the above is that currently your land objects don’t have a PlayerCanPlaceHere variable. Time for some updates!
Updating LandScript.cs
To support what CanPlayerPlaceOnLand did on the server, some changes are going to be made to the LandScript.cs script. The first will be to import Mirror and inherit NetworkBehaviour
using Mirror;
public class LandScript : NetworkBehaviour
The next is to add a new SyncVar variable called PlayerCanPlaceHere that will store what players can place on the land tile during Unit Placement.
[SyncVar(hook = nameof(HandlePlayerCanPlaceHereUpdate))] public int PlayerCanPlaceHere;

You’ll notice that PlayerCanPlaceHere is a SyncVar with a hook, meaning that when the value changes, the function in the hook will execute. That function is HandlePlayerCanPlaceHereUpdate, and the code is shown below!
public void HandlePlayerCanPlaceHereUpdate(int oldValue, int newValue)
{
Debug.Log("PlayerCanPlaceHere updated to: " + PlayerCanPlaceHere);
GameObject LocalGamePlayer = GameObject.Find("LocalGamePlayer");
GamePlayer LocalGamePlayerScript = LocalGamePlayer.GetComponent<GamePlayer>();
if (PlayerCanPlaceHere == LocalGamePlayerScript.playerNumber)
{
cannotPlaceHere = false;
RemoveCannotPlaceHereOutline();
}
else
{
cannotPlaceHere = true;
CreateCannotPlaceHereOutline();
}
}

The code does the following:
- Finds the localGamePlayer and gets the LocalGamePlayer’s GamePlayer script
- Checks if the new value for PlayerCanPlaceHere is equal to the LocalGamePlayer’s palyer number. If they match:
- set “cannotPlaceHere” to false
- Call RemoveCannotPlaceHereOutline
- If they don’t match:
- set CannotPlaceHere to true
- run CreateCannotPlaceHereOutline
So what this means is, if the player can place on this land tile, remove the CannotPlaceHereMarker. If they cannot, make sure that a CannotPlaceHere marker exists. In other words, the game is removing an already existing marker when the player can place there. The default state is that the player can’t place there.
In order for this to work correctly, scroll up to the top of LandScript.cs and change the Awake function to Start. Then, add the following code:
if (PlayerCanPlaceHere == 0)
{
cannotPlaceHere = true;
CreateCannotPlaceHereOutline();
}

This sets up so the default state of a land tile is that a player cannot place there and the marker is spawned. This is because HandlePlayerCanPlaceHereUpdate only places/removes the marker when the value of PlayerCanPlaceHere changes, but for a few tiles on the map, no player can place there so PlayerCanPlaceHere never changes. The above code in the Start function makes sure that there is a marker there by default, and that cannotPlaceHere is set to true by default.
RpcGetPlayerBase()
RpcGetPlayerBase() is a simple function that really only serves the purpose of setting the myPlayerBase and GotPlayerBase variables. The code is shown below:
[ClientRpc]
void RpcGetPlayerBase()
{
if (!this.GotPlayerBase)
{
Debug.Log("Looking for playerbase for: " + this.PlayerName);
GameObject[] playerBases = GameObject.FindGameObjectsWithTag("PlayerBase");
foreach (GameObject playerBase in playerBases)
{
PlayerBaseScript playerBaseScript = playerBase.GetComponent<PlayerBaseScript>();
Debug.Log("Playerbase: " + playerBase + " with ownerPlayerNumber: " + playerBaseScript.ownerPlayerNumber + " from player number: " + this.playerNumber);
if (playerBaseScript.ownerPlayerNumber == this.playerNumber)
{
this.myPlayerBase = playerBase;
this.GotPlayerBase = true;
Debug.Log("Found playerbase for: " + this.PlayerName);
}
}
}
}

The above code iterates through all the player bases and checks if the player base playernumber matches the GamePlayer’s player number. If they match, have GamePlayer set its myPlayerBase to the playerBase, and set GotPlayerBase to true
Calling GetPlayerBase
Finally, all this needs to be called by GameplayerManager to kick everything off. In GameplayerManager.cs create a new function called GetPlayerBase
void GetPlayerBase()
{
Debug.Log("Finding player base for: " + LocalGamePlayerScript.PlayerName);
LocalGamePlayerScript.GetPlayerBase();
}

Then, in the Start function of GameplayManger.cs, make the call for GetPlayerBase

Save everything, build and run the game, and when you start the game from the lobby, you should see the cannotPlaceHere markers placed in the correct spots!

This post is getting long, so I think I am going to call it quits for now…
Next Steps…
So, now the game will spawn units for each player, spawn their cards, find each player’s base and (locally) spawn the cannot place here markers. The next things to do are:
- Update unit movement/placement so it is verified by the server
- Also want to make it so the player only sees where their units are moving. During unit placement, they shouldn’t see the opponent’s units at all
- Have a “Ready Up” system for advancing to the next phase. The phase won’t advance until all players are readyAdvance to the next phase and sync where each player’s units were placed
Hopefully I get that working soon!