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.
In the previous post, CardConquest was updated for battles to take place and a winner of the battle is declared (or a draw if no winner). Now, the actual “Retreat Units” phase will be created. This will detect when a player or players need to retreat their units, enforce where they can retreat, and then move onto the next battle. If there are no more battles, to fight, the game will return to the Unit Movement phase.
The following tasks will need to be completed:
- Modify where units can retreat in relation to their own and their opponent’s base
- I noticed an issue in terms of game play that I want to change
- Add unit movement checks specific for retreating units
- Also do things like change how unit selection behaves in Retreat Units – don’t expand them out if they are on the battle site land tile, and so on
- Reset the units still on the land tile of the battle back to “normal” so they aren’t spaced out like they were for the battle
- Deal with the “draw scenario”
- Both players will have to retreat
- There will be some land tiles that both players will be eligible to retreat to. Let both players retreat to the same tile. Then, after Retreat Units completes, detect if a new battle should occur from both players retreating to the same land tile
- If the players retreating to the same land tile, add that to the list of battles to complete and perform all the battle highlight/unit expansion stuff
- After all battles are completed, advance to the Unit Movement phase
Changes to Where Units Can Retreat to
Right now, after a battle is decided, the game will determine where the losing player can retreat to in order to calculate any units lost due to there not being enough “space” to retreat to. The logic for this allowed for the player to retreat their units to any land tile no more than 1 land tile away from the battle site so long as there were no enemy units on that land tile. After thinking that through a bit more, I figured out a potential “problem” this could cause for the game play. Consider the below scenario:

In the above battle, the blue player is in the best position to win due to having more power from their army. If both players have the same cards available in their hand, then the blue player should be able to play a card to get a power total the green player can’t match. So, then when the green player has to retreat after losing the battle, they could then retreat their units to the two land tiles to the right of the battle site.
The “problem” that arises from this, is that ultimately the way a player will “win” the game is by capturing the other player’s base. If the green player loses the battle above, they will then basically get a free movement closer to the blue player’s base, making it easier for them to win. It also kind of makes sense that you wouldn’t be able to retreat toward an enemy’s base, as your units would then be going “through” or “past” the army you just lost to in order to retreat.
So, to fix this issue, I want to change it so that a losing player can only retreat their units to a land tile that is closer to their own base or at least has the same X position value as the battle site. This means the player would be able to retreat to the two tiles “behind” the battle site, toward their own base, and to the two tiles above and below the battle site. They could not retreat to the tiles “in front of” or closer to the enemy base. I crudely illustrated this for the above scenario by putting a check mark to tiles the green player could retreat to, and an X where they could not.

First, to code all this out, I will want a way to track where each player’s base is to calculate if they are retreating toward or away from their own base. In GamePlayer.cs, there is a SyncVar for a GameObject called myPlayerBase that stores the player’s base GameObject.
[SyncVar] public GameObject myPlayerBase;
However, GameObject’s don’t sync too well over the network, so I’m going to add a new Vector3 SyncVar called myPlayerBasePosition:
[SyncVar] public Vector3 myPlayerBasePosition;

Then, myPlayerBasePosition will be set by the server in the CmdSetPlayerBase command function in GamePlayer.cs with the following line:
requestingPlayer.myPlayerBasePosition = playerBase.transform.position;

If you save GamePlayer.cs and build and run the game, you should see the Vector3 coordinates for each player’s base set by the server and synced to the clients.

Now, to use myPlayerBasePosition to determine where player’s can retreat, the following code is added to CanPlayerRetreatFromBattle in GamePlayer.cs:
// Check where the units are in relation to the requesting player's base. Player's will only be able to retreat toward their own base, or to a tile with the same x value as the battle site
// If the base's x position is greater than the unit's x position, the player is player 2 and their base is to their right
// If it is less than the unit's position, the player is player 1 and the base is to the left
bool isLandMovingAwayFromPlayerBase = false;
if (retreatingPlayer.myPlayerBasePosition.x > battleSiteLand.transform.position.x)
{
Debug.Log(retreatingPlayer.PlayerName + " must retreat to the RIGHT");
if (landToRetreatTo.gameObject.transform.position.x < battleSiteLand.transform.position.x)
{
Debug.Log(landToRetreatTo.gameObject + " is to the LEFT of battle site " + battleSiteLand + ". " + retreatingPlayer.PlayerName + " CANNOT RETREAT HERE.");
isLandMovingAwayFromPlayerBase = true;
}
}
else if (retreatingPlayer.myPlayerBasePosition.x < battleSiteLand.transform.position.x)
{
Debug.Log(retreatingPlayer.PlayerName + " must retreat to the LEFT");
if (landToRetreatTo.gameObject.transform.position.x > battleSiteLand.transform.position.x)
{
Debug.Log(landToRetreatTo.gameObject + " is to the RIGHT of battle site " + battleSiteLand + ". " + retreatingPlayer.PlayerName + " CANNOT RETREAT HERE.");
isLandMovingAwayFromPlayerBase = true;
}
}
else if (retreatingPlayer.myPlayerBasePosition.x == battleSiteLand.transform.position.x)
{
Debug.Log(retreatingPlayer.PlayerName + " is on their own base. Cannot retreat from own base.");
isLandMovingAwayFromPlayerBase = true;
}
if (!isLandMovingAwayFromPlayerBase)
{
Debug.Log("No enemy units AND available space to retreat to AND toward player base " + landToRetreatTo.gameObject);
landWithNoEnemies.Add(landToRetreatTo);
numberAvailableToRetreat += 5 - landToRetreatTo.UnitNetIdsAndPlayerNumber.Count;
}

This new code does the following:
- This is all done after the checks for whether any enemy units are on the land and that there is at least one “space” left for a unit
- First, a boolean isLandMovingAwayFromPlayerBase will be used to track what direction the player is retreating
- Where the player’s base is in relation to the player’s unit will be calculated. If the x position of the base is greater than the battle site’s x position, then their base is to the right of the battle site. If the base x position is less, the base is to the left of the battle site
- If the player’s base is to the right of the battle site, the player can only retreat to the right. So, if the x value of the landToRetreatTo land tile is less than (and not equal) to the x position of the battle site, set isLandMovingAwayFromPlayerBase to true
- If the player’s base is to the left, the player can only retreat left. If the landToRetreatTo’s x position is greater than the battle site’s x position, set isLandMovingAwayFromPlayerBase to true
- After the checks for if the player is retreating left or right, then check if isLandMovingAwayFromPlayerBase is false. Only if it is false will the land be calculated as one the player can retreat to
So, now, to test this out, I set out the following scenario to see if unit’s are lost correctly due to there not being enough space to retreat to. The battle looks like this:

If the blue player loses, they shouldn’t have anywhere to retreat, and should lose all their units. The only tiles that don’t have enemies and aren’t full are to the left of the battle site, which is away from their base, which means they should not be able to retreat there.
After completing the battle so that the blue player should have defending all the green player’s attacking power, they still lost all their units due to the retreating restrictions

So, it works! Great!
Transition to Retreat Units Phase
When a battle ends and a player needs to retreat their units, right now the game just simply says “Retreat Units” at the top. To make a real transition to the Retreat Units phase, a few things will need to be added:
- Start the Retreat Units UI
- Will need to include something to show which player does and does not need to retreat
- Make it so Units are clickable and can be selected to move
- This will only be true for players that do need to retreat
The Retreat Units UI
To create the Retreat Units UI, I first made a duplicate of the UnitMovementUI panel in the Gameplay scene.

I then renamed the duplicated panel to RetreatUnitsPanel, and then deleted all of the child objects except for NoUnitsMoved, endUnitMovementButton, and ResetAllMovementButton

The remain children were renamed to the following:
- DoesPlayerNeedToRetreat
- endUnitsRetreat
- ResetAllRetreatingUnits

Select the DoesPlayerNeedToRetreat text object and set its Rect Transform values to the following:

You can delete all the “text” in DoesPlayerNeedToRetreat if you want. The actual text will be set by the GameplayManager.cs script, so it doesn’t matter what’s in it right now.
For the endUnitsRetreating button, select its Text child object and set the text to Done Retreating.

Then, for ResetAllRetreatingUnits, set its Rect Transform to the following:

In the hierarchy, deactivate the endUnitsRetreat and ResetAllRetreatingUnits buttons, then deactivate the RetreatUnitsPanel object.
In GameplayManager.cs, add the following variables to the top of the script:
[Header("Retreat Units UI/Info")]
[SerializeField] private GameObject RetreatUnitsPanel;
[SerializeField] private GameObject endRetreatUnitsButton;
[SerializeField] private Text doesPlayerNeedToRetreatText;
[SerializeField] private GameObject resetRetreatingUnitsbutton;
public bool haveUnitsRetreated = false;

These new variables will be used to store the UI objects so they can be interacted with in the script, and the haveUnitsRetreated variable will be used to locally track when all units required to retreat have retreated.
Save GameplayManager.cs, and then add the RetreatUnits UI objects to the GameplayManager object in the Unit Editor.

Activating the Retreat Units UI
In GameplayManager.cs, the ActivateRetreatUnitsUI function will be modified to the following:
void ActivateRetreatUnitsUI()
{
if (UnitMovementUI.activeInHierarchy)
UnitMovementUI.SetActive(false);
if (BattlesDetectedPanel.activeInHierarchy)
BattlesDetectedPanel.SetActive(false);
if (ChooseCardsPanel.activeInHierarchy)
ChooseCardsPanel.SetActive(false);
if (BattleResultsPanel.activeInHierarchy)
BattleResultsPanel.SetActive(false);
if (!RetreatUnitsPanel.activeInHierarchy)
RetreatUnitsPanel.SetActive(true);
//reset the camera size for unit movement
Camera.main.orthographicSize = 7;
Vector3 cameraPosition = new Vector3(-1.5f, 1.5f, -10f);
Camera.main.transform.position = cameraPosition;
// Move buttons to the RetreatUnitsPanel
hidePlayerHandButton.transform.SetParent(RetreatUnitsPanel.GetComponent<RectTransform>(), false);
showPlayerHandButton.transform.SetParent(RetreatUnitsPanel.GetComponent<RectTransform>(), false);
showPlayerHandButton.GetComponentInChildren<Text>().text = "Cards in Hand";
showPlayerDiscardButton.transform.SetParent(RetreatUnitsPanel.GetComponent<RectTransform>(), false);
showOpponentCardButton.transform.SetParent(RetreatUnitsPanel.GetComponent<RectTransform>(), false);
hideOpponentCardButton.transform.SetParent(RetreatUnitsPanel.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 (resetRetreatingUnitsbutton.activeInHierarchy)
resetRetreatingUnitsbutton.SetActive(false);
if (endRetreatUnitsButton.activeInHierarchy)
endRetreatUnitsButton.SetActive(false);
endRetreatUnitsButton.GetComponentInChildren<Text>().text = "Done Retreating";
if (LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().isPlayerViewingTheirHand)
{
LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().HidePlayerHandOnScreen();
}
if (opponentHandButtons.Count > 0)
{
foreach (GameObject opponentHandButton in opponentHandButtons)
{
opponentHandButton.transform.SetParent(RetreatUnitsPanel.GetComponent<RectTransform>(), false);
opponentHandButton.SetActive(false);
}
}
if (isPlayerViewingOpponentHand && playerHandBeingViewed != null)
{
playerHandBeingViewed.GetComponent<PlayerHand>().HidePlayerHandOnScreen();
playerHandBeingViewed = null;
isPlayerViewingOpponentHand = false;
}
}

ActivateRetreatUnitsUI just makes sure to go through all the various UI panels and objects and activates and deactivates as required for the Retreat Units phase.
Two new functions will need to be created in GameplayManager.cs to complete the transition to the Retreat Units phase. The first that will be created is called CheckIfPlayerNeedsToRetreat. The code for CheckIfPlayerNeedsToRetreat is shown below.
void CheckIfPlayerNeedsToRetreat()
{
if (LocalGamePlayerScript.ConnectionId == loserOfBattlePlayerConnId && LocalGamePlayerScript.PlayerName == loserOfBattleName && LocalGamePlayerScript.playerNumber == loserOfBattlePlayerNumber)
{
Debug.Log("Local player needs to retreat. Reason: Player lost battle");
doesPlayerNeedToRetreatText.text = "Retreat Your Units";
MouseClickManager.instance.canSelectUnitsInThisPhase = true;
}
else if (reasonForWinning == "Draw: No Winner")
{
Debug.Log("Local player needs to retreat. Reason: Battle was a draw. Both players retreat");
doesPlayerNeedToRetreatText.text = "Retreat Your Units";
MouseClickManager.instance.canSelectUnitsInThisPhase = true;
}
else
{
Debug.Log("Local player DOES NOT need to retreat. Player did not lose and there was no draw");
MouseClickManager.instance.canSelectUnitsInThisPhase = false;
doesPlayerNeedToRetreatText.text = loserOfBattleName;
doesPlayerNeedToRetreatText.text += " retreating";
ChangePlayerReadyStatus();
}
}

As the name of the function implies, CheckIfPlayerNeedsToRetreat checks to see if the local player needs to retreat. If the local player does, some UI changes are made, and it also sets MouseClickManager’s canSelectUnitsInThisPhase variable to true, which will allow for player’s to click on units. If the battle was a draw, it will also let the player click on their units. If the player did not lose and the battle was not a draw, then the player cannot click on their units and their Ready status will be set to “true” by calling ChangePlayerReadyStatus.
The next function, UnHideUnitsOnMap, will be created next. The code is shown below.
void UnHideUnitsOnMap()
{
GameObject allLand = GameObject.FindGameObjectWithTag("LandHolder");
foreach (Transform landObject in allLand.transform)
{
LandScript landScript = landObject.gameObject.GetComponent<LandScript>();
landScript.UnHideUnitText();
landScript.UnHideBattleHighlight();
if (landScript.UnitNetIdsAndPlayerNumber.Count > 0 && landObject.gameObject.GetComponent<NetworkIdentity>().netId != currentBattleSite)
{
foreach (KeyValuePair<uint, int> unitOnLand in landScript.UnitNetIdsAndPlayerNumber)
{
GameObject unitObject = NetworkIdentity.spawned[unitOnLand.Key].gameObject;
if (!unitObject.activeInHierarchy)
unitObject.SetActive(true);
}
}
}
}

UnHideUnitsOnMap is similar, but not the same as, ShowUnitsOnMap. ShowUnitsOnMap is specific to unhiding units around the battle site, and UnHideUnitsOnMap unhides all units in the game. I could have probably modified ShowUnitsOnMap to make it more generic and work for both situations, but I was lazy and decided not to. Anyway, UnHideUnitsOnMap goes through every land object in the game, unhides any unit text or battle highlights, and then if there are any units on the land object, makes sure that the unit is active in the hierarchy.
The last thing, then, is to make sure both CheckIfPlayerNeedsToRetreat and UnHideUnitsOnMap are called from StartRetreatUnits.

Save GameplayManager.cs, save the Gameplay scene in the Unity Editor, then build and run to test. The game should look like this when you need to retreat your units:

And like this when you don’t need to retreat your units:

Note that the non-retreating player is already marked as “ready”, if you are retreating it says “Retreat your units,” and if you are not it tells you which player is retreating.
Unit Movement in Retreat Units
Now the retreating player will need to be able to actually move their units and retreat them. The first thing they will need to do is be able to click on their units. CheckIfPlayerNeedsToRetreat will make the units clickable, but if you test it out in your game right now, you will see that the game behaves quite strangely when you click on a unit.

The units above were “expanded” because they are on the same land object as the unit that was clicked on. In the Retreat Units phase, this shouldn’t happen when the player clicks on a unit that is on the battle site. To prevent this from happening, MouseClickManager.cs will need to be modified.
In MouseClickManager.cs, there is a check made that allows a unit to be clicked on and “selected” that will cause the unit expansion to happen. This check will be modified so it doesn’t execute during Retreat Units.
if (rayHitUnit.collider.gameObject.GetComponent<NetworkIdentity>().hasAuthority && !playerViewingHand && !playerViewingOpponentHand && !playerReadyForNextPhase && canSelectUnitsInThisPhase && GameplayManager.instance.currentGamePhase != "Retreat Units")
Then, a new else if check will be added to be executed only if the current phase is Retreat units.
else if (rayHitUnit.collider.gameObject.GetComponent<NetworkIdentity>().hasAuthority && !playerViewingHand && !playerViewingOpponentHand && !playerReadyForNextPhase && canSelectUnitsInThisPhase && GameplayManager.instance.currentGamePhase == "Retreat Units")
Under that check will be the following code:
else if (rayHitUnit.collider.gameObject.GetComponent<NetworkIdentity>().hasAuthority && !playerViewingHand && !playerViewingOpponentHand && !playerReadyForNextPhase && canSelectUnitsInThisPhase && GameplayManager.instance.currentGamePhase == "Retreat Units")
{
//Unit movement specifically for retreating units
//only allow player to select unit that has to retreat
if (LocalGamePlayerScript.playerArmyNetIds.Contains(rayHitUnit.collider.gameObject.GetComponent<NetworkIdentity>().netId))
{
UnitScript unitScript = rayHitUnit.collider.GetComponent<UnitScript>();
if (!unitScript.currentlySelected)
{
Debug.Log("Selecting a new unit.");
unitsSelected.Add(rayHitUnit.collider.gameObject);
unitScript.currentlySelected = !unitScript.currentlySelected;
unitScript.ClickedOn();
if (unitScript.currentLandOccupied != null && unitScript.currentLandOccupied.GetComponent<NetworkIdentity>().netId != GameplayManager.instance.currentBattleSite)
{
LandScript landScript = unitScript.currentLandOccupied.GetComponent<LandScript>();
landScript.HighlightLandArea();
}
}
else
{
unitsSelected.Remove(rayHitUnit.collider.gameObject);
Debug.Log("Deselecting the unit unit.");
unitScript.currentlySelected = !unitScript.currentlySelected;
unitScript.ClickedOn();
unitScript.CheckLandForRemainingSelectedUnits();
}
}
else
{
Debug.Log("Player clicked on unit that doesn't have to retreat.");
}
}

The above check does the following:
- Checks to see if the unit that was clicked on is in the player’s “army” by check if its net id is in the playerArmyNetIds list. If the unit is:
- Select the unit if it is not selected, deselect the unit if is already selected and the player clicked on it again
- Check to see if the unit’s “currentLandOccupied” is the current battle site. If it IS NOT the battle site, highlight the land object
- This should also prevent the “unit expansion” on the battle site, because LandScript’s ExpandUnits function is called from HighlightLandArea
- If the unit is not in their army list, don’t select it or run anything
So, the above will rely on the player’s playerArmyNetIds to determine what units were in their army and need to retreat. This will require then that playerArmyNetIds is an accurate list of the player’s units. Right now, though, playerArmyNetIds will still contain the net ids of units that were destroy in the battle. So, back in GamePlayer.cs, the DestroyUnitsLostInBattle function will need to be updated to include the following:
if (gamePlayer.playerArmyNetIds.Contains(unitNetId))
gamePlayer.playerArmyNetIds.Remove(unitNetId);

Now, when the unit is destroyed by the server, it will also be removed from the player’s army list. If you build and run now, you should be able to select units on the battle site and see that they aren’t being “expanded” weirdly:

Validate Unit Movement for Retreating Units
Now, the movement for a retreating unit will need to be validated by the server. This will be done in UnitScript.cs, specifically in the CmdServerCanUnitsMove function.
First, there is one issue that I need to fix that I only noticed recently. In the check for how many units are being moved in a single request, I had CmdServerCanUnitsMove count the number of units selected and then also add any units already on the land the user clicked to the count. The issue is that if one of the units a player selected is already on the land they clicked, then that unit would be double counted against the total. So, a check is added to make sure only units already on the land that are not part of the units selected is added to the total. The new code is shown below:
foreach (uint unitId in landScript.UnitNetIdsOnLand)
{
if (requestingPlayer.playerUnitNetIds.Contains(unitId))
{
// make sure that units selected already on the land clicked aren't counted twice against the "total units to move" count
if (!unitsSelected.Contains(NetworkIdentity.spawned[unitId].gameObject))
totalUnitsToMove++;
}
}

Next will be the actual checks for the Retreat Units phase. A new check for Retret Units will be added between the Unit Placement and Unit Movement checks. The code is shown below:
if (GameplayManager.instance.currentGamePhase == "Retreat Units")
{
// Check where the units are in relation to the requesting player's base. Player's will only be able to retreat toward their own base, or to a tile with the same x value as the battle site
// If the base's x position is greater than the unit's x position, the player is player 2 and their base is to their right
// If it is less than the unit's position, the player is player 1 and the base is to the left
UnitScript firstUnitScript = unitsSelected[0].GetComponent<UnitScript>();
if (requestingPlayer.myPlayerBasePosition.x > firstUnitScript.startingPosition.x)
{
Debug.Log("Requesting player: " + requestingPlayer.PlayerName + "'s base is to their RIGHT. They can only retreat to the RIGHT.");
if (landUserClicked.transform.position.x < firstUnitScript.startingPosition.x)
{
Debug.Log("Requesting player: " + requestingPlayer.PlayerName + " clicked on a land to the left of the battle site. Cannot retreat that direction.");
TargetReturnCanUnitsMove(connectionToClient, false, landUserClicked, positionToMoveTo);
return;
}
}
else if (requestingPlayer.myPlayerBasePosition.x < firstUnitScript.startingPosition.x)
{
Debug.Log("Requesting player: " + requestingPlayer.PlayerName + "'s base is to their LEFT. They can only retreat to the LEFT.");
if (landUserClicked.transform.position.x > firstUnitScript.startingPosition.x)
{
Debug.Log("Requesting player: " + requestingPlayer.PlayerName + " clicked on a land to the left of the battle site. Cannot retreat that direction.");
TargetReturnCanUnitsMove(connectionToClient, false, landUserClicked, positionToMoveTo);
return;
}
}
else if (requestingPlayer.myPlayerBasePosition.x == firstUnitScript.startingPosition.x)
{
Debug.Log("Requesting player: " + requestingPlayer.PlayerName + "is retreating from their base? Player cannot retreat from their base.");
TargetReturnCanUnitsMove(connectionToClient, false, landUserClicked, positionToMoveTo);
return;
}
// make sure retreating player is not retreating to a land tile that has an enemy on it
if (landUserClicked.GetComponent<NetworkIdentity>().netId != GameplayManager.instance.currentBattleSite)
{
foreach (KeyValuePair<uint, int> unitsAndPlayer in landScript.UnitNetIdsAndPlayerNumber)
{
if (unitsAndPlayer.Value != requestingPlayer.playerNumber)
{
if (GameplayManager.instance.reasonForWinning.StartsWith("Draw:"))
{
UnitScript unitOnLandScript = NetworkIdentity.spawned[unitsAndPlayer.Key].gameObject.GetComponent<UnitScript>();
if (unitOnLandScript.startingPosition != landUserClicked.transform.position)
{
Debug.Log("CmdServerCanUnitsMove: Enemy unit on land BUT that unit retreated here. Allowing move.");
}
else if (unitOnLandScript.startingPosition == landUserClicked.transform.position)
{
Debug.Log("CmdServerCanUnitsMove: enemy unit on land AND that unit started here. Movement denied.");
TargetReturnCanUnitsMove(connectionToClient, false, landUserClicked, positionToMoveTo);
return;
}
}
else
{
Debug.Log("CmdServerCanUnitsMove: Player cannot retreat here. This land has an opposing player's unit on it");
TargetReturnCanUnitsMove(connectionToClient, false, landUserClicked, positionToMoveTo);
return;
}
}
}
}
}

This new movement check does the following:
- Gets the x position of the unit select and determines if the player’s base is to the right or left of the unit
- If their base is to the right, the player can only retreat to the right. If the base is to the left, the player can only retreat to the left
- If the player is trying to move their unit in a direction away from their own base and instead toward the enemy base, the movement will be rejected and TargetReturnCanUnitsMove will be returned with “false”
- The game then checks to see if the player is moving to a land object that is not the current battle site. If the land object isn’t the current battle site, then:
- Goes through each unit and player keyvalue pair in the UnitNetIdsAndPlayerNumber dictionary from the clicked on land
- If a unit is discovered on the land that does not belong to the requesting player, the following checks are then done:
- If the battle DID NOT end in a draw, the move is rejected
- If the battle WAS a draw, the following checks are then made:
- If the enemy unit that was on the land has that land object as its starting position, the move is rejected.
- If the enemy unit that is on the land has a different starting position than the land’s position, the move will be allowed
- This is allowed because if the enemy unit’s starting position is different from the clicked on land, that means the enemy unit just retreated there. This is how the two players in a draw will be allowed to retreat to the same land tile. If they do, later a function will be created to check if a new battle needs to be created
The next thing to be done is to change the Unit Movement check to also include the Retreat Units phase. This will be what checks to make sure the unit is only retreating 1 land tile away from the battle site.
if (GameplayManager.instance.currentGamePhase == "Unit Movement" || GameplayManager.instance.currentGamePhase == "Retreat Units")
If you save everything and build and run, you should see the unit movement “work” when retreating:

And I put “work” in scare quotes because if you do something like try to move back to the battle site, as the ResetAllRetreatingUnits button will do later, you will see some weird stuff like this:

So, that will need to be fixed! The units on the battle site should collapse like that, and the unit text shouldn’t be generated on the battle site during Retreat Units.
Reseting Units on the Battle Site
In addition to the above issue of units getting a little “messed up” and collapsed down when a retreating unit moves back to the battle site, after a battle ends where units were destroyed, some units can be left kind of “hanging” off the edge of the battle site, like this:

What I want instead is for those two blue units to be rearranged back on the battle site after units are killed. In LandScript.cs, a new function called RearrangeUnitsAfterTheyAreKilledFromBattle will be created. Code shown below:
public void RearrangeUnitsAfterTheyAreKilledFromBattle(int playerNumber)
{
Debug.Log("Executing RearrangeUnitsAfterTheyAreKilledFromBattle");
if (playerNumber == 1)
{
if (Player1Inf.Count > 0)
{
Vector3 temp = this.transform.position;
temp.y -= 0.5f;
temp.x -= 0.5f;
foreach (GameObject inf in Player1Inf)
inf.transform.position = temp;
}
if (Player1Tank.Count > 0)
{
Vector3 temp = this.transform.position;
temp.y += 0.5f;
temp.x -= 0.7f;
foreach (GameObject tank in Player1Tank)
tank.transform.position = temp;
}
ExpandUnitsForBattleResults(Player1Tank, Player1Inf, -1);
}
else if (playerNumber == 2)
{
if (Player2Inf.Count > 0)
{
Vector3 temp = this.transform.position;
temp.y -= 0.5f;
temp.x += 0.5f;
foreach (GameObject inf in Player2Inf)
inf.transform.position = temp;
}
if (Player2Tank.Count > 0)
{
Vector3 temp = this.transform.position;
temp.y += 0.5f;
temp.x += 0.7f;
foreach (GameObject tank in Player2Tank)
tank.transform.position = temp;
}
ExpandUnitsForBattleResults(Player2Tank, Player2Inf, 1);
}
else if (playerNumber == -1)
{
if (Player1Inf.Count > 0)
{
Vector3 temp = this.transform.position;
temp.y -= 0.5f;
temp.x -= 0.5f;
foreach (GameObject inf in Player1Inf)
inf.transform.position = temp;
}
if (Player1Tank.Count > 0)
{
Vector3 temp = this.transform.position;
temp.y += 0.5f;
temp.x -= 0.7f;
foreach (GameObject tank in Player1Tank)
tank.transform.position = temp;
}
if (Player2Inf.Count > 0)
{
Vector3 temp = this.transform.position;
temp.y -= 0.5f;
temp.x += 0.5f;
foreach (GameObject inf in Player2Inf)
inf.transform.position = temp;
}
if (Player2Tank.Count > 0)
{
Vector3 temp = this.transform.position;
temp.y += 0.5f;
temp.x += 0.7f;
foreach (GameObject tank in Player2Tank)
tank.transform.position = temp;
}
ExpandUnitsForBattleResults(Player1Tank, Player1Inf, -1);
ExpandUnitsForBattleResults(Player2Tank, Player2Inf, 1);
}
UpdateUnitText();
}

Using a player number as an argument, the tanks and infantry in any PlayerXInf and PlayerXTank lists will be reset back to “Default” position, and then ExpandUnitsForBattleResults will be called to re-expand them out for the battle site. Then, UpdateUnitText will be called to make sure all the unit text is correct. UpdateUnitText will also be updated to hide any unit texts that exist on the land tile if it is the “Choose Card” phase. This check is mostly to resolve some issues I was having on the server’s game.
if (GameplayManager.instance.currentGamePhase.StartsWith("Choose Card"))
{
if (infText != null)
{
infText.SetActive(false);
}
if (tankText != null)
{
tankText.SetActive(false);
}
}

Since RearrangeUnitsAfterTheyAreKilledFromBattle will rely on units in the PlayerX list’s, those lists will need to be updated to remove any units that are destroyed. Currently, the destroyed units remain in those lists after a battle. So, now, in UnitScript.cs, a OnDestroy function will be added to remove those units from the PlayerX lists if they exist in them. Once the destroyed unit is removed from the list, RearrangeUnitsAfterTheyAreKilledFromBattle from LandScript.cs will be called.
private void OnDestroy()
{
if (currentLandOccupied)
{
LandScript landScript = currentLandOccupied.GetComponent<LandScript>();
if (landScript.tanksOnLand.Contains(this.gameObject))
landScript.tanksOnLand.Remove(this.gameObject);
if (landScript.infantryOnLand.Contains(this.gameObject))
landScript.infantryOnLand.Remove(this.gameObject);
if (landScript.Player1Inf.Contains(this.gameObject))
landScript.Player1Inf.Remove(this.gameObject);
if (landScript.Player1Tank.Contains(this.gameObject))
landScript.Player1Tank.Remove(this.gameObject);
if (landScript.Player2Inf.Contains(this.gameObject))
landScript.Player2Inf.Remove(this.gameObject);
if (landScript.Player2Tank.Contains(this.gameObject))
landScript.Player2Tank.Remove(this.gameObject);
landScript.RearrangeUnitsAfterTheyAreKilledFromBattle(GameplayManager.instance.loserOfBattlePlayerNumber);
}
}

There will also be some other cleanup down in LandScript.cs. In MoveUnitsForBattleSite, the PlayerX lists will first be cleared out before they are populated.
//Clear out any old data
Player1Inf.Clear();
Player1Tank.Clear();
Player2Inf.Clear();
Player2Tank.Clear();

Also in MoveUnitsForBattleSite, the newPosition Vector3 variable will be set to the land object’s transform.position, and the y value offset will be set for each unit type (-0.5 for infantry, +0.5 for tanks)

Then, in UnitTextForBattles, any old BattleUnitText objects will be cleared out when they are first created:
if (BattleUnitTexts.Count > 0)
{
foreach (KeyValuePair<GameObject, int> text in BattleUnitTexts)
{
GameObject textToDestroy = text.Key;
Destroy(textToDestroy);
textToDestroy = null;
}
BattleUnitTexts.Clear();
}

If you save everything and create a battle result where units are destroyed, like this:

You should then see them rearranged during retreat units, like this:

And the battle lists will be updated appropriately on the land object:

Remove/Add Unit From Battle Lists When They Retreat
So, removing units from the battle lists is important on the battle sites. Now, when a unit retreats away from a battle site, they should be removed from that land’s battle lists. Also, if the unit moves back to the battle site, they should be added back to the list.
This will be done in UnitScript.cs’s UpdateUnitLandObject function. The following two lines:
currentLandOccupied.GetComponent<LandScript>().infantryOnLand.Remove(gameObject);
currentLandOccupied.GetComponent<LandScript>().UpdateUnitText();
will be changed to the following:
if (GameplayManager.instance.currentGamePhase == "Retreat Units" && currentLandOccupied.GetComponent<NetworkIdentity>().netId == GameplayManager.instance.currentBattleSite)
{
Debug.Log("Will remove unit from the battle sites army");
currentLandOccupiedScript.infantryOnLand.Remove(gameObject);
if (currentLandOccupiedScript.Player1Inf.Contains(this.gameObject))
currentLandOccupiedScript.Player1Inf.Remove(this.gameObject);
if (currentLandOccupiedScript.Player2Inf.Contains(this.gameObject))
currentLandOccupiedScript.Player2Inf.Remove(this.gameObject);
}
else
{
Debug.Log("Removing unit. Not moving away from or to the current battle site");
currentLandOccupiedScript.infantryOnLand.Remove(gameObject);
currentLandOccupiedScript.UpdateUnitText();
}

This new check does the following:
- If the current phase is Retreat Units, and the unit’s “currentOccupiedLand” is the battle site (meaning they are moving away from the battle site), do the following:
- Remove the infantry from the infantry on land lsit
- remove the infantry from any Battle lists
- If the above is not true, move the unit as normal
The above difference accomplishes removing the unit from the battle lists and also makes sure that UpdateUnitText isn’t called on the battle site, which causes issues during Retreat Units.
Next, the following check:
if (landScript.infantryOnLand.Count > 1)
{
landScript.MultipleUnitsUIText("infantry");
Debug.Log("More than 1 infantry on land");
}
Will be replaced with:
if (GameplayManager.instance.currentGamePhase != "Retreat Units" || LandToMoveTo.GetComponent<NetworkIdentity>().netId != GameplayManager.instance.currentBattleSite)
{
Debug.Log("Unit NOT moving back to battle site during retreat.");
if (landScript.infantryOnLand.Count > 1)
{
landScript.MultipleUnitsUIText("infantry");
Debug.Log("More than 1 infantry on land");
}
}
else if (GameplayManager.instance.currentGamePhase == "Retreat Units" && LandToMoveTo.GetComponent<NetworkIdentity>().netId == GameplayManager.instance.currentBattleSite)
{
Debug.Log("Unit moved back to battle site during retreat");
if (ownerPlayerNumber == 1)
landScript.Player1Inf.Add(this.gameObject);
if (ownerPlayerNumber == 2)
landScript.Player2Inf.Add(this.gameObject);
landScript.RearrangeUnitsAfterTheyAreKilledFromBattle(GameplayManager.instance.loserOfBattlePlayerNumber);
}

First, the above code checks if it is Retreat Units and the player is NOT moving to the current battle site. If they aren’t, the normal calls to MultipleUnitsUIText to display unit text is called.
If the unit IS moving to the current battle site (moving back to the battle site), then the infantry’s onwer’s playernumber is checked and they are added to the appropriate battle list.
The same will then need to be replicated for the “tank” section of UpdateUnitLandObject. The code for the whole function is shown below:
public void UpdateUnitLandObject(GameObject LandToMoveTo)
{
LandScript landScript = LandToMoveTo.GetComponent<LandScript>();
if (currentLandOccupied != LandToMoveTo)
{
//Current land tile should only be null when the game is first started and the unit hasn't been "assigned" a land tile yet
if (currentLandOccupied == null)
{
currentLandOccupied = LandToMoveTo;
}
Debug.Log("Unit moved to new land");
if (currentLandOccupied != null)
{
LandScript currentLandOccupiedScript = currentLandOccupied.GetComponent<LandScript>();
if (gameObject.tag == "infantry")
{
//Remove unit from previous land tile
Debug.Log("Removed infantry from previous land object at: " + currentLandOccupied.transform.position.x.ToString() + "," + currentLandOccupied.transform.position.y.ToString());
//currentLandOccupied.GetComponent<LandScript>().infantryOnLand.Remove(gameObject);
//currentLandOccupied.GetComponent<LandScript>().UpdateUnitText();
if (GameplayManager.instance.currentGamePhase == "Retreat Units" && currentLandOccupied.GetComponent<NetworkIdentity>().netId == GameplayManager.instance.currentBattleSite)
{
Debug.Log("Will remove unit from the battle sites army");
currentLandOccupiedScript.infantryOnLand.Remove(gameObject);
if (currentLandOccupiedScript.Player1Inf.Contains(this.gameObject))
currentLandOccupiedScript.Player1Inf.Remove(this.gameObject);
if (currentLandOccupiedScript.Player2Inf.Contains(this.gameObject))
currentLandOccupiedScript.Player2Inf.Remove(this.gameObject);
}
else
{
Debug.Log("Removing unit. Not moving away from or to the current battle site");
currentLandOccupiedScript.infantryOnLand.Remove(gameObject);
currentLandOccupiedScript.UpdateUnitText();
}
//Add Unit to new land tile
Debug.Log("Added infantry unit to land object at: " + LandToMoveTo.transform.position.x.ToString() + "," + LandToMoveTo.transform.position.y.ToString());
landScript.infantryOnLand.Add(gameObject);
if (GameplayManager.instance.currentGamePhase != "Retreat Units" || LandToMoveTo.GetComponent<NetworkIdentity>().netId != GameplayManager.instance.currentBattleSite)
{
Debug.Log("Unit NOT moving back to battle site during retreat.");
if (landScript.infantryOnLand.Count > 1)
{
landScript.MultipleUnitsUIText("infantry");
Debug.Log("More than 1 infantry on land");
}
}
else if (GameplayManager.instance.currentGamePhase == "Retreat Units" && LandToMoveTo.GetComponent<NetworkIdentity>().netId == GameplayManager.instance.currentBattleSite)
{
Debug.Log("Unit moved back to battle site during retreat");
if (ownerPlayerNumber == 1)
landScript.Player1Inf.Add(this.gameObject);
if (ownerPlayerNumber == 2)
landScript.Player2Inf.Add(this.gameObject);
landScript.RearrangeUnitsAfterTheyAreKilledFromBattle(GameplayManager.instance.loserOfBattlePlayerNumber);
}
}
else if (gameObject.tag == "tank")
{
//Remove unit from previous land tile
Debug.Log("Removed tank from previous land object at: " + currentLandOccupied.transform.position.x.ToString() + "," + currentLandOccupied.transform.position.y.ToString());
if (GameplayManager.instance.currentGamePhase == "Retreat Units" && currentLandOccupied.GetComponent<NetworkIdentity>().netId == GameplayManager.instance.currentBattleSite)
{
currentLandOccupiedScript.tanksOnLand.Remove(gameObject);
if (currentLandOccupiedScript.Player1Tank.Contains(this.gameObject))
currentLandOccupiedScript.Player1Tank.Remove(this.gameObject);
if (currentLandOccupiedScript.Player2Tank.Contains(this.gameObject))
currentLandOccupiedScript.Player2Tank.Remove(this.gameObject);
}
else
{
currentLandOccupiedScript.tanksOnLand.Remove(gameObject);
currentLandOccupiedScript.UpdateUnitText();
}
//Add Unit to new land tile
Debug.Log("Added infantry unit to land object at: " + LandToMoveTo.transform.position.x.ToString() + "," + LandToMoveTo.transform.position.y.ToString());
landScript.tanksOnLand.Add(gameObject);
if (GameplayManager.instance.currentGamePhase != "Retreat Units" || LandToMoveTo.GetComponent<NetworkIdentity>().netId != GameplayManager.instance.currentBattleSite)
{
if (landScript.tanksOnLand.Count > 1)
{
landScript.MultipleUnitsUIText("tank");
Debug.Log("More than 1 tank on land");
}
}
else if (GameplayManager.instance.currentGamePhase == "Retreat Units" && LandToMoveTo.GetComponent<NetworkIdentity>().netId == GameplayManager.instance.currentBattleSite)
{
Debug.Log("Unit moved back to battle site during retreat");
if (ownerPlayerNumber == 1)
landScript.Player1Tank.Add(this.gameObject);
if (ownerPlayerNumber == 2)
landScript.Player2Tank.Add(this.gameObject);
landScript.RearrangeUnitsAfterTheyAreKilledFromBattle(GameplayManager.instance.loserOfBattlePlayerNumber);
}
}
// Remove the land highlight when a unit moves
currentLandOccupied.GetComponent<LandScript>().RemoveHighlightLandArea();
}
float disFromCurrentLocation = Vector3.Distance(LandToMoveTo.transform.position, currentLandOccupied.transform.position);
Debug.Log("Unit moved distance of: " + disFromCurrentLocation.ToString("0.00"));
currentLandOccupied = LandToMoveTo;
}
else if (currentLandOccupied == LandToMoveTo && GameplayManager.instance.currentGamePhase == "Retreat Units" && LandToMoveTo.GetComponent<NetworkIdentity>().netId == GameplayManager.instance.currentBattleSite)
{
Debug.Log("Unit that was already on current battle site moved back to battle site again.");
landScript.RearrangeUnitsAfterTheyAreKilledFromBattle(GameplayManager.instance.loserOfBattlePlayerNumber);
}
}
At the end of UpdateUnitLandObject there is a check that does the following:
else if (currentLandOccupied == LandToMoveTo && GameplayManager.instance.currentGamePhase == "Retreat Units" && LandToMoveTo.GetComponent<NetworkIdentity>().netId == GameplayManager.instance.currentBattleSite)
{
Debug.Log("Unit that was already on current battle site moved back to battle site again.");
landScript.RearrangeUnitsAfterTheyAreKilledFromBattle(GameplayManager.instance.loserOfBattlePlayerNumber);
}
This just checks to see if a unit started on the battle site and is also “moved” back to the battle site. Basically, if the player selected many units, at least one of which was still on the battle site, and then clicked on the battle site. Without this check, the game will collapse the units down and it looks wrong. so, this makes sure that in that scenario RearrangeUnitsAfterTheyAreKilledFromBattle is called to keep the units expanded for Retreat Units. This will be important when the Reset Movement functionality is added to Retreat Units.
If you save, build and run, then test, you should see the units starting in the battle lists:

Then, if you move 1 tank and 1 inf as player 1, they should be removed from the lists:

And then if you only move the tank back, you should see it added back to the list:

Reset Retreating Units
In the RetreatUnitsPanel, the ResetAllRetreatingUnits button was created. This will be used to “reset” all the retreating units back to the battle site. Well, now it’s time to do that!
First, the ResetAllRetreatingUnits button will need to be activated in the game. To start things out, in MouseClickManager.cs, a new check will be added to the MoveAllUnits function for the Retreat Units phase.
if (GameplayManager.instance.currentGamePhase == "Retreat Units")
GameplayManager.instance.CheckIfUnitsHaveRetreated();

When units are moved, and it is the Retreat Units phase, the CheckIfUnitsHaveRetreated function in GameplayManager.cs will be called. So, now in GameplayManager.cs, the CheckIfUnitsHaveRetreated function will need to be created. Code shown below:
public void CheckIfUnitsHaveRetreated()
{
Debug.Log("Executing CheckIfUnitsHaveRetreated to check if at least one unit has retreated");
bool unitRetreated = false;
GameObject battleSite = NetworkIdentity.spawned[currentBattleSite].gameObject;
if (LocalGamePlayerScript.doesPlayerNeedToRetreat)
{
foreach (uint unitToRetreatNetId in LocalGamePlayerScript.playerArmyNetIds)
{
GameObject unitToRetreat = NetworkIdentity.spawned[unitToRetreatNetId].gameObject;
if (unitToRetreat.GetComponent<UnitScript>().currentLandOccupied != battleSite)
{
Debug.Log("CheckIfUnitsHaveRetreated: At least one unit has retreated off the battle site");
unitRetreated = true;
break;
}
}
if (unitRetreated)
{
haveUnitsRetreated = true;
if (!resetRetreatingUnitsbutton.activeInHierarchy)
resetRetreatingUnitsbutton.SetActive(true);
CheckIfAllUnitsHaveRetreated();
}
else
{
haveUnitsRetreated = false;
if (resetRetreatingUnitsbutton.activeInHierarchy)
resetRetreatingUnitsbutton.SetActive(false);
}
}
}

CheckIfUnitsHaveRetreated will first check if the local game played needs to retreat their units. If they do, all units in the player’s playerArmyNetIds list will be checked to see if they are on the current battle site or not. If at least one unit is no longer on the battle site, the resetRetreatingUnitsbutton is activated and CheckIfAllUnitsHaveRetreated is called.
CheckIfAllUnitsHaveRetreated doesn’t exist yet, so it will now be created. Code should below:
void CheckIfAllUnitsHaveRetreated()
{
bool unitNotRetreated = false;
GameObject battleSite = NetworkIdentity.spawned[currentBattleSite].gameObject;
if (LocalGamePlayerScript.doesPlayerNeedToRetreat)
{
foreach (uint unitToRetreatNetId in LocalGamePlayerScript.playerArmyNetIds)
{
GameObject unitToRetreat = NetworkIdentity.spawned[unitToRetreatNetId].gameObject;
if (unitToRetreat.GetComponent<UnitScript>().currentLandOccupied == battleSite)
{
Debug.Log("CheckIfAllUnitsHaveRetreated: At least one unit HAS NOT retreated off the battle site");
unitNotRetreated = true;
break;
}
}
if (unitNotRetreated)
{
Debug.Log("CheckIfAllUnitsHaveRetreated: At least one unit has NOT retreated");
if (endRetreatUnitsButton.activeInHierarchy)
endRetreatUnitsButton.SetActive(false);
}
else
{
Debug.Log("CheckIfAllUnitsHaveRetreated: All units for the local player have retreated!");
if (!endRetreatUnitsButton.activeInHierarchy)
endRetreatUnitsButton.SetActive(true);
}
}
}

CheckIfAllUnitsHaveRetreated is similar to, but not the same as, CheckIfUnitsHaveRetreated. As the difference in the names implies, CheckIfAllUnitsHaveRetreated checks if all units have retreated off the current battle site. If they have all retreated, the endRetreatUnitsButton is set to active.
Then, in UpdateReadyButton, a check for Retreat Units will be added to change the text of the endRetreatUnitsButton when the player is ready to end the Retreat Units phase. The player’s will only really see this in a draw scenario.
if (currentGamePhase == "Retreat Units")
{
if (LocalGamePlayerScript.ReadyForNextPhase)
{
Debug.Log("Local Player is ready to go to next phase.");
endRetreatUnitsButton.GetComponentInChildren<Text>().text = "Unready";
if (resetRetreatingUnitsbutton.activeInHierarchy)
resetRetreatingUnitsbutton.SetActive(false);
if (MouseClickManager.instance.unitsSelected.Count > 0)
MouseClickManager.instance.ClearUnitSelection();
}
else
{
Debug.Log("Local Player IS NOT ready to go to next phase.");
endRetreatUnitsButton.GetComponentInChildren<Text>().text = "Done Retreating";
if (haveUnitsRetreated)
{
resetRetreatingUnitsbutton.SetActive(true);
}
}
}

Make sure to save MouseClickManager.cs and GameplayManager.cs, then you should be able to build and run to test. When units are moved off the battle site, the reset units and done retreating buttons should all appear:

Now, when you click on Done Retreating, the Retret Units phase ends and… nothing happens!
Advancing Past the Retreat Units Phase
When the players are done retreting their units, a couple things will have to happen
- Check if there are any remaining battles
- If yes, move to the next battle to fight
- If no, go to the unit movement phase
- Cleanup and syncing from the battle that just completed:
- sync positions of units that just retreated
- remove battle site stuff like the highlight and number text
- reposition the units on the previous battle site that didn’t have to retreat
To get the ball rolling on this, a new check will be added to GamePlayer.cs’s CheckIfAllPlayersAreReadyForNextPhase server function. The code for that is shown below:
if (Game.CurrentGamePhase == "Retreat Units")
{
bool didAllUnitsRetreat = VerifyAllUnitsRetreated();
if (didAllUnitsRetreat)
{
Debug.Log("Server: All players retreated.");
CleanupPreviousBattleInfo();
bool moreBattles = CheckForMoreBattles();
if (moreBattles)
{
Debug.Log("At least one more battle to fight. Moving to the next battle.");
ChangeToNextBattle();
}
else
{
Game.CurrentGamePhase = "Unit Movement";
Debug.Log("Changing phase to Unit Movement");
RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
return;
}
}
RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
return;
}

There’s a bit going on here that hasn’t been created. The code does the following:
- Sets a bool value from the result of the VerifyAllUnitsRetreated
- if VerifyAllUnitsRetreated returns true, do the following:
- Call CleanupPreviousBattleInfo
- set a bool to the result of CheckForMoreBattles
- if CheckForMoreBattles returns true, call ChangeToNextBattle
- If false, set the next phase to unit movement
So, the following functions will now need to be created to support this:
- VerifyAllUnitsRetreated
- CleanupPreviousBattleInfo
- CheckForMoreBattles
- ChangeToNextBattle
VerifyAllUnitsRetreated
VerifyAllUnitsRetreated is a server function that will go through all units that needed to retreat and make sure that they did, in fact, retreat off the battle site. The code for that is shown below:
bool VerifyAllUnitsRetreated()
{
Debug.Log("Executing VerifyAllUnitsRetreated on the server.");
bool allUnitsRetreated = false;
GameObject battleSite = NetworkIdentity.spawned[GameplayManager.instance.currentBattleSite].gameObject;
foreach (GamePlayer gamePlayer in Game.GamePlayers)
{
if (gamePlayer.doesPlayerNeedToRetreat)
{
Debug.Log("VerifyAllUnitsRetreated: Retreating player found: " + gamePlayer.PlayerName);
foreach (uint unitToRetreatNetId in gamePlayer.playerArmyNetIds)
{
GameObject unitToRetreat = NetworkIdentity.spawned[unitToRetreatNetId].gameObject;
if (unitToRetreat.GetComponent<UnitScript>().newPosition == battleSite.transform.position)
{
Debug.Log("VerifyAllUnitsRetreated: At least one unit HAS NOT retreated off the battle site");
allUnitsRetreated = false;
break;
}
else
{
Debug.Log("VerifyAllUnitsRetreated: Unit has retreated from the battle site");
allUnitsRetreated = true;
}
}
if (!allUnitsRetreated)
{
Debug.Log("VerifyAllUnitsRetreated: At least one unit did not retreat. Returning false");
break;
}
}
}
return allUnitsRetreated;
}

VerifyAllUnitsRetreated finds the current battle site object. It then gets a list of all the players who need to retreat from their doesPlayerNeedToRetreat value. Then, it goes through each retreating player’s playerArmyNetIds list and makes sure that the unit is no longer on the battle site. If all units from all retreating players are NOT on the battle site, VerifyAllUnitsRetreated returns true.
CleanupPreviousBattleInfo
Next up is CleanupPreviousBattleInfo, which will cleanup some old info from the previous battle. The code is shown below:
[Server]
void CleanupPreviousBattleInfo()
{
//Remove the previous battle site from the list of battle sites based on its battle number
if (GameplayManager.instance.battleNumber != 0)
GameplayManager.instance.battleSiteNetIds.Remove(GameplayManager.instance.battleNumber);
RpcUpdatedUnitPositionsForBattleSites();
//RpcRemoveBattleHighlightAndBattleTextFromPreviousBattle(GameplayManager.instance.currentBattleSite);
GameplayManager.instance.RpcRemoveBattleHighlightAndBattleTextFromPreviousBattle(GameplayManager.instance.currentBattleSite);
GameplayManager.instance.HandleCurrentBattleSiteUpdate(GameplayManager.instance.currentBattleSite, 0);
}

- CleanupPreviousBattleInfo removes the current battle site/battle number from the battleSiteNetIds list in GameplayManager
- calls RpcUpdatedUnitPositionsForBattleSites on all clients to update unit positions
- Calls RpcRemoveBattleHighlightAndBattleTextFromPreviousBattle in GameplayManager to remove battle highlights and text
- then finally sets the currentBattleSite value in GameplayManager to 0
A couple of new functions will now need to be created.
RpcUpdatedUnitPositionsForBattleSites
RpcUpdatedUnitPositionsForBattleSites will start off a chain of functions to have the server update the updatedUnitPositionsForBattleSites value on each gameplayer to false. The code for all the functions is show below:
[ClientRpc]
void RpcUpdatedUnitPositionsForBattleSites()
{
GameObject LocalGamePlayer = GameObject.Find("LocalGamePlayer");
GamePlayer LocalGamePlayerScript = LocalGamePlayer.GetComponent<GamePlayer>();
LocalGamePlayerScript.ResetUpdatedUnitPositionsForBattleSites();
}
void ResetUpdatedUnitPositionsForBattleSites()
{
if (hasAuthority)
CmdResetUpdatedUnitPositionsForBattleSites();
}
[Command]
void CmdResetUpdatedUnitPositionsForBattleSites()
{
NetworkIdentity networkIdentity = connectionToClient.identity;
GamePlayer requestingPlayer = networkIdentity.GetComponent<GamePlayer>();
Debug.Log("Executing CmdResetUpdatedUnitPositionsForBattleSites for " + requestingPlayer.PlayerName);
requestingPlayer.updatedUnitPositionsForBattleSites = false;
}

The ultimate goal here was to update updatedUnitPositionsForBattleSites to false on all gameplayers. I couldn’t just set this as false by looping through each game player object on the server. Since updatedUnitPositionsForBattleSites is a syncvar with a hook function, it seemed like it could only be updated by having a player make a request to a server command function to update the syncvar value
So, RpcUpdatedUnitPositionsForBattleSites first finds the local game player object and then calls ResetUpdatedUnitPositionsForBattleSites on the local game player.
ResetUpdatedUnitPositionsForBattleSites then makes a request to the server to execute CmdResetUpdatedUnitPositionsForBattleSites.
On the server, CmdResetUpdatedUnitPositionsForBattleSites sets the value of updatedUnitPositionsForBattleSites for the requesting player to false.
RpcRemoveBattleHighlightAndBattleTextFromPreviousBattle
A new ClientRpc function will be added to GameplayManager.cs called RpcRemoveBattleHighlightAndBattleTextFromPreviousBattle. The code is shown below:
[ClientRpc]
public void RpcRemoveBattleHighlightAndBattleTextFromPreviousBattle(uint battleSiteNetId)
{
Debug.Log("RpcRemoveBattleHighlightAndBattleTextFromPreviousBattle: Instructing player to remove battle highlight and text on land with net id: " + battleSiteNetId.ToString());
LandScript battleSiteScript = NetworkIdentity.spawned[battleSiteNetId].gameObject.GetComponent<LandScript>();
battleSiteScript.RemoveBattleSiteHighlightAndText();
LocalGamePlayerScript.UpdateUnitPositions();
battleSiteScript.ResetUnitPositionAndUnitTextAfterBattle();
}

RpcRemoveBattleHighlightAndBattleTextFromPreviousBattle takes a uint argument that is the netid of the attle site. It then calls RemoveBattleSiteHighlightAndText on the battle site land object, UpdateUnitPositions on the local game player script, and then ResetUnitPositionAndUnitTextAfterBattle on the battle site.
RemoveBattleSiteHighlightAndText and ResetUnitPositionAndUnitTextAfterBattle now need to be created in LandScript.cs.
RemoveBattleSiteHighlightAndText is pretty simple. It just checks if the battle outline and battle number text exist. If either does, they are destroyed. Code shown below:
public void RemoveBattleSiteHighlightAndText()
{
if (battleOutlineObject)
{
Destroy(battleOutlineObject);
battleOutlineObject = null;
}
if (battleNumberTextObject)
{
Destroy(battleNumberTextObject);
battleNumberTextObject = null;
}
}

ResetUnitPositionAndUnitTextAfterBattle is a bit more involved. The code is shown below:
public void ResetUnitPositionAndUnitTextAfterBattle()
{
Debug.Log("Executing ResetUnitPositionAndUnitTextAfterBattle");
// Clear out old army info since the battle is over
Player1Inf.Clear();
Player1Tank.Clear();
Player2Inf.Clear();
Player2Tank.Clear();
//Destroy and clear out the BattleUnitTexts
foreach (KeyValuePair<GameObject, int> battleText in BattleUnitTexts)
{
Destroy(battleText.Key);
}
BattleUnitTexts.Clear();
//Reposition the units still on the land
if (tanksOnLand.Count > 0)
{
foreach (GameObject tank in tanksOnLand)
{
Vector3 temp = this.transform.position;
temp.y += 0.5f;
tank.transform.position = temp;
}
}
if (infantryOnLand.Count > 0)
{
foreach (GameObject inf in infantryOnLand)
{
Vector3 temp = this.transform.position;
temp.y -= 0.5f;
inf.transform.position = temp;
}
}
// Create unit texts if multiple units are on the land
if (infantryOnLand.Count > 1)
{
MultipleUnitsUIText("infantry");
if (GameplayManager.instance.battleSiteNetIds.Count > 0)
{
Debug.Log("ResetUnitPositionAndUnitTextAfterBattle: Hiding inf text because of other battles");
if (infText.activeInHierarchy)
infText.SetActive(false);
}
}
if (tanksOnLand.Count > 1)
{
MultipleUnitsUIText("tank");
if (GameplayManager.instance.battleSiteNetIds.Count > 0)
{
Debug.Log("ResetUnitPositionAndUnitTextAfterBattle: Hiding tank text because of other battles");
if (tankText.activeInHierarchy)
tankText.SetActive(false);
}
}
}

ResetUnitPositionAndUnitTextAfterBattle does the following:
- Clears out old battle lists
- If any battle unit texts exist, destroy them and then clear the list
- Goes through each tank and infantry object on the land and resets their position
- Checks to see if there are multiple tanks/infantry on the land. If yes, call MultipleUnitsUIText to create the unit text objects
- also deactivates the text objects in the hierarchy to not mess with how things are displayed during battles
I ran into some issues in how MultipleUnitsUIText was behaving on the server, so I added the following check to MultipleUnitsUIText. It made sure that unit text wouldn’t be displayed on the screen if the game moved to the next battle / choose cards phase:
if (GameplayManager.instance.currentGamePhase.StartsWith("Choose Card"))
{
if (infText != null)
{
infText.SetActive(false);
}
if (tankText != null)
{
tankText.SetActive(false);
}
}

HandleCurrentBattleSiteUpdate
The last change to make is in GameplayManager.cs’s HandleCurrentBattleSiteUpdate. CleanupPreviousBattleInfo is setting the currentBattleSite value to 0. Right now, HandleCurrentBattleSiteUpdate will take the new value of currentBattleSite and then call functions in the game that will try to find that battle site based on its network id. No object will have a network id of zero that the client can find, so HandleCurrentBattleSiteUpdate will need to be update to not do anything if the new value is 0.
public void HandleCurrentBattleSiteUpdate(uint oldValue, uint newValue)
{
if (isServer)
{
currentBattleSite = newValue;
}
if (isClient && newValue != 0)
{
ZoomOnBattleSite();
HideNonBattleUnits();
HideNonBattleLandTextAndHighlights();
SetGamePlayerArmy();
}
}

I was also having issues with the whole localZoomedInOnBattleSite check in ZoomOnBattleSite, so I just removed the check entirely. ZoomOnBattleSite will run twice on the server, probably, but at least it runs once on the client!
void ZoomOnBattleSite()
{
Debug.Log("Starting ZoomOnBattleSite for battle site with network id: " + currentBattleSite.ToString());
GameObject battleSite = NetworkIdentity.spawned[currentBattleSite].gameObject;
Vector3 newCameraPosition = battleSite.transform.position;
Camera.main.orthographicSize = 5f;
newCameraPosition.x += 2.15f;
newCameraPosition.z = -10f;
Camera.main.transform.position = newCameraPosition;
}
CheckForMoreBattles
The next function to add is CheckForMoreBattles, which as its name says, will check to see if there are any remaining battles to fight. The code is shown below:
[Server]
bool CheckForMoreBattles()
{
Debug.Log("Executing CheckForMoreBattles");
bool moreBattles = false;
if (GameplayManager.instance.battleSiteNetIds.Count > 0)
{
Debug.Log("CheckForMoreBattles: More battle remain to fight.");
moreBattles = true;
}
return moreBattles;
}

CheckForMoreBattles is pretty simple. Since CleanupPreviousBattleInfo has already removed the previous battle, CheckForMoreBattles just checks to see if the battleSiteNetIds list on GameplayManager is greater than 0. If it is, there are more battles to fight and CheckForMoreBattles returns true.
ChangeToNextBattle
If CheckForMoreBattles returns true, the server will then execute ChangeToNextBattle to move onto the next battle in the list. The code for ChangeToNextBattle is shown below:
[Server]
void ChangeToNextBattle()
{
Debug.Log("Executing ChangeToNextBattle on the server.");
GameplayManager.instance.battleNumber++;
if (GameplayManager.instance.battleSiteNetIds.ContainsKey(GameplayManager.instance.battleNumber))
{
GameplayManager.instance.HandleCurrentBattleSiteUpdate(GameplayManager.instance.currentBattleSite, GameplayManager.instance.battleSiteNetIds[GameplayManager.instance.battleNumber]);
Game.CurrentGamePhase = "Choose Cards:\nBattle #" + GameplayManager.instance.battleNumber.ToString();
Debug.Log("ChangeToNextBattle: Game phase changed to " + Game.CurrentGamePhase);
}
}

ChangeToNextBattle first increases the battleNumber value on GameplayManager by 1. It then makes sure to check that that battle exists in battleSiteNetIds, and if it does, updates the currentBattleSite value and sets the game phase to Choose Cards. Once that is done, the next battle will be fought in its change cards phase.
Actually Advancing the Phase
Now that the game phase is set to Choose Cards, the clients have to be told to take some actions to update the UI and everything to the Choose Cards phase. So, in GameplayManager.cs’s ChangeGamePhase function, the followig line:
if (currentGamePhase == "Battle(s) Detected" && newGamePhase.StartsWith("Choose Cards"))
will be changed to this:
if ((currentGamePhase == "Battle(s) Detected" || currentGamePhase == "Retreat Units") && newGamePhase.StartsWith("Choose Cards"))
So, if the previous phase was Battles Detected OR Retreat Units, the game will move onto the Choose Cards phase and activate the Choose Cards UI with StartChooseCards.
Updating Battle Scores in the Next Battle
When the game changes to the next battle, new battle scores will need to be displayed. First, though, those battle score panels will need to be moved back to the choose cards UI. This will be done in GameplayManager.cs’s CreateBattlePanels function. CreateBattlePanels will be updated so that it will destroy any old battle panels and create new ones:
if (localPlayerBattlePanel)
{
Debug.Log("CreateBattlePanels: Destroying local player battle panel");
foreach (Transform child in localPlayerBattlePanel.transform)
{
if (child.tag == "Card")
{
Debug.Log("Card object found as child of localPlayerBattlePanel. Removing as a child object.");
child.transform.parent = null;
}
}
Destroy(localPlayerBattlePanel);
localPlayerBattlePanel = null;
}
if (opponentPlayerBattlePanel)
{
Debug.Log("CreateBattlePanels: Destroying opponent player battle panel");
foreach (Transform child in opponentPlayerBattlePanel.transform)
{
if (child.tag == "Card")
{
Debug.Log("Card object found as child of opponentPlayerBattlePanel. Removing as a child object.");
child.transform.parent = null;
}
}
Destroy(opponentPlayerBattlePanel);
opponentPlayerBattlePanel = null;
}

Before the panels are destroyed, the game checks to see if a player card is still a child object of the panel. If there is a card as a child object, it is removed as a child object. This prevents a card’s gameobject from being destroyed when the player battle panel is destroyed.
Now CreateBattlePanels will need to be called. Currently, it is called from CheckIfAllPlayerBattleScoresSet, and that is called from the hook function HandleBattleScoreSet in GamePlayer.cs. CheckIfAllPlayerBattleScoresSet will only run if the GamePlayer’s isPlayerBattleScoreSet is set to true.
From the previous battle, isPlayerBattleScoreSet is already true for all players. So, to trigger the update that will spawn the new battle panels, isPlayerBattleScoreSet will need to be set to false before the next battle begins.
In CheckIfAllPlayersAreReadyForNextPhase in GamePlayer.cs, a new for loop will be added to the Choose Card section, shown below:
foreach (GamePlayer player in Game.GamePlayers)
{
RpcUpdateIsPlayerBattleScoreSet();
}
RpcUpdateIsPlayerBattleScoreSet will then need to be created. RpcUpdateIsPlayerBattleScoreSet will set off the chain of ClientRpc -> Find local game player -> call Command function to update value on server. The code for that whole chain is shown below:
[ClientRpc]
void RpcUpdateIsPlayerBattleScoreSet()
{
Debug.Log("Executing RpcUpdateIsPlayerBattleScoreSet for player: " + this.PlayerName);
GameObject LocalGamePlayer = GameObject.Find("LocalGamePlayer");
GamePlayer LocalGamePlayerScript = LocalGamePlayer.GetComponent<GamePlayer>();
LocalGamePlayerScript.UpdateIsPlayerBattleScoreSet();
}
public void UpdateIsPlayerBattleScoreSet()
{
if (hasAuthority)
{
Debug.Log("UpdateIsPlayerBattleScoreSet call from local player object. isPlayerBattleScoreSet is set to: " + isPlayerBattleScoreSet.ToString() + " for player: " + this.PlayerName);
if (isPlayerBattleScoreSet)
{
Debug.Log("Executing CmdUpdateIsPlayerBattleScoreSet to set isPlayerBattleScoreSet to false on the server.");
CmdUpdateIsPlayerBattleScoreSet();
}
}
}
[Command]
void CmdUpdateIsPlayerBattleScoreSet()
{
NetworkIdentity networkIdentity = connectionToClient.identity;
GamePlayer requestingPlayer = networkIdentity.GetComponent<GamePlayer>();
Debug.Log("Executing CmdUpdateIsPlayerBattleScoreSet for " + requestingPlayer.PlayerName);
requestingPlayer.HandleBattleScoreSet(requestingPlayer.isPlayerBattleScoreSet, false);
}

So, when the Battle Results phase starts, each player’s isPlayerBattleScoreSet is set to false. Then, when Choose Cards for the next battle starts, isPlayerBattleScoreSet will be set to true and the battle panels will be updated.
Back in GameplayManager.cs, the CheckIfAllPlayerBattleScoresSet will be updated real quick. At the end of it, the following check is added to call CreateBattlePanel in some situations where for some reason it wasn’t being called for me:
else if (!haveAllBattleScoresBeenSet && battleNumber > 1)
{
Debug.Log("CheckIfAllPlayerBattleScoresSet: Not all players are ready. After first battle, so updating anyway.");
CreateBattlePanels();
}

And one last thing, in GamePlayer.cs’s CheckIfAllPlayersAreReadyForNextPhase function, I wanted to add a new function called AfterBattleCleanUp that gets called after the Battle Results phase:

The code for AfterBattleCleanUp is shown below:
[Server]
void AfterBattleCleanUp()
{
Debug.Log("Executing: AfterBattleCleanUp on the server");
//Reset a bunch of syncvars and stuff back to false that were set during previous battle scenario
GameplayManager.instance.HandleAreBattleResultsSet(GameplayManager.instance.areBattleResultsSet, false);
GameplayManager.instance.HandleAreUnitsLostCalculated(GameplayManager.instance.unitsLostCalculated, false);
GameplayManager.instance.HandleUnitsLostFromRetreat(GameplayManager.instance.unitsLostFromRetreat, false);
}

AfterBattlecleanUp just cleans up some other syncvar stuff in GameplayManager. It may not be necessary right now, but whatever.
Anyway, if you save everything, build and run, you should see the panels update for the next battle:

Fixing Show Nearby Units
Right now, if you try and press the “Show Nearby Units” button on a second battle, the first battle was within 1 tile of the new battle, AND a unit died in the battle, you will get an error saying that no object with that network ID exists. That is because Show Nearby Units runs ShowUnitsOnMap from GameplayManager.cs, and ShowUnitsOnMap goes through each network id listed in UnitNetIdsAndPlayerNumber on a land object and then tries to find an object with that key, the key being a network id. The issue is that when a unit is lost in the battle, the object with that network id no longer exists but the network id is still listed in UnitNetIdsAndPlayerNumber on the land object.
To resolve the issue and remove the network ids from UnitNetIdsAndPlayerNumber the following lines can be added to the DestroyUnitsLostInBattle function in GamePlayer.cs:
//Check the battle site's lists for the units that were destroyed and remove them from that list
LandScript battleSiteScript = NetworkIdentity.spawned[GameplayManager.instance.currentBattleSite].gameObject.GetComponent<LandScript>();
if (battleSiteScript.UnitNetIdsOnLand.Contains(unitNetId))
battleSiteScript.UnitNetIdsOnLand.Remove(unitNetId);
if (battleSiteScript.UnitNetIdsAndPlayerNumber.ContainsKey(unitNetId))
battleSiteScript.UnitNetIdsAndPlayerNumber.Remove(unitNetId);

Save, and it should work now!
Setting the “Selected Card” to Null/Empty
After a battle, the Player’s “Selected card” never updates, so that will be taken care of now.
First, in GamePlayer.cs, there will be a new ClientRpc -> Local Game Player func -> Cmd Function chain, shown below:
[ClientRpc]
void RpcClearSelectedCard()
{
GameObject LocalGamePlayer = GameObject.Find("LocalGamePlayer");
GamePlayer LocalGamePlayerScript = LocalGamePlayer.GetComponent<GamePlayer>();
LocalGamePlayerScript.ClearSelectedCard();
}
public void ClearSelectedCard()
{
if (hasAuthority && selectedCard)
CmdClearSelectedCard();
}
[Command]
void CmdClearSelectedCard()
{
NetworkIdentity networkIdentity = connectionToClient.identity;
GamePlayer requestingPlayer = networkIdentity.GetComponent<GamePlayer>();
Debug.Log("Executing CmdClearSelectedCard for " + requestingPlayer.PlayerName);
requestingPlayer.HandleUpdatedPlayerBattleCard(playerBattleCardNetId, 0);
}

RpcClearSelectedCard needs to be called at the end of the MovePlayedCardToDiscard function:

Then, the HandleUpdatedPlayerBattleCard hook function in GamePlayer.cs will need to be updated to set the selectedCArd value to null.
if (isClient && newValue == 0)
{
selectedCard = null;
}

Updating Battle Results Panel for Second+ Battles
Right now, the battle results panel in your second or third or fourth…etc. battles won’t actually update. Updating the results panel is triggered in HandleAreUnitsLostCalculated in GameplayManager.cs. The calls to update the results panel will only execute if unitsLostCalculatedLocal is set to false, and after the first battle, that is set to true.
AfterBattleCleanUp in GamePlayer.cs sets unitsLostCalculated to false, which triggers the HandleAreUnitsLostCalculated hook function. Now, HandleAreUnitsLostCalculated just needs to be updated so that when unitsLostCalculated is set to false, unitsLostCalculatedLocal is set to false also.
else if (!newValue)
{
unitsLostCalculatedLocal = false;
}

Now, if you save everything and build and run, you should see the battle results panel update correctly on your second battle:

How to Handle All Retreating Units Dying
Right now, the game assumes that there will be a “Retreat Units” phase after the battle results phase. However, i all the losing player’s units die in a battle, either by card attack powers or by there not being anywhere to retreat to, the game doesn’t know what to do. Since all units died, there are no units to retreat, and thus no Retreat Units phase. But, with no Retreat Units phase, the game doesn’t know how to continue. So, how to deal with that situation?
In GamePlayer.cs, in its CheckIfAllPlayersAreReadyForNextPhase function, the game will need to check if there are any units left to retreat after the Battle Results phase ends. After the game does AfterBattleCleanUp, DestroyUnitsLostInBattle, and MovePlayedCardToDiscard, it will check if any units remain alive to retreat with the following:
bool unitsLeftToRetreat = CheckIfUnitsAreLeftToRetreat();
if (unitsLeftToRetreat)
{
Debug.Log("Units left to retreat");
Game.CurrentGamePhase = "Retreat Units";
Debug.Log("Game phase changed to Retreat Units");
}
else
{
//Code later
}

So, a new function called CheckIfUnitsAreLeftToRetreat will return if there are any units left to retreat. If CheckIfUnitsAreLeftToRetreat returns true, the game behaves as it did before: The game phase advances to Retreat Units. If it returns false, well, we’ll deal with that soon. For now, let’s create the CheckIfUnitsAreLeftToRetreat function. The code is shown below:
[Server]
bool CheckIfUnitsAreLeftToRetreat()
{
bool areAnyUnitsLeftToRetreat = false;
foreach (GamePlayer player in Game.GamePlayers)
{
if (player.doesPlayerNeedToRetreat)
{
foreach (uint unit in player.playerArmyNetIds)
{
if (!GameplayManager.instance.unitNetIdsLost.Contains(unit))
{
Debug.Log("At least one unit left to retreat");
areAnyUnitsLeftToRetreat = true;
break;
}
}
}
if (areAnyUnitsLeftToRetreat)
break;
}
return areAnyUnitsLeftToRetreat;
}

CheckIfUnitsAreLeftToRetreat is a server function that will loop through each player makred as “doesPlayerNeedToRetreat.” It will then check that player’s playerArmyNetIds list, and if there are any units in playerArmyNetIds that aren’t also in GameplayManager’s unitNetIdsLost list, then there is at least one unit left to retreat.
Now that the game is checking for what to do after all units are killed in a battle, it will then need to decide what to do next. That should be one of the following:
- Check for more battles. If there are any, move to the next battle
- If there are no more battles, go to Unit Movement
Basically, it does the same checks as when the Retreat Units phase ends. So, under that else statement for when CheckIfUnitsAreLeftToRetreat returns false, add the following code:
Debug.Log("Server: There were no units left to retreat. Checking for another battle.");
CleanupPreviousBattleInfo();
bool moreBattles = CheckForMoreBattles();
if (moreBattles)
{
Debug.Log("At least one more battle to fight. Moving to the next battle.");
ChangeToNextBattle();
}
else
{
Game.CurrentGamePhase = "Unit Movement";
Debug.Log("Changing phase to Unit Movement");
RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
return;
}

With all that taken care of, back in GameplayManager.cs the ChangeGamePhase function will need to be updated to make sure that the Game UI updates correctly when phases are changed.
The check that ends with && newGamePhase.StartsWith("Choose Cards")
should be updated to the following:
if ((currentGamePhase == "Battle(s) Detected" || currentGamePhase == "Retreat Units" || currentGamePhase == "Battle Results") && newGamePhase.StartsWith("Choose Cards"))
And the check for currentGamePhase == "Unit Movement" && newGamePhase == "Unit Movement"
should be updated to the following:
if ((currentGamePhase == "Unit Movement" || currentGamePhase == "Retreat Units" || currentGamePhase == "Battle Results") && newGamePhase == "Unit Movement")
So now a new battle (the Choose Cards) phase will start from either Battles Detected, Retreat Units, or Battle Results. The Unit Movement phase will start from either Unit Movement (when no battles occurred after movement), Retreat Units, or Battle Results.
To get everything to work correctly, some things need to be rearranged in CheckIfAllPlayersAreReadyForNextPhase’s if (Game.CurrentGamePhase == "Battle Results")
check. Specifically, it is important that MovePlayedCardToDiscard is called before DestroyUnitsLostInBattle is.
if (Game.CurrentGamePhase == "Battle Results")
{
AfterBattleCleanUp();
MovePlayedCardToDiscard();
DestroyUnitsLostInBattle();
bool unitsLeftToRetreat = CheckIfUnitsAreLeftToRetreat();
if (unitsLeftToRetreat)
{
Debug.Log("Units left to retreat");
Game.CurrentGamePhase = "Retreat Units";
Debug.Log("Game phase changed to Retreat Units");
}
else
{
Debug.Log("Server: There were no units left to retreat. Checking for another battle.");
CleanupPreviousBattleInfo();
bool moreBattles = CheckForMoreBattles();
if (moreBattles)
{
Debug.Log("At least one more battle to fight. Moving to the next battle.");
ChangeToNextBattle();
}
else
{
Game.CurrentGamePhase = "Unit Movement";
Debug.Log("Changing phase to Unit Movement");
RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
return;
}
}
RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
return;
}

And, in HandleUpdatedPlayerBattleCard, I forgot to make sure that RemoveSelectedCardFromHandAndReposition is NOT executed when the newValue is 0.

In GameplayManager.cs, some updates need to be made to how the Unit Movement phase starts. First, change StartUnitMovementPhase to the following:
public void StartUnitMovementPhase()
{
Debug.Log("Starting the Unit Movement Phase.");
haveUnitsMoved = false;
if (MouseClickManager.instance.unitsSelected.Count > 0)
MouseClickManager.instance.ClearUnitSelection();
ActivateUnitMovementUI();
SaveUnitStartingLocation();
LocalGamePlayerScript.UpdateUnitPositions();
SetGamePhaseText();
}

ActivateUnitMovementUI should be updated to the below code. The main thing should be to reset the camera’s position and size at the beginning, but I’m pasting the whole thing in case I forgot some stuff:
void ActivateUnitMovementUI()
{
//set camera position
Camera.main.orthographicSize = 7;
Vector3 cameraPosition = new Vector3(-1.5f, 1.5f, -10f);
Camera.main.transform.position = cameraPosition;
Debug.Log("Activating the Unit Movement UI");
if (!UnitMovementUI.activeInHierarchy && currentGamePhase == "Unit Movement")
UnitMovementUI.SetActive(true);
if (BattlesDetectedPanel.activeInHierarchy)
BattlesDetectedPanel.SetActive(false);
if (ChooseCardsPanel.activeInHierarchy)
ChooseCardsPanel.SetActive(false);
if (BattleResultsPanel.activeInHierarchy)
BattleResultsPanel.SetActive(false);
if (RetreatUnitsPanel.activeInHierarchy)
RetreatUnitsPanel.SetActive(false);
// Move buttons to the UnitMovementUI
hidePlayerHandButton.transform.SetParent(UnitMovementUI.GetComponent<RectTransform>(), false);
showPlayerHandButton.transform.SetParent(UnitMovementUI.GetComponent<RectTransform>(), false);
showPlayerHandButton.GetComponentInChildren<Text>().text = "Cards in Hand";
showPlayerDiscardButton.transform.SetParent(UnitMovementUI.GetComponent<RectTransform>(), false);
showOpponentCardButton.transform.SetParent(UnitMovementUI.GetComponent<RectTransform>(), false);
hideOpponentCardButton.transform.SetParent(UnitMovementUI.GetComponent<RectTransform>(), false);
if (!unitMovementNoUnitsMovedText.gameObject.activeInHierarchy)
unitMovementNoUnitsMovedText.gameObject.SetActive(true);
if (!endUnitMovementButton.activeInHierarchy)
endUnitMovementButton.SetActive(true);
if (endUnitMovementButton.activeInHierarchy)
endUnitMovementButton.GetComponent<Image>().color = Color.white;
if (resetAllMovementButton.activeInHierarchy)
resetAllMovementButton.SetActive(false);
//if (hidePlayerHandButton.activeInHierarchy && !PlayerHand.instance.isPlayerViewingTheirHand)
//hidePlayerHandButton.SetActive(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);
endUnitMovementButton.GetComponentInChildren<Text>().text = "End Unit Movement";
if (LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().isPlayerViewingTheirHand)
{
LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().HidePlayerHandOnScreen();
}
// When the movement phase begins, save the land occupied by the unit to be used in movement resets
SaveUnitStartingLocation();
if (!gamePlayerHandButtonsCreated)
CreateGamePlayerHandButtons();
else
{
if (opponentHandButtons.Count > 0)
{
foreach (GameObject opponentHandButton in opponentHandButtons)
{
opponentHandButton.transform.SetParent(UnitMovementUI.GetComponent<RectTransform>(), false);
opponentHandButton.SetActive(false);
}
}
}
if (opponentHandButtons.Count > 0)
{
foreach (GameObject opponentHandButton in opponentHandButtons)
{
opponentHandButton.SetActive(false);
}
}
if (isPlayerViewingOpponentHand && playerHandBeingViewed != null)
{
playerHandBeingViewed.GetComponent<PlayerHand>().HidePlayerHandOnScreen();
playerHandBeingViewed = null;
isPlayerViewingOpponentHand = false;
}
//Verifying all unit text is available on land objects. Sometimes these get hidden after battles
GameObject allLand = GameObject.FindGameObjectWithTag("LandHolder");
foreach (Transform landObject in allLand.transform)
{
LandScript landScript = landObject.gameObject.GetComponent<LandScript>();
if(landScript.infantryOnLand.Count > 1 || landScript.tanksOnLand.Count > 1)
landScript.UnHideUnitText();
}
GameObject[] PlayerUnitHolders = GameObject.FindGameObjectsWithTag("PlayerUnitHolder");
foreach (GameObject unitHolder in PlayerUnitHolders)
{
foreach (Transform unitChild in unitHolder.transform)
{
if (!unitChild.gameObject.activeInHierarchy)
unitChild.gameObject.SetActive(true);
}
}
}

Updating Battle Sites for Next Round of Battles
Everything seems to work great now when you finish your battles and move onto the Unit Movement phase. However, when the next round of battles start, the new battle sites are never highlighted and all that stuff. That’s because HighlightBattleSites doesn’t execute anything if haveBattleSitesBeenDone is true. So, that will need to be set to false somewhere.
To do this, a new SyncVar boolean called BattleSitesHaveBeenSet will be created in GameplayManager.cs with a hook function called HandleBattleSitesSet.
[SyncVar(hook = nameof(HandleBattleSitesSet))] public bool BattleSitesHaveBeenSet = false;

The code for the hook function HandleBattleSitesSet is shown below:
void HandleBattleSitesSet(bool oldValue, bool newValue)
{
Debug.Log("BattleSitesHaveBeenSet has been set to: " + newValue.ToString());
if (newValue)
HighlightBattleSites();
else if (!newValue)
haveBattleSitesBeenDone = false;
}

So, when BattleSitesHaveBeenSet is set to true, HighlightBattleSites will be called. When it is set to false, it will set haveBattleSitesBeenDone to false.
To make sure it gets set to false, the below line of code will be added to GamePlayer.cs’s CleanupPreviousBattleInfo function.
GameplayManager.instance.BattleSitesHaveBeenSet = false;

To set BattleSitesHaveBeenSet to true, a new Command function will be added to GamePlayer.cs called CmdCheckIfBattleSitesHaveBeenSet.
[Command]
public void CmdCheckIfBattleSitesHaveBeenSet()
{
if (GameplayManager.instance.battleSiteNetIds.Count > 0)
GameplayManager.instance.BattleSitesHaveBeenSet = true;
}

CmdCheckIfBattleSitesHaveBeenSet will now be called from GameplayManager.cs’s CheckIfAllUpdatedUnitPositionsForBattleSites function. Replace the call for HighlightBattleSites with LocalGamePlayerScript.CmdCheckIfBattleSitesHaveBeenSet()
.

Units Lost Who Couldn’t Retreat Text Not Disappearing
I noticed that when you get the “Retreating Units Destroyed” text to pop up once, it then stays there forever in ever subsequent battle. That isn’t good! So, to fix that, add the following to HandleUnitsLostFromRetreat in GameplayManager.cs:
else if (!newValue)
{
if (retreatingUnitsDestroyed.activeInHierarchy)
retreatingUnitsDestroyed.SetActive(false);
localUnitsLostFromRetreat = false;
}

Then add a similar check to UpdateUnitsLostValues:
if (!unitsLostFromRetreat)
{
if (retreatingUnitsDestroyed.activeInHierarchy)
retreatingUnitsDestroyed.SetActive(false);
}

The Draw Scenario: Check for New Battles
When a battle ends in a draw, both players have to retreat. The retreating units check allows for the two retreating players to place units onto the same tile. This should result in a new battle. So, some new checks and stuff need to be added to detect when there is a new battle after a retreat.
In GamePlayer.cs’s CheckIfAllPlayersAreReadyForNextPhase function, under the check if (Game.CurrentGamePhase == "Retreat Units")
, the following code will be added to check for any new battles after the draw:
if (GameplayManager.instance.reasonForWinning.StartsWith("Draw"))
{
Debug.Log("The last battle was a draw. Check if players retreated to same tile and started a new battle.");
bool newBattlesAfterRetreat = CheckForNewBattlesAfterDrawRetreat();
if (newBattlesAfterRetreat)
{
Debug.Log("New battle detected after draw retreat.");
CleanupPreviousBattleInfo();
Game.CurrentGamePhase = "New Battle Detected";
Debug.Log("Changing phase to New Battle Detected");
RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
return;
}
else
{
Debug.Log("NO new battle detected after draw retreat.");
}
}

So, if there was a draw, CheckForNewBattlesAfterDrawRetreat will be called. If CheckForNewBattlesAfterDrawRetreat returns true, that means there were new battles, and the game will advance to a new “New Battle Detected” phase. If there are no new battles, then the game continues as normal.
The code for CheckForNewBattlesAfterDrawRetreat can be found below:
[Server]
bool CheckForNewBattlesAfterDrawRetreat()
{
Debug.Log("Executing CheckForNewBattlesAfterDrawRetreat on the server.");
bool areThereNewBattlesAfterRetreat = false;
GameObject landTileHolder = GameObject.FindGameObjectWithTag("LandHolder");
foreach (Transform landObject in landTileHolder.transform)
{
//check land objects not already in the battle site list
if (!GameplayManager.instance.battleSiteNetIds.Any(b => b.Value == landObject.GetComponent<NetworkIdentity>().netId))
{
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);
areThereNewBattlesAfterRetreat = true;
//int battleNumber = GameplayManager.instance.battleSiteNetIds.Count + 1;
int battleNumber = 0;
//Get the largest "battle number" in the dictionary. Dictionary values would have been removed at this point so can't just use battleSiteNetIds.Count+1
foreach (KeyValuePair<int, uint> battleSite in GameplayManager.instance.battleSiteNetIds)
{
if (battleNumber < battleSite.Key)
battleNumber = battleSite.Key;
}
battleNumber++;
GameplayManager.instance.battleSiteNetIds.Add(battleNumber, landObject.GetComponent<NetworkIdentity>().netId);
break;
}
playerNumber = units.Value;
}
}
}
}
return areThereNewBattlesAfterRetreat;
}

CheckForNewBattlesAfterDrawRetreat will go through each land tile, check to see if opposing units are on the same land tile, and if so, add that land tile to the battle list in GameplayManager’s battleSiteNetIds dictionary.
An important line is the following:
if (!GameplayManager.instance.battleSiteNetIds.Any(b => b.Value == landObject.GetComponent<NetworkIdentity>().netId))
This means that it will check to make sure that the land tile being investifated is not already a battle site by making sure that the land tile’s network id does not exist as a value anyhwere in the battleSiteNetIds dictionary.
Creating the New Battle Detected Phase
So, when a new battle is detected, the server sets the new phase to “New Battle Detected.” So, Now GameplayManager.cs will need a way to change it to that phase. A new check will be added to the ChangeGamePhase function in New Battle Detected:
if (currentGamePhase == "Retreat Units" && newGamePhase == "New Battle Detected")
{
MouseClickManager.instance.canSelectPlayerCardsInThisPhase = false;
MouseClickManager.instance.canSelectUnitsInThisPhase = false;
currentGamePhase = newGamePhase;
StartBattlesDetected();
}

Then, || currentGamePhase == "New Battle Detected"
will need to be added to the check that ends with && newGamePhase.StartsWith("Choose Cards")
.
if ((currentGamePhase == "Battle(s) Detected" || currentGamePhase == "Retreat Units" || currentGamePhase == "Battle Results" || currentGamePhase == "New Battle Detected") && newGamePhase.StartsWith("Choose Cards"))
Some small changes will need to be made to ActivateBattlesDetectedUI:
if (RetreatUnitsPanel.activeInHierarchy)
RetreatUnitsPanel.SetActive(false);
startBattlesButton.GetComponentInChildren<Text>().text = "Start Battles";
if (currentGamePhase == "New Battle Detected")
startBattlesButton.GetComponentInChildren<Text>().text = "Return to Battles";

And now, I made a mistake in the previous post for marking which players need to retreat. The GamePlayer.cs function CheckWhichPlayersNeedToRetreat is used to determine and mark game players that will need to retreat. It is called from UnitsLostFromBattle. Currently, CheckWhichPlayersNeedToRetreat is called from within a if (winningPlayer && losingPlayer)
check, which will be a false check if there is a draw, meaning neither player will be marked as needing to retreat. So, that call to CheckWhichPlayersNeedToRetreat needs to be put outside of the if statement, along with the call for GameplayManager.instance.HandleAreUnitsLostCalculated(GameplayManager.instance.unitsLostCalculated, true);
.

Expanding Units for a Tie
When there is a draw scenario, the units should be expanded during Battle Results. In LandScript.cs, a new function called ExpandForTie will be created. Code is shown below:
public void ExpandForTie()
{
int player1Multiplier;
int player2Multiplier;
Vector3 player1UnitStartPosition = new Vector3(0, 0, 0);
Vector3 player2UnitStartPosition = new Vector3(0, 0, 0);
if (Player1Inf.Count > 0)
player1UnitStartPosition = Player1Inf[0].gameObject.transform.position;
else
player1UnitStartPosition = Player1Tank[0].gameObject.transform.position;
if (Player2Inf.Count > 0)
player2UnitStartPosition = Player2Inf[0].gameObject.transform.position;
else
player2UnitStartPosition = Player2Tank[0].gameObject.transform.position;
if (player1UnitStartPosition.x > player2UnitStartPosition.x)
{
player1Multiplier = 1;
player2Multiplier = -1;
}
else
{
player1Multiplier = -1;
player2Multiplier = 1;
}
ExpandUnitsForBattleResults(Player1Tank, Player1Inf, player1Multiplier);
ExpandUnitsForBattleResults(Player2Tank, Player2Inf, player2Multiplier);
foreach (KeyValuePair<GameObject, int> battleText in BattleUnitTexts)
{
GameObject battleTextToDestroy = battleText.Key;
Destroy(battleTextToDestroy);
battleTextToDestroy = null;
}
BattleUnitTexts.Clear();
}

ExpandForTie will then be called from the UpdateResultsPanel function in GameplayManager.cs.
if (reasonForWinning == "Draw: No Winner")
{
unitsLost.text = "No units lost";
NetworkIdentity.spawned[currentBattleSite].gameObject.GetComponent<LandScript>().ExpandForTie();
}

Then, to make sure that the battle sites get updated for the new detected battles, a check needs to be added to GamePlayer.cs’s CmdUpdateUnitPositions function. The following:
if (Game.CurrentGamePhase == "Battle(s) Detected")
requestingPlayer.GetComponent<GamePlayer>().updatedUnitPositionsForBattleSites = true;
Needs to include: || Game.CurrentGamePhase == "New Battle Detected"
if (Game.CurrentGamePhase == "Battle(s) Detected" || Game.CurrentGamePhase == "New Battle Detected")
requestingPlayer.GetComponent<GamePlayer>().updatedUnitPositionsForBattleSites = true;

Then, to make sure that updating unit positions doesn’t mess with the battle site expansions on new battle sites, a check will be added to the UpdateUnitLandObject function in UnitScript.cs. The check is added after the else if (gameObject.tag == "tank")
check.
if (GameplayManager.instance.currentGamePhase == "New Battle Detected")
{
bool isNewLandABattleSite = false;
uint landClickedOnNetId = LandToMoveTo.GetComponent<NetworkIdentity>().netId;
foreach (KeyValuePair<int, uint> battleSites in GameplayManager.instance.battleSiteNetIds)
{
if (battleSites.Value == landClickedOnNetId)
isNewLandABattleSite = true;
}
if (isNewLandABattleSite)
landScript.MoveUnitsForBattleSite();
}
And now, well, I think everything should be working. Should I try and make a video? Let’s see! Ok I did, the video is below:
Next Steps…
This was a really long post. That makes sense though, because I am coming closer and closer to a game that can actually be played to the end. CardConquest allows players to move their units, fight battles, pick cards for those battles, and then do it all over again! So, what’s left?
- “Roll Over” cards when a player has used them all
- After all cards are discarded, the discard cards should roll back into the player’s “hand” to start over
- Detecting Game Over
- The game needs to detect when a battle is taking place on a player base, or when an opposing enemy is on a player base
- There will be a player base fight – The base has a strength of 2 or something on its own
- if the opposing player wins the player base fight, the game is over and the opposing player wins.
- If the defending player wins the player base fight, the game goes on…
- The game needs to detect when a battle is taking place on a player base, or when an opposing enemy is on a player base
Smell ya later nerds!