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.
Currently in CardConquest, players can move their units a distance of 1 land tile and then advance to the next turn. If players put their units on the same land tile and the turn advances, things get a little…strange visually but not really happens. What I want to have happen, though, is that two players move units onto the same land tile, the game will detect that and declare a “Battle” that will need to be fought.
To detect battles, CardConquest will need to be updated to do the following:
- Go through each land tile and check what units are on that land
- Determine which player each unit belongs two
- If units belong to two or more different players on the land tile, declare that land tile a “battle site”
- When the battle sites are detected, change the game phase from “Unit Movement” to “Battle(s) Detected”
- Some changes to the UI will be needed to indicate to the user where the battle sites are and what units are involved
- Highlight the land tiles where a battle site is
- “separate” the units that belong to different players so it is clear which “side” of a battle a unit is on
- Have the server keep track of the battle sites. If there are multiple battle sites, the server will need to order them and display that so the player’s know which battle will be fought first, second, and so on
Detecting Battles
In order to detect when a battle should occur, the game must be able to detect when the units of two different players are on the same land tile. Right now, LandScript.cs tracks what units are on the tile by adding the unit’s network id to the UnitNetIdsOnLand list.
One way to detect a battle would be to go through each unit in UnitNetIdsOnLand, and then compare it to each GamePlayer’s playerUnitNetIds list, and track when there are two different player’s on the same tile.
The way I decided to make this a bit simpler was to change add a Sync Dictionary to LandScript.cs called UnitNetIdsAndPlayerNumber. The key of the dictionary would be a unit’s network ID, and the value would be the player that unit belongs to. To check if a battle should occur, you simply need to iterate through the one UnitNetIdsAndPlayerNumber dictionary and see if there are two different player values instead of checking a unit network ID against multiple player lists.
First, add UnitNetIdsAndPlayerNumber to LandScript.cs.
public SyncDictionary<uint, int> UnitNetIdsAndPlayerNumber = new SyncDictionary<uint, int>();

You will then need to make sure that the dictionary is updated with the unit and player information. This is done in UnitScript.cs. In the CmdUpdateUnitNewPosition function, make sure that the unit and player are added to the dictionary.
NetworkIdentity networkIdentity = connectionToClient.identity;
GamePlayer requestingPlayer = networkIdentity.GetComponent<GamePlayer>();
landScript.UnitNetIdsAndPlayerNumber.Add(unitNetId, requestingPlayer.playerNumber);

The “requestingPlayer” will be the player who had just requested the server to move one of their units, so the requesting player’s number will be added to the dictionary.
Next, within CmdUpdateUnitNewPosition, you will also need to make sure that the unit and player number are removed from the land’s UnitNetIdsAndPlayerNumber dictionary when the player moves the player to a new land tile. There is a foreach loop to finds the previous land tile the unit was on and removes it from the land’s unit network id list. Now it will need to be updated to also remove that unit from the dictionary. This is done with the following code:
if (landChildScript.UnitNetIdsAndPlayerNumber.ContainsKey(unitNetId))
landChildScript.UnitNetIdsAndPlayerNumber.Remove(unitNetId);

If you save everything and build and run the game now, you should see the UnitNetIdsAndPlayerNumber dictionary update with the correct unit network IDs and player numbers. Below is an example of playing through Unit Movement until I could move two units from each player onto a tile:

Checking Each Land for a Battle
The game will check for a possible battle after the Unit Movement phase ends. After all players pressed the “ready” button to end the Unit Movement phase, the server will check each land tile to see if it has more than one player on it. If there is, a battle is detected. If there isn’t, the Unit Movement phase begins again.
To start, in GameplayManager.cs, a new dictionary variable called battleSiteNetIds will be created to track and sync battle sites between the server and clients.
[Header("Player Battle Info")]
public SyncDictionary<int, uint> battleSiteNetIds = new SyncDictionary<int, uint>();

In order for GameplayManager.cs to be able to use SyncDictionary, you need to make sure it imports Mirror and also inherits from NetworkBehaviour:

The game phase changes are done in GamePlayer.cs, so, in that script a new server function called CheckForPossibleBattles will be created. The code is shown below.
[Server]
public bool CheckForPossibleBattles()
{
Debug.Log("Ran CheckForPossibleBattles");
GameObject landTileHolder = GameObject.FindGameObjectWithTag("LandHolder");
bool wasBattleDetected = false;
GameplayManager.instance.battleSiteNetIds.Clear();
foreach (Transform landObject in landTileHolder.transform)
{
LandScript landScript = landObject.gameObject.GetComponent<LandScript>();
if (landScript.UnitNetIdsAndPlayerNumber.Count > 1)
{
int playerNumber = -1;
foreach (KeyValuePair<uint, int> units in landScript.UnitNetIdsAndPlayerNumber)
{
if (playerNumber != units.Value && playerNumber != -1)
{
Debug.Log("Two different player values discovered. Value 1: " + playerNumber + " Value 2: " + units.Value);
wasBattleDetected = true;
int battleNumber = GameplayManager.instance.battleSiteNetIds.Count + 1;
GameplayManager.instance.battleSiteNetIds.Add(battleNumber, landObject.GetComponent<NetworkIdentity>().netId);
break;
}
playerNumber = units.Value;
}
}
}
return wasBattleDetected;
}

CheckForPossibleBattles does the following:
- CheckForPossibleBattles is a bool function, meaning it will return a value of either true or false when it is called and executed
- Gets the LandTileHolder object by its tag, “LandHolder”
- creates a boolean variable wasBattleDetected
- Clears the battleSiteNetIds dictionary in GameplayManager
- iterates through every land object in LandTileHolder
- Gets the LandScript.cs script of the land object
- checks if there is more than 1 unit on the land tile
- create a new int variable playerNumber. Set it to -1 for now, but later will be used to save the player number from the dictionary
- If yes, iterate through every key value pair in the land’s UnitNetIdsAndPlayerNumber dictionary
- if the player number of the current key value pair does not equal the player number of the previous key value pair, declare that a battle was detected
- set wasBattleDetected to true
- Add the land objects network id to GameplayManager’s battleSiteNetIds dictionary
- break out of the foreach loop
- set playerNumber to the playernumber value that key value pair of UnitNetIdsAndPlayerNumber
- Return the value of wasBattleDetected
Now, CheckForPossibleBattles will need to be used to actually check for battles. This will be done in the CheckIfAllPlayersAreReadyForNextPhase function of GamePlayer.cs. CheckIfAllPlayersAreReadyForNextPhase is already used to advance to the next game phase. So, now, when the current phase is Unit Movement (meaning the players just played a turn in Unit Movement) the server will call CheckForPossibleBattles to detect battles. If a battle is detected, the phase will be changed to “Battle(s) Detected.” The new code for CheckIfAllPlayersAreReadyForNextPhase is shown below:
if (allPlayersReady)
{
foreach (GamePlayer gamePlayer in Game.GamePlayers)
{
gamePlayer.ReadyForNextPhase = false;
}
if (Game.CurrentGamePhase == "Unit Placement")
{
Game.CurrentGamePhase = "Unit Movement";
Debug.Log("Changing phase to Unit Movement");
RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
return;
}
if (Game.CurrentGamePhase == "Unit Movement")
{
// Placeholder code for real code which will do things like check for battles
bool areThereAnyBattles = CheckForPossibleBattles();
Debug.Log("Current phase is Unit Movement");
if (areThereAnyBattles)
{
Debug.Log("Battle detected. Changing Game phase to 'Battle(s) Detected'");
Game.CurrentGamePhase = "Battle(s) Detected";
RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
return;
}
else
{
Debug.Log("No battles detected");
Debug.Log("Game phase remains on Unit Movement");
RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
return;
}
}
RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
}

So, if CheckForPossibleBattles returns as true, a battle was detected and the phase changes. If it returns false, the phase doesn’t change and the players do another turn of Unit Movement.
If you save everything and build and run now, then make it so players end the Unit Movement phase with two players on the same land tile, it looks like nothing really happened. It kind of looks like the Unit Movement phase didn’t end correctly. Why’s that?
Well, if you look at logs you should see that CheckForPossibleBattles found the battle. Then, if you look in Unity at the Network Manager, you will the “Current Game Phase” is set to “Battle(s) Detected,” as you would expect.


But why doesn’t the phase change? Well, after CheckIfAllPlayersAreReadyForNextPhase is run on the server, it returns the current game phase to the players through RpcAdvanceToNextPhase. RpcAdvanceToNextPhase then calls ChangeGamePhase from GameplayManager with the new game phase as an argument. The reason the phase doesn’t appear to change is that ChangeGamePhase doesn’t have anything that tells it what to do if the new phase is “Battle(s) Detected,” as you can see in the code below.
public void ChangeGamePhase(string newGamePhase)
{
if (currentGamePhase == "Unit Placement" && newGamePhase == "Unit Movement")
{
currentGamePhase = newGamePhase;
EndUnitPlacementPhase();
}
if (currentGamePhase == "Unit Movement" && newGamePhase == "Unit Movement")
{
currentGamePhase = newGamePhase;
StartUnitMovementPhase();
}
}
So, let’s fix that!
Starting the Battles Detected Phase
A new function in GameplayManager called StartBattlesDetected will be created. For right now, the code will be very simply and will only make sure that the GamePhase text of the UI is updated. The code is shown below.
void StartBattlesDetected()
{
Debug.Log("Starting StartBattlesDetected");
SetGamePhaseText();
}

StartBattlesDetected will then need to be called from ChangeGamePhase when the new phase is “Battle(s) Detected.”
if (currentGamePhase == "Unit Movement" && newGamePhase == "Battle(s) Detected")
{
currentGamePhase = newGamePhase;
StartBattlesDetected();
}

If you save and run everything now, you should see that the “GamePhase” text is updated to “Battle(s) Detected.” That text doesn’t quite fit, but that will be fixed next!

Updating UI for Battles Detected
Beyond just changing the GamePhase text, the UI will need to be updated for the Battles Detected phase. Some new UI elements will be added, but not much. All I really need for the Battles Detected UI to do is allow players to see that battles were detected and then allow players to advance to the next phase (fighting the battles and stuff). One thing I do want to do is to still allow players to view all the cards like they could before to help planning out their battles, so those UI elements (the buttons to view the cards) will need to be moved to the new Battles Detected UI.
First, a new BattlesDetectedPanel will be created in Unity. You can do this by selecting the UnitMovementUI panel and using the ctrl+d shortcut to duplicate it.

Next, rename the duplicated UnitMovementUI panel to BattlesDetectedPanel. Expand BattlesDetectedPnale and delete everything except for endUnitMovementButton.

Rename the endUnitMovementButton to startBattlesButton. Select its child Text object, and set the text to “Start Battles.” This button will be used by the players to advance to the next phase, same as the endUnitMovementButton was used during the Unit Movement phase.

Open GameplayManager.cs in Visual Studio. Add a new GameObject variable called BattlesDetectedPanel to store the BattlesDetectedPanel object.
[SerializeField] GameObject BattlesDetectedPanel;

Then add another variable for the startBattlesButton.
[Header("Ready Buttons")]
[SerializeField] private GameObject startBattlesButton;

Save GameplayManager.cs and go back to the Unity Editor. Select the GameplayManager object, and attach the BattlesDetectedPanel and startBattlesButton objects.

Activating the Battles Detected UI
Back in GameplayManager.cs, create a new function called ActivateBattlesDetectedUI. This will be used to activate the UI for the phase. To start, ActivateBattlesDetectedUI will check if other phase UI panels are active, and if they are, deactivate them. It will then activate the BattlesDetectedPanel object.
if (UnitPlacementUI.activeInHierarchy)
UnitPlacementUI.SetActive(false);
if (UnitMovementUI.activeInHierarchy)
UnitMovementUI.SetActive(false);
if (!BattlesDetectedPanel.activeInHierarchy)
BattlesDetectedPanel.SetActive(true);

During the Battles Detected phase, I still want players to be able to view their cards and their opponent cards. I could re-create all the buttons in the new BattlesDetectedPanel, but then I run into the problem of the dynamically create buttons for opponent cards. So, the following code will be added to move the buttons from the Unit Movement UI panel to the BattlesDetectedPanel. First, move the non-dynamically created buttons to show the player’s own cards and the show/hide opponent buttons
hidePlayerHandButton.transform.SetParent(BattlesDetectedPanel.GetComponent<RectTransform>(), false);
showPlayerHandButton.transform.SetParent(BattlesDetectedPanel.GetComponent<RectTransform>(), false);
showPlayerDiscardButton.transform.SetParent(BattlesDetectedPanel.GetComponent<RectTransform>(), false);
showOpponentCardButton.transform.SetParent(BattlesDetectedPanel.GetComponent<RectTransform>(), false);
hideOpponentCardButton.transform.SetParent(BattlesDetectedPanel.GetComponent<RectTransform>(), false);

The above code is setting the buttons to be children of the RectTransform of the BattlesDetectedPanel. By setting them as children to the RectTransform, this should allow for the buttons to render in the correct position relative to the panel. That’s something that caused issues for me before when trying to dynamically create new UI elements and something I wish I knew earlier! Oh well.
Next, the correct buttons will be activated/deactivated as necessary so the player sees the correct “default” view they should see when no cards are being viewed.
if (hidePlayerHandButton.activeInHierarchy)
hidePlayerHandButton.SetActive(false);
if (!showPlayerHandButton.activeInHierarchy)
showPlayerHandButton.SetActive(true);
if (!showPlayerDiscardButton.activeInHierarchy)
showPlayerDiscardButton.SetActive(true);
if (!showOpponentCardButton.activeInHierarchy)
showOpponentCardButton.SetActive(true);
if (hideOpponentCardButton.activeInHierarchy)
hideOpponentCardButton.SetActive(false);
if (!startBattlesButton.activeInHierarchy)
startBattlesButton.SetActive(true);

Next will be the code to check if the player was viewing their own cards when the game phase changed. If they were, hide their cards.
if (LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().isPlayerViewingTheirHand)
{
LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().HidePlayerHandOnScreen();
}
Then, the dynamically generated opponent card buttons will be looped through, deactivated from the UI and also moved to be children of BattlesDectedPanel’s RectTransform.
if (opponentHandButtons.Count > 0)
{
foreach (GameObject opponentHandButton in opponentHandButtons)
{
opponentHandButton.transform.SetParent(BattlesDetectedPanel.GetComponent<RectTransform>(), false);
opponentHandButton.SetActive(false);
}
}

The last bit of code will be to check if the player was viewing and opponent cards when the phase changed, and if so, hide those cards.
if (isPlayerViewingOpponentHand && playerHandBeingViewed != null)
{
playerHandBeingViewed.GetComponent<PlayerHand>().HidePlayerHandOnScreen();
playerHandBeingViewed = null;
isPlayerViewingOpponentHand = false;
}

Now you will need to call ActivateBattlesDetectedUI from the StartBattlesDetected function. I also added some other more “cleanup” actions to StartBattlesDetected to clear and selected units and make sure that the “haveUnitsMoved” value was set to false.
void StartBattlesDetected()
{
Debug.Log("Starting StartBattlesDetected");
SetGamePhaseText();
haveUnitsMoved = false;
if (MouseClickManager.instance.unitsSelected.Count > 0)
MouseClickManager.instance.ClearUnitSelection();
ActivateBattlesDetectedUI();
}

The last change for the UI will be to adjust the font size of the GamePhase text so the “Battle(s) Detected” string doesn’t get cut off. In the SetGamePhaseText function of GameplayManager.cs, add the following code to adjust the font size if it is the Battle(s) Detected phase.
if (currentGamePhase == "Battle(s) Detected")
GamePhaseText.fontSize = 40;
else
GamePhaseText.fontSize = 50;

Save GameplayManager.cs, save the Gameplay scene in Unity, go back to the TitleScreen scene, then build and run. Get to a situation where a “battle” should be detected, and watch the new UI spawn!

Highlighting Battle Sites
Now that the game is detecting when battles should occur and where, the game should indicate to the player in some way where that battle is. I decided to do that by highlighting the land area of the battle site using my already created land-outline prefab. The game will simply spawn the highlight when a battle is detected.
In LandScript.cs, create a new GameObject variable which will be used to store the highlight object for the battle.
private GameObject battleOutlineObject;

Next, create a new function called HighlightBattleSite that will create the battle highlgiht by instantiating the landOutline prefab and saving it in battleOutlineObject.
public void HighlightBattleSite()
{
if (!battleOutlineObject)
{
Debug.Log("Creating the highlight for the battle site");
battleOutlineObject = Instantiate(landOutline, transform.position, Quaternion.identity);
battleOutlineObject.transform.SetParent(gameObject.transform);
}
}

Now all that will need to be done is to call HighlightBattleSite on each land tile where there is a battle site. In GameplayManager.cs, first create a new boolean variable to keep track of whether battle sites were highlighted or not.
public bool haveBattleSitesBeenDone = false;

Then, create a new function called HighlightBattleSites that will go through each keyvalue pair in the battleSiteNetIds dictionary and call HighBattleSites from LandScript.cs.
public void HighlightBattleSites()
{
Debug.Log("HighlightBattleSites starting. Total battle sites: " + battleSiteNetIds.Count);
if (battleSiteNetIds.Count > 0 && !haveBattleSitesBeenDone)
{
foreach (KeyValuePair<int, uint> battleSiteId in battleSiteNetIds)
{
LandScript battleSiteIdScript = NetworkIdentity.spawned[battleSiteId.Value].gameObject.GetComponent<LandScript>();
battleSiteIdScript.HighlightBattleSite();
}
haveBattleSitesBeenDone = true;
}
}

To highlight each battle site, HighlightBattleSites in GameplayManager.cs needs to be called when the game phase changes. I thought this would be really simple, but due to what I believe is latency issues between the SyncDict of battle sites syncing on the server and clients, and that there is only one GameplayManager instance per game instead of one per player, it turned out to be quite a convoluted and strange process! I hope I am able to explain the next few sections clearly enough for you to follow along…
Updating Unit Positions
The first thing to do in this process will be to make sure that the position of the units gets updated when the phase changes to Battle(s) Detected. You may have noticed in the image above when the UI gets updated, the units don’t get updated. While I was testing this out before writing this blog post, it seemed like if I didn’t make sure that the unit positions were updated before trying to highlight battles sites (and then later reorganize units on the screen and other battle related stuff), I’d run into issues. So, it’s apparently important to get this all sorted!
To update the unit’s positions, go to StartBattlesDetected in GameplayManager.cs and add calls for SaveUnitStartingLocation
and LocalGamePlayerScript.UpdateUnitPositions
.

After all the unit positions have been updated, we will want to trigger an event to start the process to highlight the battle sites. This will be done with a SyncVar variable and a hook function.
In GamePlayer.cs, create a new SyncVar boolean variable called updatedUnitPositionsForBattleSites that has a hook function called HandleUpdatedUnitPositionsForBattleSites.
[Header("Player Battle Info")]
[SyncVar(hook = nameof(HandleUpdatedUnitPositionsForBattleSites))] public bool updatedUnitPositionsForBattleSites = false;

At the end of the CmdUpdateUnitPositions function of GamePlayer.cs, add a check to set updatedUnitPositionsForBattleSites to true for the requesting player if it is the Battle(s) Detected phase.
if (Game.CurrentGamePhase == "Battle(s) Detected")
requestingPlayer.GetComponent<GamePlayer>().updatedUnitPositionsForBattleSites = true;
else
requestingPlayer.GetComponent<GamePlayer>().updatedUnitPositionsForBattleSites = false;

Then, create the HandleUpdatedUnitPositionsForBattleSites hook function. If the value is changed to true, it will call a function from GameplayManager.
void HandleUpdatedUnitPositionsForBattleSites(bool oldValue, bool newValue)
{
if (newValue)
GameplayManager.instance.CheckIfAllUpdatedUnitPositionsForBattleSites();
}

The CheckIfAllUpdatedUnitPositionsForBattleSites function doesn’t exist yet in GameplayManager, so let’s create it!
public void CheckIfAllUpdatedUnitPositionsForBattleSites()
{
Debug.Log("Executing CheckIfAllUpdatedUnitPositionsForBattleSites");
bool haveAllUnitsUpdated = false;
if (!LocalGamePlayerScript.updatedUnitPositionsForBattleSites)
{
Debug.Log("CheckIfAllUpdatedUnitPositionsForBattleSites: LocalGamePlayer not ready");
return;
}
else
haveAllUnitsUpdated = LocalGamePlayerScript.updatedUnitPositionsForBattleSites;
GameObject[] allGamePlayers = GameObject.FindGameObjectsWithTag("GamePlayer");
foreach (GameObject gamePlayer in allGamePlayers)
{
GamePlayer gamePlayerScript = gamePlayer.GetComponent<GamePlayer>();
if (!gamePlayerScript.updatedUnitPositionsForBattleSites)
{
haveAllUnitsUpdated = false;
Debug.Log("CheckIfAllUpdatedUnitPositionsForBattleSites: " + gamePlayerScript.PlayerName + " not ready");
break;
}
else
{
haveAllUnitsUpdated = gamePlayerScript.updatedUnitPositionsForBattleSites;
}
}
if (haveAllUnitsUpdated)
{
Debug.Log("CheckIfAllUpdatedUnitPositionsForBattleSites: all gameplayers are ready!");
HighlightBattleSites();
}
}

The point of CheckIfAllUpdatedUnitPositionsForBattleSites is to make sure that all GamePlayer’s have had their units’ positions updated. It first checks if the LocalGamePlayer has its updatedUnitPositionsForBattleSites value set to true, and then goes through all other GamePlayer objects and checks if their updatedUnitPositionsForBattleSites values are set to true. If all GamePlayer’s have updatedUnitPositionsForBattleSites set to true, then it calls HighlightBattleSites!
The reason to go through all this, to wait for all the units positions to update and to make sure that the SyncVar updatedUnitPositionsForBattleSites had synced to “true” for all players, was to make sure there had been enough “time” for the SyncDict battleSiteNetIds had time to update and sync to all clients, so that dictionary could then be used to highlight the battle sites. Since updatedUnitPositionsForBattleSites is on all the GamePlayer objects, CheckIfAllUpdatedUnitPositionsForBattleSites will be run on the client after each client has their updatedUnitPositionsForBattleSites changed. Then, HighlightBattleSites will only run after all the GamePlayer’s units were updated.
Save everything, build and run, and you should see the battle sites highlighted in the Battles Detected phase.

Separating Units of Different Players
When a battle is detected, the Game should then “move” the units of different players to one side of the battle site to make it clear who is fighting who, and with what units they will be fighting with. For right now, I’m going to just hard code this as a two player game and at a (much later) time figure out how to do this for 3+ players.
Open LandScript.cs and create some new lists that will be used to store the GameObjects of each player’s infantry and tanks. There will also be a new list created to store the unit text for each player’s units.
[Header("Battle Unit Lists")]
public List<GameObject> Player1Inf = new List<GameObject>();
public List<GameObject> Player1Tank = new List<GameObject>();
public List<GameObject> Player2Inf = new List<GameObject>();
public List<GameObject> Player2Tank = new List<GameObject>();
public List<GameObject> BattleUnitTexts = new List<GameObject>();

Then, a new function will be created in LandScript.cs called MoveUnitsForBattleSite. This will loop through the UnitNetIdsAndPlayerNumber dictionary, check which unit belongs to which player, and move the unit to its correct position for that player and unit type. The code is shown below:
public void MoveUnitsForBattleSite()
{
Debug.Log("Executing MoveUnitsForBattleSite");
//For now this will be "hard coded" for a 2 player game where the playernumbers are either 1 or 2
foreach (KeyValuePair<uint, int> units in UnitNetIdsAndPlayerNumber)
{
GameObject unitObject = NetworkIdentity.spawned[units.Key].gameObject;
Vector3 newPosition = unitObject.transform.position;
// Adjust units for player 1
if (units.Value == 1)
{
if (unitObject.tag == "infantry")
{
newPosition.x -= 0.5f;
unitObject.transform.position = newPosition;
Player1Inf.Add(unitObject);
}
else if (unitObject.tag == "tank")
{
newPosition.x -= 0.7f;
unitObject.transform.position = newPosition;
Player1Tank.Add(unitObject);
}
}
//Adjust units for Player 2
else if (units.Value == 2)
{
if (unitObject.tag == "infantry")
{
newPosition.x += 0.5f;
unitObject.transform.position = newPosition;
Player2Inf.Add(unitObject);
}
else if (unitObject.tag == "tank")
{
newPosition.x += 0.7f;
unitObject.transform.position = newPosition;
Player2Tank.Add(unitObject);
}
}
}
}

MoveUnitsForBattleSite loops through each unit in the dictionary. It finds the unit’s GameObject based on its network ID. Then, based on the Player Number value stored in UnitNetIdsAndPlayerNumber, repositions the unit to either the right or left by adjusting its transform.position.x value. Units for player 1 are moved to the left. Units for player 2 moved to the right. Each player’s units are added to a list for that player, which will be used later to keep track of which unit belongs to which player.
MoveUnitsForBattleSite will be called from the HighlightBattleSites function of GameplayManager.cs. After battleSiteIdScript.HighlightBattleSite()
is called to highlight the battle site, battleSiteIdScript.MoveUnitsForBattleSite()
will be called to move the units on the battle site.

Save all the scripts, build and run, move units until there is a battle, and you should now see not only the battle site highlights, but the units moved as well!

Adding Unit Text for each Player’s Units
As you can see in the image above, the units are separated, but you can’t tell how many of each unit each player has. In LandScript.cs, a new function called UnitTextForBattles for creating the text objects. The code is shown below
public void UnitTextForBattles()
{
if (infText)
{
Destroy(infText);
infText = null;
}
if (tankText)
{
Destroy(tankText);
tankText = null;
}
//Spawn unit text for player 1
if (Player1Inf.Count > 1)
{
Debug.Log("Creating text box for multiple infantry for player 1");
GameObject player1InfText = Instantiate(infTextHolder, gameObject.transform);
player1InfText.transform.position = transform.position;
Vector3 player1InfTextPosition = new Vector3(-1.75f, -0.75f, 0.0f);
player1InfText.transform.localPosition = player1InfTextPosition;
player1InfText.transform.GetChild(0).GetComponent<TextMeshPro>().SetText("x" + Player1Inf.Count.ToString());
BattleUnitTexts.Add(player1InfText);
}
if (Player1Tank.Count > 1)
{
Debug.Log("Creating text box for multiple tanks for player 1");
GameObject player1TankText = Instantiate(tankTextHolder, gameObject.transform);
player1TankText.transform.position = transform.position;
Vector3 player1TankTextPosition = new Vector3(-3.0f, -0.75f, 0.0f);
player1TankText.transform.localPosition = player1TankTextPosition;
player1TankText.transform.GetChild(0).GetComponent<TextMeshPro>().SetText("x" + Player1Tank.Count.ToString());
BattleUnitTexts.Add(player1TankText);
}
//Spawn unit text for player2
if (Player2Inf.Count > 1)
{
Debug.Log("Creating text box for multiple infantry for player 2");
GameObject player2InfText = Instantiate(infTextHolder, gameObject.transform);
player2InfText.transform.position = transform.position;
Vector3 player2InfTextPosition = new Vector3(0.3f, -0.75f, 0.0f);
player2InfText.transform.localPosition = player2InfTextPosition;
player2InfText.transform.GetChild(0).GetComponent<TextMeshPro>().SetText("x" + Player2Inf.Count.ToString());
BattleUnitTexts.Add(player2InfText);
}
if (Player2Tank.Count > 1)
{
Debug.Log("Creating text box for multiple tanks for player 2");
GameObject player2TankText = Instantiate(tankTextHolder, gameObject.transform);
player2TankText.transform.position = transform.position;
Vector3 player2TankTextPosition = new Vector3(0.0f, -0.75f, 0.0f);
player2TankText.transform.localPosition = player2TankTextPosition;
player2TankText.transform.GetChild(0).GetComponent<TextMeshPro>().SetText("x" + Player2Tank.Count.ToString());
BattleUnitTexts.Add(player2TankText);
}
}

First, if there is any already existing unit text objects, it is destroyed. Then, the lists for Player 1 and Player 2’s units are checked if they have more than 1 unit in them. If they do, a new text object is spawned, repositioned below the unit, and then added to the “BattleUnitTexts” list.
Now, UnitTextForBattles(); just needs to be called. This will be done from the “MoveUnitsForBattleSite” function in LandScript.cs. So, after the units are moved on the battle site, the unit text objects will be spawned.

Save everything, build and run, and observe the unit text being spawned under the moved units!

Labeling The Battle Sites
Battles are being detected, highlighted, and the units are being moved and labeled. Now, the players should know what order they should expect to fight the battles in. When battles are detected in GamePlayer.cs’s CheckForPossibleBattles, they are added to GameplayManager.cs’s battleSiteNetIds dictionary, with the key being the battleNumber. The battleNumber value will be the order battles are fought in.
So, what I want to do is spawn some text on the battle site that just says “#1” for the first battle site, “#2” for the second, and so on.
To do this, I am first going to create a new prefab for the text object. I first selected my infTextHolder prefab, then used ctrl+d to duplicate it.

Rename the duplicated prefab to battleNumberText, then move it to the Assets>OfflinePrefabs>MapPrefabs directory.

Double click on the battleNumberText prefab to open the prefab in the Unity Editor. If there is a NetworkIdentity component on the prefab, you can remove it. Then, Expand the battleNumberText object and select the unitText child object. Set the RectTransform values to the following:
- Pos X: 0
- Pos Y: 1.8
- Width: 2
- Height: 2

These values will help position the text in the correct position when it is spawed.
Then, scroll down to the “TextMeshPro – Text” section and set the font size to 10 and the alignment to centered.

Make sure the prefab is saved, then open LandScript.cs. Add the following variables to be used to store the battleNumberText prefab and the GameObject that will be spawned from the prefab.
[Header("Text Objects")]
[SerializeField] private GameObject battleNumberTextPrefab;
public GameObject battleNumberTextObject;

Save LandScript.cs and go back to the Unity Editor. In the Assets>Resources>Prefabs>MapPrefabs directory, open hex-tile-land to edit the prefab. Drag and drop the battleNumberText prefab onto hex-land-tile’s BattleNumberTextPrefab variable to attach the prefab.

Then, in LandScript.cs, create a new function called SpawnBattleNumberText. It will take an integar battleSiteNumber as an argument.
public void SpawnBattleNumberText(int battleSiteNumber)
{
if (!battleNumberTextObject)
{
battleNumberTextObject = Instantiate(battleNumberTextPrefab, this.transform);
battleNumberTextObject.transform.position = transform.position;
battleNumberTextObject.transform.GetChild(0).GetComponent<TextMeshPro>().SetText("#" + battleSiteNumber);
}
}

SpawnBattleNumberText will spawn a new battleNumberTextObject using the battleNumberTextPrefab. The text will be set using the battleSiteNumber argument.
SpawnBattleNumberText will then be called from GameplayManager.cs in the HighlightBattleSites function. The key value from battleSiteNetIds will be used to set the battleNumberText value.

Save everything, build and run, and you should now see the battle number text spawn above battle sites.

Advancing to the Next Game Phase
This whole “Battles Detected” phase is really just an intermediary phase (or “liminal” phase, if you will) between Unit Movement and the phases for fighting actual battles. I wanted Battles Detected to be a way to inform players that they will begin to fight battles in the next phase and give them a moment to prepare/realize that instead of just dropping them into a battle without warning.
All of that is to say that nothing really “happens” in this game phase, at least in terms of playing a game. The players are informed that battles were found, and then they can click on the “Start Battles” button to begin the battles.
When the game is advanced past Battles Detected, the players will start fighting the first battle. To keep track of the current “Battle,” two new variables will be added to GameplayManager.cs to track the current battle number and the network id of the current battle site.
[SyncVar] public int battleNumber;
[SyncVar] public uint currentBattleSite;

Then, in GamePlayer.cs, a new check will be added to CheckIfAllPlayersAreReadyForNextPhase to advance it to the next phase.
if (Game.CurrentGamePhase == "Battle(s) Detected")
{
GameplayManager.instance.battleNumber = 1;
foreach (KeyValuePair<int, uint> battles in GameplayManager.instance.battleSiteNetIds)
{
if (battles.Key == GameplayManager.instance.battleNumber)
{
GameplayManager.instance.currentBattleSite = battles.Value;
break;
}
}
Game.CurrentGamePhase = "Choose Cards:\nBattle #1";
Debug.Log("Game phase changed to Choose Cards");
RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
return;
}

So, if the current game phase is “Battle(s) Detected,” the battle number will be set to 1 since “1” will always be the first battle fought. Then, the network id of the battle site will be retrieved from the battleSiteNetIds based on the battle number as a key. The game phase will then be changed to “Choose Cards:\nBattle #1.” The first step in a battle will be for players to choose a card to play for their battle, so that’s why the next phase will be “Choose Cards…”
In GameplayManager.cs, the ChangeGamePhase function will need to be updated to tell the game what to do when the phase is changed to “Choose Cards.”
if (currentGamePhase == "Battle(s) Detected" && newGamePhase.StartsWith("Choose Cards"))
{
currentGamePhase = newGamePhase;
StartChooseCards();
}

StartChooseCards will need to be created, along with an ActivateChooseCards function. For right now, ActivateChooseCards will be an empty function, and StartChooseCards will call SetGamePhaseText and ActivateChooseCards.
public void StartChooseCards()
{
Debug.Log("Starting StartBattles");
SetGamePhaseText();
ActivateChooseCards();
}
void ActivateChooseCards()
{
}

Save everything, build and run, move units into battles, then advance past Battles Detected.

Great! The game advanced to “Choose Cards:.” But, wait. Wasn’t the phase changed to “Choose Cards:\nBattle #1?” I don’t see the “Battle #1” part…
Some UI Adjustments
The full Game Phase of “Choose Cards:\nBattle #1” doesn’t display because the newline character (the “\n”) makes the text larger than the box the text is in. So, make the box bigger!
Go to the Gameplay scene in the Unity Editor. Expand the GameplayUI canvas and select GamePhase. Change its height from 50 to 100. Then, set its Pos Y value from -25 to -50.

A new UI panel will also be created for the Choose Cards phase. Duplicate the BattlesDetectedPanel and rename it to ChooseCardsPanel.

Expand ChooseCardsPanel, select the duplicated StartBattlesButton, and rename it to confirmChooseCard.

Expand confirmChooseCard, select its Text child object, and set the Text value to “Confirm Card”

Open GameplayManager.cs in Visual Studio and add two new variables to store the ChooseCardsPanel and confirmCardButton objects.
[Header("Choose Cards Section")]
[SerializeField] private GameObject ChooseCardsPanel;
[SerializeField] private GameObject confirmCardButton;

Go back into the Unity Editor and attach the ChooseCardsPanel and confirmChooseCard objects to their respective variables on the GameplayManager object.

Back in GameplayManager.cs you can now add the following code to ActivateChooseCards to deactivate previous phase panels, activate the ChooseCardsPanel, and then move the “view card” buttons to the new ChooseCardsPanel. It will also detect if player’s are view their own or other player’s cards and hide them as necessary.
void ActivateChooseCards()
{
if (UnitMovementUI.activeInHierarchy)
UnitMovementUI.SetActive(false);
if (BattlesDetectedPanel.activeInHierarchy)
BattlesDetectedPanel.SetActive(false);
if (!ChooseCardsPanel.activeInHierarchy)
ChooseCardsPanel.SetActive(true);
// Move buttons to the UnitMovementUI
hidePlayerHandButton.transform.SetParent(ChooseCardsPanel.GetComponent<RectTransform>(), false);
showPlayerHandButton.transform.SetParent(ChooseCardsPanel.GetComponent<RectTransform>(), false);
showPlayerDiscardButton.transform.SetParent(ChooseCardsPanel.GetComponent<RectTransform>(), false);
showOpponentCardButton.transform.SetParent(ChooseCardsPanel.GetComponent<RectTransform>(), false);
hideOpponentCardButton.transform.SetParent(ChooseCardsPanel.GetComponent<RectTransform>(), false);
if (hidePlayerHandButton.activeInHierarchy)
hidePlayerHandButton.SetActive(false);
if (!showPlayerHandButton.activeInHierarchy)
showPlayerHandButton.SetActive(true);
if (!showPlayerDiscardButton.activeInHierarchy)
showPlayerDiscardButton.SetActive(true);
if (!showOpponentCardButton.activeInHierarchy)
showOpponentCardButton.SetActive(true);
if (hideOpponentCardButton.activeInHierarchy)
hideOpponentCardButton.SetActive(false);
if (!startBattlesButton.activeInHierarchy)
startBattlesButton.SetActive(true);
if (LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().isPlayerViewingTheirHand)
{
LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().HidePlayerHandOnScreen();
}
if (opponentHandButtons.Count > 0)
{
foreach (GameObject opponentHandButton in opponentHandButtons)
{
opponentHandButton.transform.SetParent(ChooseCardsPanel.GetComponent<RectTransform>(), false);
opponentHandButton.SetActive(false);
}
}
if (isPlayerViewingOpponentHand && playerHandBeingViewed != null)
{
playerHandBeingViewed.GetComponent<PlayerHand>().HidePlayerHandOnScreen();
playerHandBeingViewed = null;
isPlayerViewingOpponentHand = false;
}
}

Updating the Battles Detected Ready Button
One thing I didn’t do earlier was update GameplayManager.cs’s UpdateReadyButton function to change the button text during the Battle(s) detected phase. The code is shown below.
if (currentGamePhase == "Battle(s) Detected")
{
if (LocalGamePlayerScript.ReadyForNextPhase)
{
Debug.Log("Local Player is ready to go to next phase.");
startBattlesButton.GetComponentInChildren<Text>().text = "Unready";
if (MouseClickManager.instance.unitsSelected.Count > 0)
MouseClickManager.instance.ClearUnitSelection();
}
else
{
Debug.Log("Local Player IS NOT ready to go to next phase.");
startBattlesButton.GetComponentInChildren<Text>().text = "Start Battles";
}
}

Save everything, build and run, and you can advance to the Choose Cards phase with the updated UI!

Hiding Unit Text when Viewing Cards
Oh, another thing I forgot about! When you view cards, the new “battle unit text” and the battle site number text display over the cards, like this:

This can be fixed in LandScript.cs’s HideUnitText and UnHideUnitText. In HideUnitText, add the following code:
if (BattleUnitTexts.Count > 0)
{
foreach (GameObject battleText in BattleUnitTexts)
{
if (battleText)
battleText.SetActive(false);
}
}
if (battleNumberTextObject)
battleNumberTextObject.SetActive(false);
If there are any objects in the BattleUnitTexts list, hideUnitText will iterate through each of them and deactivate them. If the battleNumberTextObject exists, it will be deactivated.
UnHideUnitText will then do the opposite and reactivate everything with the following code:
if (BattleUnitTexts.Count > 0)
{
foreach (GameObject battleText in BattleUnitTexts)
{
if (battleText)
battleText.SetActive(true);
}
}
if (battleNumberTextObject)
battleNumberTextObject.SetActive(true);

Save everything, build and run, and the text is hidden when viewing cards!

One More Problem to Fix…
One thing that causes issues is that after units are moved on battle sites, players can still “select” the units by click on them. When the unit is selected, the units are expanded, and well, this messes things up as you can see below when I first selected a unit then deselected it


So, to make it so units couldn’t be selected during Battle(s) detected or Choose Cards, I added a new boolean varialbe to MouseClickManager called canSelectUnitsInThisPhase
public bool canSelectUnitsInThisPhase = false;

Then add && canSelectUnitsInThisPhase
to the end of the if statement that checks if you clicked on a unit object.
if (rayHitUnit.collider.gameObject.GetComponent<NetworkIdentity>().hasAuthority && !playerViewingHand && !playerViewingOpponentHand && !playerReadyForNextPhase && canSelectUnitsInThisPhase)
canSelectUnitsInThisPhase defaults to false, so it will need to be set to true somewhere. This will be done in GameplayManager.cs by adding MouseClickManager.instance.canSelectUnitsInThisPhase = true
to functions.
First, it will be added to the ActivateUnitPlacementUI function.

After that, canSelectUnitsInThisPhase will only need to be changed in the ChangeGamePhase function. When the phase is changed to Unit Movement, it will be set to true. When it changes to Battles Detected or Choose Cards, it will be set to false.

Remember, for Battles Detected and Choose cards it needs to be set to false with MouseClickManager.instance.canSelectUnitsInThisPhase = false
!
And now, here’s a video of everything in action!
Next Steps
Next, I will to start letting players do some battles! So, I think it will be something like:
- New UI! Have to display to players the power of their units plus the power of their opponent’s units in the current battle
- Allow Players to select their own cards
- And probably a bunch more!
Smell ya later nerds