CardConquest Unity Multiplayer GameDev Blog #21: The Results of the Battle

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, the player chose their cards for a battle and I created some logic to determine who, if anyone, won the battle. Now, it’s time to display those results to the players. I will want to do the following:

  • Determine how many, if any units will be destroyed in the battle
  • Display the battle results to the players
  • When the “Battle Results” turn ends, discard player cards and destroy units that were lost

Let’s get going then!

Determine How Many Units Were Lost in Battle (RIP)

After a battle is over, and there is a winner declared for the battle, the game will determine if any units are lost by the losing player. The number of units lost will be determined by the “attack” value of the winner’s played card, and the “defense” value of the loser’s played card.

On the cards themselves, the attack value is displayed in the number of crossed swords on the card. In the below “Power 3” card, the card has an attack value of 3.

The defense value is displayed in the number of shields on the card. In the below “Power 2” card, the card has a defense value of 2

Cards can have both attack and defense values. The Power 4 card has 2 attack and 1 defense

If you play Power 4 and you win, then the 2 attack value is used. If you player Power 4 and you lose, the 1 defense value is used.

In regards to lost units from a battle, the number of units that will be lost is calculated by taking the attack value of the winner’s card and subtracting the defense value of the loser’s card. So, if the winner player Power 3, and the loser played Power 2, then an Attack Value of 3 minus a Defense Value of 2 means 1 unit is lost. If an attacker plays Power 3 and the loser plays Power 4, then only the 1 defense value of Power 4 is used. The 2 attack value of Power 4 is ignored because the losing player played it.

So, then, let’s code out how to calculate that in the game! It shouldn’t be too hard, right?

First, in GameplayManager.cs, a few new functions will be created to keep track of the number of units lost and the network IDs of the units that were lost. The following variables can be added under the “Battle Results” header section of GameplayManager.cs.

[SyncVar] public int numberOfTanksLost;
[SyncVar] public int numberOfInfLost;
public SyncList<uint> unitNetIdsLost = new SyncList<uint>();

With those variables added to GameplayManager.cs, a new server function called UnitsLostFromBattle can be added to GamePlayer.cs. The code is shown below:

[Server]
void UnitsLostFromBattle(GamePlayer winningPlayer, GamePlayer losingPlayer)
{
	Debug.Log("Executing UnitsLostFromBattle");
	if (winningPlayer && losingPlayer)
	{
		Card winningCard = NetworkIdentity.spawned[winningPlayer.playerBattleCardNetId].gameObject.GetComponent<Card>();
		Card losingCard = NetworkIdentity.spawned[losingPlayer.playerBattleCardNetId].gameObject.GetComponent<Card>();

		//Clear out old data
		GameplayManager.instance.numberOfTanksLost = 0;
		GameplayManager.instance.numberOfInfLost = 0;
		GameplayManager.instance.unitNetIdsLost.Clear();

		if (winningCard.AttackValue > losingCard.DefenseValue)
		{
			Debug.Log("Attack value greater than defense. Units will be lost. Attack value: " + winningCard.AttackValue.ToString() + " Defense Value: " + losingCard.DefenseValue.ToString());

			int unitsToLose = winningCard.AttackValue - losingCard.DefenseValue;
			if (unitsToLose > losingPlayer.playerArmyNetIds.Count)
			{
				unitsToLose = losingPlayer.playerArmyNetIds.Count;
			}
KillUnitsFromBattle(losingPlayer, unitsToLose);
		}
		else
		{
			Debug.Log("No units lost. Attack value: " + winningCard.AttackValue.ToString() + " Defense Value: " + losingCard.DefenseValue.ToString());
		}
		//GameplayManager.instance.HandleAreUnitsLostCalculated(GameplayManager.instance.unitsLostCalculated, true);
	}
}

UnitsLostFromBattle does the following:

  • Takes two GamePlayer objects as parameters: winningPlayer and losingPlayer
  • Checks to make sure that winningPlayer and losingPlayer are not null. If they aren’t null, then the rest of the code is executed
  • Gets the winning player’s card by looking up the card’s network id that is stored in the winner’s GamePlayer’s playerBattleCardNetId variable. NetworkIdentity.spawned is what is used to find the object based on its network id
  • Gets the losing player’s card by looking up the card’s network id that is stored in the loser’s GamePlayer’s playerBattleCardNetId variable
  • Clears any old data out the variables in GameplayManager that store lost units
  • If the winning card’s attack value is greater than the loser’s defense value, do the following:
    • calculate the difference between the attack value and defense value. store as the number of unitsTolose
    • If the value of unitsToLose is greater than the number of units in the losing player’s army, then set the unitsToLose value as the number of units in the loser’s army
    • call KullUnitsFromBattle passing the losingPlayer GamePlayer object and the unitsToLose value as arguments
  • Some commented out shit I will get to later!

So, UnitsLostFromBattle just calculates the number of units to lose from a battle. If units need to be lost/killed, then the KillUnitsFromBattle function will be called. Based on that function’s name, that’s where the units will actually be killed? Well, sort of. It’s where the the game will save what units need to be killed. And now it’s time to create that function! code shown below!

[Server]
void KillUnitsFromBattle(GamePlayer retreatingPlayer, int numberOfUnitsToKill)
{
	Debug.Log("Executing KillUnitsThatCantRetreat. Will destroy " + numberOfUnitsToKill.ToString() + " for player: " + retreatingPlayer.PlayerName + ":" + retreatingPlayer.playerNumber.ToString());
	// Get all the player's units from the battle site
	GameObject battleSiteLand = NetworkIdentity.spawned[GameplayManager.instance.currentBattleSite].gameObject;
	List<uint> retreatingPlayerUnits = new List<uint>();
	foreach (KeyValuePair<uint, int> unit in battleSiteLand.GetComponent<LandScript>().UnitNetIdsAndPlayerNumber)
	{
		if (unit.Value == retreatingPlayer.playerNumber)
			retreatingPlayerUnits.Add(unit.Key);
	}

	//Remove uint IDs that have already been "killed" in UnitsLostFromBattle
	foreach (uint unitLost in GameplayManager.instance.unitNetIdsLost)
	{
		if (retreatingPlayerUnits.Contains(unitLost))
			retreatingPlayerUnits.Remove(unitLost);
	}

	if (retreatingPlayerUnits.Count > 0)
	{
		if (numberOfUnitsToKill > retreatingPlayerUnits.Count)
			numberOfUnitsToKill = retreatingPlayerUnits.Count;
		//Determine what remaining units are tanks versus what units are infantry. Kill tanks before infantry
		List<uint> playerTanks = new List<uint>();
		List<uint> playerInf = new List<uint>();
		foreach (uint unitNetId in retreatingPlayerUnits)
		{
			if (NetworkIdentity.spawned[unitNetId].gameObject.tag == "tank")
			{
				playerTanks.Add(unitNetId);
			}
			else if (NetworkIdentity.spawned[unitNetId].gameObject.tag == "infantry")
			{
				playerInf.Add(unitNetId);
			}
		}
		//Go through and kill tanks
		if (playerTanks.Count > 0)
		{
			foreach (uint unitNetId in playerTanks)
			{
				GameplayManager.instance.unitNetIdsLost.Add(unitNetId);
				GameplayManager.instance.numberOfTanksLost++;
				numberOfUnitsToKill--;
				if (numberOfUnitsToKill <= 0)
					break;
			}
		}
		//Go through and kill infantry
		if (numberOfUnitsToKill > 0 && playerInf.Count > 0)
		{
			foreach (uint unitNetId in playerInf)
			{
				GameplayManager.instance.unitNetIdsLost.Add(unitNetId);
				GameplayManager.instance.numberOfInfLost++;
				numberOfUnitsToKill--;
				if (numberOfUnitsToKill <= 0)
					break;
			}
		}
	}
}

KillUnitsFromBattle will be used to save the network ids of any units to kill, as well as store that information in GameplayManager.cs’s variables. It will also be used later when destroying units that can’t retreat. Anyway, here is what KillUnitsFromBattle is doing:

  • Two arguments are required. A GamePlayer object called retreatingPlayer (think of it as losingPlayer. I named it retreatingPlayer when I first made this to only be used for “killing” retreating units. Just go with it) and the number of units to “kill” called numberOfUnitsToKill
  • The land object of the battle site is retrieved
  • A new list variable called retreatingPlayerUnits is created
  • A foreach loop goes through all units in the battle site’s UnitNetIdsAndPlayerNumber dictionary
    • if the corresponding player number of the unit matches the retreatingPlayer’s player number, add the unit’s network id to the retreatingPlayerUnits list
  • Next, go through all the network ids in GameplayManager’s unitNetIdsLost list
    • unitNetIdsLost stores units already marked as lost
    • if any units in reatreatingPlayerUnits is in unitNetIdsLost, remove that unit from retreatingPlayerUnits
      • this is to make sure we aren’t trying to remove the same unit twice or anything like that
  • If retreatingPlayerUnits has a count of more than 0, as in there are still units available to be lost, do the following:
    • if numberOfUnitsToKill is greater than the total number of units in retreatingPlayerUnits, set numberOfUnitsToKill to the count of retreatingPlayerUnits
    • create two new lists for the tanks and infantry of the retreating player
    • Go through each unit in retreatingPlayerUnits
      • use NetworkIdentity.spawned to get the unit’s gameobject and its tag. Sort the units by tanks and infantry. Add the units to the corresponding playerTanks and playerInf lists
    • Tanks will be killed before infantry are. So, check if there are any remaining tanks. If there are, go through each tank
      • Add the tank’s network id to GameplayManager.cs’s unitNetIdsLost
      • Increase the numberOfTanksLost value in GameplayManager.cs
      • decrease numberOfUnitsToKill
      • if numberOfUnitsToKill is now 0, break out of the foreach loop
    • if numberOfUnitsToKill is greater than 0 and so is the number of infantry remaining for the player, go through each infantry unit
      • Add the infantry’s network id to GameplayManager.cs’s unitNetIdsLost
      • increase numberOfInfLost
      • decrease numberOfUnitsToKill
      • if numberOfUnitsToKill is now 0, break out of the foreach loop

KillUnitsFromBattle doesn’t actually kill the units, or destroy them in the game, it just saves the network id of the unit for GameplayManager. Later, the unit will actually be destroyed, but that would be until later when the Battle Results phase ends.

Right now, though UnitsLostFromBattle still needs to be called somewhere. Throughout DetermineWhoWonBattle in GamePlayer.cs, a call to UnitsLostFromBattle will need to be added to each victory condition scenario, like this:

UnitsLostFromBattle(player1, player2);

The below screenshot should show all of the calls UnitsLostFromBattle that are required:

Notice that the last call, in the draw scenario, is UnitsLostFromBattle(null,null). Because there is no winner, and no loser, in the draw, no GamePlayer objects are passed. The call is still made, though, because later a check for retreating units will be called KillUnitsFromBattle and that will still need to occur even when there is a draw scenario.

Anyway, you can test out the above for killing units based on card values now. In my test scenario, Player1 won and Player2 lost. Player1 played Power 3 with an attack of 3. Player2 played Power1 with a defense of 1. This resulted in two of Player2’s tanks being killed/lost, which is reflected in GameplayManager’s variables in the unity inspector

Check if Units Can Retreat

You may be wondering what is going to happen to the losing player’s units that weren’t destroyed. Will they just, like, stay on the battle site? No! They must now retreat.

How will retreating work? Well, the retreating units will still be limited by normal movement restrictions. The units can only retreat 1 tile away from the battle site. They can’t go to a tile that already has 5 units from the same player. And, importantly, I don’t want a player to be able to retreat to a tile that has an enemy unit on it. Partly because I don’t want another battle to occur and have to figure out how to code a new battle in the middle of the battle round, but also to “add” the “strategy” of surrounding and cutting off your opponents to destroy units.

So, anyway, first a new server function will be added to GamePlayer.cs to check which player’s need to retreat called CheckWhichPlayersNeedToRetreat. The game will check which player’s need to retreat in the event there is a draw and both player’s need to, and maybe for the future when there could be more than two players? idk. Anyway, before that begins, a new boolean syncvar called doesPlayerNeedToRetreat will be added to GamePlayer.cs to track which players will need to retreat.

[Header("Retreat Units Section")]
[SyncVar] public bool doesPlayerNeedToRetreat = false;

In GameplayManager.cs, some new variables will be added to store information on the losing player in addition to the winning player.

[SyncVar] public string loserOfBattleName;
[SyncVar] public int loserOfBattlePlayerNumber;
[SyncVar] public int loserOfBattlePlayerConnId;

The “loserOfBattle” variables will need to be updated in GamePlayer.cs’s DetermineWhoWonBattle function, like this:

GameplayManager.instance.loserOfBattleName = player2.PlayerName;
GameplayManager.instance.loserOfBattlePlayerNumber = player2.playerNumber;
GameplayManager.instance.loserOfBattlePlayerConnId = player2.ConnectionId;

For the draw scenario, just use the same values stored in winnerOfBattle for the loserOfBattle stuff (“tie” for the palyer name, and -1 for the player number and connection id). Once you’ve updated loserOfBattle in all the victory conditions, the CheckWhichPlayersNeedToRetreat function can be created. Code shown below.

[Server]
void CheckWhichPlayersNeedToRetreat()
{
	List<GamePlayer> playersToRetreat = new List<GamePlayer>();
	//If the battle is a tie, make sure that all players involved are set to retreat
	if (GameplayManager.instance.reasonForWinning == "Draw: No Winner")
	{
		//Get all the GamePlayers involved in the battle
		LandScript battleSiteLandScript = NetworkIdentity.spawned[GameplayManager.instance.currentBattleSite].gameObject.GetComponent<LandScript>();
		List<int> battlePlayerNumbers = new List<int>();
		foreach (KeyValuePair<uint, int> battleUnitPlayer in battleSiteLandScript.UnitNetIdsAndPlayerNumber)
		{
			bool isPlayerNumberInList = battlePlayerNumbers.Contains(battleUnitPlayer.Value);
			if (!isPlayerNumberInList)
				battlePlayerNumbers.Add(battleUnitPlayer.Value);
		}

		if (battlePlayerNumbers.Count > 0)
		{
			foreach (GamePlayer gamePlayer in Game.GamePlayers)
			{
				if (battlePlayerNumbers.Contains(gamePlayer.playerNumber))
				{
					gamePlayer.doesPlayerNeedToRetreat = true;
					playersToRetreat.Add(gamePlayer);
				}
				else
					gamePlayer.doesPlayerNeedToRetreat = false;
			}
		}
	}
	else
	{
		foreach (GamePlayer gamePlayer in Game.GamePlayers)
		{
			if (gamePlayer.playerNumber == GameplayManager.instance.loserOfBattlePlayerNumber && gamePlayer.ConnectionId == GameplayManager.instance.loserOfBattlePlayerConnId)
			{
				gamePlayer.doesPlayerNeedToRetreat = true;
				playersToRetreat.Add(gamePlayer);
			}
			else
				gamePlayer.doesPlayerNeedToRetreat = false;
		}
	}
	CanPlayerRetreatFromBattle(playersToRetreat, GameplayManager.instance.currentBattleSite);
}

CheckWhichPlayersNeedToRetreat does the following:

  • first, checks if the current battle was a draw. IF yes:
    • gets the land object of the battle site
    • stores the player numbers of the battle
    • Goes through each GamePlayer in the server’s Game.GamePlayers list. If the GamePlayer’s player number matches a player number involved in the battle, sets the GamePlayer’s doesPlayerNeedToRetreat value to true and adds the GamePlayer to the playersToRetreat list. If not, sets it to false
  • If the battle wasn’t a draw:
    • Goes through each GamePlayer in Game.GamePlayers. If the GamePlayer’s player number/info matches the loserOfBattle, add that GamePlayer to playersToRetreat
  • Finally, calls CanPlayerRetreatFromBattle and passes the playersToRetreat list and the currentBattleSite network ID as arguments

CanPlayerRetreatFromBattle will then check if the player is capable of retreating. It will check what nearby lands have enemy units on them and what number units the player already has on nearby lands. If there isn’t enough “space” for the player to retreat all their units onto nearby lands, units will be killed until there is enough space or no units remain.

The code for CanPlayerRetreatFromBattle is shown below:

[Server]
void CanPlayerRetreatFromBattle(List<GamePlayer> playersToRetreat, uint battleSiteId)
{
	//Get list of land tiles 1 distance away from battle site
	List<LandScript> landsToRetreatTo = new List<LandScript>();
	GameObject battleSiteLand = NetworkIdentity.spawned[GameplayManager.instance.currentBattleSite].gameObject;
	GameObject allLand = GameObject.FindGameObjectWithTag("LandHolder");
	foreach (Transform landObject in allLand.transform)
	{
		if (landObject.transform.position != battleSiteLand.transform.position)
		{
			float distanceFromBattle = Vector3.Distance(battleSiteLand.transform.position, landObject.gameObject.transform.position);
			if (distanceFromBattle < 3.01f)
			{
				Debug.Log("Can retret to " + landObject.gameObject + ". Distance from battlesite: " + distanceFromBattle.ToString("0.00") + ". " + battleSiteLand.transform.position + " " + landObject.gameObject.transform.position);
				LandScript landScript = landObject.gameObject.GetComponent<LandScript>();
				landsToRetreatTo.Add(landScript);
			}
		}            
	}

	//Check if the available lands have units from another player on it. Player's can only retreat to lands with no units on them or only have their own units
	foreach (GamePlayer retreatingPlayer in playersToRetreat)
	{
		//Get number of units player has to retreat
		int numberOfUnitsToRetreat = 0;
		foreach (KeyValuePair<uint, int> unit in battleSiteLand.GetComponent<LandScript>().UnitNetIdsAndPlayerNumber)
		{
			if (unit.Value == retreatingPlayer.playerNumber)
				numberOfUnitsToRetreat++;
		}
		//remove killed units from card power from the number to retreat
		int numberOfUnitsAlreadyKilled = 0;
		foreach (uint unit in GameplayManager.instance.unitNetIdsLost)
		{
			if (retreatingPlayer.playerUnitNetIds.Contains(unit))
			{
				numberOfUnitsAlreadyKilled++;
			}
		}
		//numberOfUnitsToRetreat -= GameplayManager.instance.unitNetIdsLost.Count;
		numberOfUnitsToRetreat -= numberOfUnitsAlreadyKilled;
		Debug.Log(retreatingPlayer.PlayerName + ":" + retreatingPlayer.playerNumber + " must retreat " + numberOfUnitsToRetreat.ToString() + " number of units.");
		
		// If the retreating player has units remaining to retreat, check each land if there are enemy units on, and keep track of the available space on land. If there isn't enough "room" on a land tile to retreat all units, more units will need to be destroyed.
		int numberAvailableToRetreat = 0;
		if (numberOfUnitsToRetreat > 0)
		{
			List<LandScript> landWithNoEnemies = new List<LandScript>();
			foreach (LandScript landToRetreatTo in landsToRetreatTo)
			{
				bool enemyUnitsOnLand = false;
				foreach (KeyValuePair<uint, int> unit in landToRetreatTo.UnitNetIdsAndPlayerNumber)
				{
					if (unit.Value != retreatingPlayer.playerNumber)
					{
						enemyUnitsOnLand = true;
						break;
					}
				}
				if (!enemyUnitsOnLand)
				{
					Debug.Log("No enemy units on " + landToRetreatTo.gameObject);
					if (landToRetreatTo.UnitNetIdsAndPlayerNumber.Count < 5)
					{
						Debug.Log("No enemy units AND available space to retreat to " + landToRetreatTo.gameObject);
						landWithNoEnemies.Add(landToRetreatTo);
						numberAvailableToRetreat += 5 - landToRetreatTo.UnitNetIdsAndPlayerNumber.Count;
					}
				}
			}
		}

		//Check if all units are able to retreat
		if (numberOfUnitsToRetreat <= numberAvailableToRetreat)
		{
			Debug.Log("Enough space for all of " + retreatingPlayer.PlayerName + ":" + retreatingPlayer.playerNumber + " to retreat all units.");
		}
		else if (numberAvailableToRetreat == 0)
		{
			Debug.Log("NO space for ANY of " + retreatingPlayer.PlayerName + ":" + retreatingPlayer.playerNumber + " to retreat units. ALL UNITS WILL BE DESTROYED!");
			KillUnitsFromBattle(retreatingPlayer, numberOfUnitsToRetreat);
			GameplayManager.instance.HandleUnitsLostFromRetreat(GameplayManager.instance.unitsLostFromRetreat, true);
		}
		else
		{
			int numberOfUnitsToDestroy = numberOfUnitsToRetreat - numberAvailableToRetreat;
			Debug.Log("Not enough space for " + retreatingPlayer.PlayerName + ":" + retreatingPlayer.playerNumber + " to retreat all units. " + numberOfUnitsToDestroy.ToString() + " will need to be destroyed.");
			KillUnitsFromBattle(retreatingPlayer, numberOfUnitsToDestroy);
			GameplayManager.instance.HandleUnitsLostFromRetreat(GameplayManager.instance.unitsLostFromRetreat, true);
		}
		
	}
}

CanPlayerRetreatFromBattle does the following:

  • First gets a list of all land objects that are only 1 tile away from the battle site and adds them to the landsToRetreatTo list
  • goes through each GamePlayer in playersToRetreat
    • Get’s the number of units a player has to retreat from the units on the battle site
    • subtracts number of units in unitNetIdsLost from the number of units to retreat
      • ONLY subtracts units in unitNetIdsLost that belong to the retreating player
    • sets numberAvailableToRetreat to 0. This will be used to store the amount of space for the player’s units to retreat
    • If there are units that the player has to retreat, does the following:
      • Creates a new empty list called landWithNoEnemies
      • goes through each land object in the landsToRetreatTo list
        • Checks if a unit belonging to player that IS NOT the retreatingPlayer. If another player’s unit is discovered, skip to the next land object
        • if NO ENEMY UNITS are discovered, do the following:
          • Checks to make sure the number of units already on the land is less than 5 (5 is the max number of units allowed
          • Subtracts the number of units on the land from 5 to get the “space” available on the land tile, adds that value to numberAvailableToRetreat
    • If numberAvailableToRetreat is greater than or equal to the numberOfUnitsToRetreat, all units can retreat
    • If numberAvailableToRetreat is 0, all units will need to be killed
      • Calls KillUnitsFromBattle and passes the retreatingPlayer and numberOfUnitsToRetreat values
    • else, if numberAvailableToRetreat is less than numberOfUnitsToRetreat:
      • get the number of units to destroy by subtracting numberAvailableToRetreat from numberOfUnitsToRetreat
      • call KillUnitsFromBattle with the retreatingPlayer and the numberOfUnitsToDestroy value

There are some commented out calls to a hook function in GameplayManager. Those will be used later for some UI updates. We’ll get to it eventually…

But for now, one last thing needs to be added before a test can be done. Make the call for CheckWhichPlayersNeedToRetreat() in UnitsLostFromBattle

Save and build, and try it out. I first created a scenario where the player2 (blue player) would be surrounded with only 1 friendly tile to retreat to. That retreatable tile would only have 1 space remained.

I then attacked with the winning player playing Power 3 (attack value: 3) and the losing player player2 playing Power 2 (defense value: 2). That means 1 unit would be lost from the card values. But, due to the lack of space to retreat, the unit can only retreat 1 of their 3 remaining units, meaning 2 more units must be killed. This means a total of 3 units are killed and 1 survived for player2. You should see in GameplayManager’s values that 2 tanks and 1 infantry were lost:

Displaying Battle Results to Players

It’s now time for my favorite activity: Adding new UI! It’s so much fun to create and even more fun to write about how to create! The battle results UI will do the following:

  • Display what card each player played
  • Re calculate battle scores on the screen for opposing players
  • Show who won
  • say why that player won
  • say what units, if any, were lost
    • I will also expand out units of the losers and use a sprite to mark units that were killed
  • Display a message if any units were lost due to not being able to retreat

Making Some Panels and Text and Stuff

Open the Gameplay scene in the unity editor. A new panel will be created for the Battle Results phase of the game. To quickly create a new panel, select the ChooseCardsPanel and use ctrl+d to duplicate it.

Activate the duplicated ChooseCardsPanel and rename it to BattleResultsPanel. Expand BattleResultsPanel and delete the selectThisCard button. Then, rename confirmChooseCard to endBattleResults and enable it in the hierarchy. Expand the button and make its text to “Next Turn”

Next, create a new panel object as a child of the BattleResultsPanel

Rename the new Panel to ResultsPanel. Select ResultsPanel, and then set the following in its RectTransform:

  • Anchor: middle right
  • Pos X: -170
  • Pos Y: 0
  • Width: 300
  • Height: 550

The ResultsPanel should look like this in the scene:

Next a bunch of text stuff is going to be made. For right now, create one new text object and rename it to BattleWinner

Select BattleWinner and set the following for its RectTransform

  • Middle Center anchor
  • Pos X 0
  • Pos Y 230
  • Width 290
  • Height 35

Then, set the text to:

  • Text: The Winner Is:
  • Font: ThaleahFat
  • Font Size: 35
  • Alignment: Centered
  • Color: White

The BattleWinner text should be done. Now, you can duplicate it in the heirarchy 6 times.

Select duplicate 1, rename it to WinnerName, and set the following:

To get the yellow color, use the following RGB: R:255,G:255,B:0

Select duplicate (2), rename it to VictoryCondition, and set the following:

Rename duplicate (3) to ReasonForVictory and set the following:

The text will be set by the script so you can set it to whatever you want.

Rename Duplicate (4) to UnitsLost and set the following:

Set the name of Duplicate (5) to NumberOfUnitsLost and set the following:

The text isn’t very important as it will also be set by the script. Just make sure the text box is large enough to fit 3 short lines of text.

Finally, rename duplicate (6) to retreatingUnitsDestroyed and set the following:

The ResultsPanel should now look like the following in the game scene:

Updating the Battle Results Panel

In GameplayManager.cs, some new variables will be added so the marvelous UI that was just created can be updated by the GameplayManager.cs script.

[Header("Battle Results UI")]
[SerializeField] GameObject BattleResultsPanel;
[SerializeField] private GameObject endBattleResultsButton;
[SerializeField] private Text winnerName;
[SerializeField] private Text victoryCondition;
[SerializeField] private Text unitsLost;
[SerializeField] private GameObject retreatingUnitsDestroyed;

Save the script and then add the corresponding UI objects to their variables on the GameplayManager object in the Unity editor.

You can then deactivate the “retreatingUnitsDestroyed” text:

and then deactivate the BattleResultsPanel. Everything will be activated as needed by GameplayManager.cs.

In GameplayManager.cs, go through all the “Activate” UI functions, such as ActivateUnitPlacementUI, and add the following to make sure that the BattleResultsPanel is deactivated during that phase.

if (BattleResultsPanel.activeInHierarchy)
	BattleResultsPanel.SetActive(false);

This line should be added to:

  • ActivateUnitPlacementUI
  • ActivateUnitMovementUI
  • ActivateBattlesDetectedUI
  • ActivateChooseCards

After that is taken care of, a new ActivateBattleResultsUI can be created to actually activate the BattleResultsPanel! Code shown below:

void ActivateBattleResultsUI()
{
	if (UnitMovementUI.activeInHierarchy)
		UnitMovementUI.SetActive(false);
	if (BattlesDetectedPanel.activeInHierarchy)
		BattlesDetectedPanel.SetActive(false);
	if (ChooseCardsPanel.activeInHierarchy)
		ChooseCardsPanel.SetActive(false);
	if (!BattleResultsPanel.activeInHierarchy)
		BattleResultsPanel.SetActive(true);

	localPlayerBattlePanel.transform.SetParent(BattleResultsPanel.GetComponent<RectTransform>(), false);
	opponentPlayerBattlePanel.transform.SetParent(BattleResultsPanel.GetComponent<RectTransform>(), false);

	if (LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().isPlayerViewingTheirHand)
	{
		LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().HidePlayerHandOnScreen();
	}

	if (isPlayerViewingOpponentHand && playerHandBeingViewed != null)
	{
		playerHandBeingViewed.GetComponent<PlayerHand>().HidePlayerHandOnScreen();
		playerHandBeingViewed = null;
		isPlayerViewingOpponentHand = false;
	}
	SetOpponentBattleScoreAndCard();
}

The first thing ActivateBattleResultsUI does is deactivate and activate UI panels as needed

Then, the PlayerBattlePanels are moved to the BattleResultsPanel

And then, finally, a call is made to SetOpponentBattleScoreAndCard, a function that needs to be created! shown below:

void SetOpponentBattleScoreAndCard()
{
	GamePlayer opponentGamePlayer = GameObject.FindGameObjectWithTag("GamePlayer").GetComponent<GamePlayer>();
	if (opponentGamePlayer.playerBattleCardNetId > 0)
	{
		//find opponent card and reposition it under the opponent battle panel
		GameObject opponentSelectedCard = NetworkIdentity.spawned[opponentGamePlayer.playerBattleCardNetId].gameObject;
		opponentGamePlayer.selectedCard = opponentSelectedCard;
		opponentSelectedCard.SetActive(true);
		opponentSelectedCard.transform.SetParent(opponentPlayerBattlePanel.transform);
		opponentSelectedCard.transform.localPosition = new Vector3(-27f, -110f, 1f);
		opponentSelectedCard.transform.localScale = new Vector3(70f, 70f, 1f);

		//update the opponent's score
		int opponentBattleScore = opponentGamePlayer.playerBattleScore + opponentSelectedCard.GetComponent<Card>().Power;
		opponentCardPower.GetComponent<Text>().text = opponentBattleScore.ToString();
		opponentSelectCardText.SetActive(true);
		opponentCardText.SetActive(true);
		opponentCardPower.SetActive(true);
	}
}

SetOpponentBattleScoreAndCard looks for your opponents played card and then displays it under their battle panel. It then calculates their battle score locally and displays it in their battle panel as well. If you save everything and test it out now, you should see something similar to the below image after both player’s have chosen a card.

Now, all that needs to be updated is the information in the ResultsPanel under BattleResultsPanel. I was having some syncing issues when trying to update that info in SetOpponentBattleScoreAndCard, so I created some new syncvars in GameplayManager.cs that all have hook functions that will be used to update their components of the ResultsPanel. The new SyncVars are shown below:

[SyncVar(hook = nameof(HandleAreBattleResultsSet))] public bool areBattleResultsSet = false;
private bool updateResultsPanelLocal = false;
[SyncVar(hook = nameof(HandleAreUnitsLostCalculated))] public bool unitsLostCalculated = false;
private bool unitsLostCalculatedLocal = false;
[SyncVar(hook = nameof(HandleUnitsLostFromRetreat))] public bool unitsLostFromRetreat = false;
private bool localUnitsLostFromRetreat = false;

The first hook function to be called will be HandleAreBattleResultsSet and its corresponding UpdateResultsPanel function which will update the winner name, victory condtion, and if its a draw, the units lost.

public void HandleAreBattleResultsSet(bool oldValue, bool newValue)
{
	if (isServer)
	{
		areBattleResultsSet = newValue;
	}
	if (isClient && newValue && !updateResultsPanelLocal)
	{
		UpdateResultsPanel();
		updateResultsPanelLocal = true;
	}
}
void UpdateResultsPanel()
{
	Debug.Log("Executing UpdateResultsPanel");
	winnerName.text = winnerOfBattleName;
	victoryCondition.text = reasonForWinning;
	updateResultsPanelLocal = true;
	if (reasonForWinning == "Draw: No Winner")
	{
		unitsLost.text = "No units lost";
	}
}

Now HandleAreBattleResultsSet just needs to be called so the value is updated. That is down from GamePlayer.cs’s DetermineWhoWonBattle function:

GameplayManager.instance.HandleAreBattleResultsSet(GameplayManager.instance.areBattleResultsSet, true);

The next hook function is HandleAreUnitsLostCalculated:

public void HandleAreUnitsLostCalculated(bool oldValue, bool newValue)
{
	if (isServer)
	{
		unitsLostCalculated = newValue;
	}
	if (isClient && unitsLostCalculated && !unitsLostCalculatedLocal)
	{
		UpdateUnitsLostValues();
		ShowDeadUnits();
		unitsLostCalculatedLocal = true;
	}
}

HandleAreUnitsLostCalculated will require two new functions to be created, UpdateUnitsLostValues and ShowDeadUnits. UpdateUnitsLostValues is fairly straightforward and is shown below:

void UpdateUnitsLostValues()
{
	Debug.Log("Executing UpdateUnitsLostValues");
	unitsLost.text = "";
	if (numberOfInfLost == 0 && numberOfTanksLost == 0)
	{
		Debug.Log("no units were lost in the battle");
		unitsLost.text = loserOfBattleName + " lost 0 units.";
	}
	else
	{
		Debug.Log("Units lost from " + loserOfBattleName + ". Tanks: " + numberOfTanksLost.ToString() + " infantry: " + numberOfInfLost.ToString());
		unitsLost.text = loserOfBattleName + " lost\n";
		if (numberOfTanksLost > 0)
		{
			Debug.Log("Tanks lost.");
			unitsLost.text += numberOfTanksLost.ToString() + " tanks\n";
		}
		if (numberOfInfLost > 0)
		{
			Debug.Log("Infantry lost.");
			unitsLost.text += numberOfInfLost.ToString() + " infantry";
		}
	}
}

UpdateUnitsLostValues just goes through and updates the unitsLost text with the values saved in GameplayManager.cs. It’s pretty simple.

ShowDeadUnits is the next function that needs to be added for HandleAreUnitsLostCalculated, and that one is a bit more involved…

ShowDeadUnits

The code for ShowDeadUnits is shown below:

void ShowDeadUnits()
{
	Debug.Log("Executing ShowDeadUnits");
	NetworkIdentity.spawned[currentBattleSite].gameObject.GetComponent<LandScript>().ExpandLosingUnits(loserOfBattlePlayerNumber);

	foreach (uint unitNetId in unitNetIdsLost)
	{
		NetworkIdentity.spawned[unitNetId].gameObject.GetComponent<UnitScript>().SpawnUnitDeadIcon();
	}
}

ShowDeadUnits looks fairly simple, but as you can see, it involves two new functions that don’t exist yet. The first is “ExpandLosingUnits” which is added to LandScript.cs, and the second is SpawnUnitDeadIcon added to UnitScript.cs. Let’s go through those!

ExpandLosingUnits in LandScript.cs

ShowDeadUnits finds the gameobject of the currentBattleSite and then calls ExpandLosingUnits from its LandScript.cs script. So what does ExpandLosingUnits do? Well, it will “expand” the units of the retreating player out on a land tile so it becomes more obvious what units need to retreat from the battle and what units don’t.

The first thing that will need to be done in LandScript.cs is to change the variable BattleUnitTexts from a List of GameOBjects to a dictionary of GameObjects and integars. The integar will be the corresponding player’s player number. When the retreating player’s units are expanded, the corresponding unit texts will need to be removed, and this will help make it easier to know when unit texts to remove. So, create the new dictionary:

public Dictionary<GameObject, int> BattleUnitTexts = new Dictionary<GameObject, int>();

A lot of changes now need to be made to LandScript.cs to fix all the new errors that were just created. First, change all the BattleUnitTexts.Add calls to include the player number, like this:

BattleUnitTexts.Add(player1InfText,1);

and this:

BattleUnitTexts.Add(player2InfText, 2);

BattleUnitTexts.Add should be used 4 times in UnitTextForBattles.

Next you will need to update HideUnitText. Change this:

foreach (GameObject battleText in BattleUnitTexts)
{
	if (battleText)
		battleText.SetActive(false);
}

to this:

foreach (KeyValuePair<GameObject, int> battleText in BattleUnitTexts)
{
	if (battleText.Key)
		battleText.Key.SetActive(false);
}

The in UnHideUnitText, change this:

foreach (GameObject battleText in BattleUnitTexts)
            {
                if (battleText)
                    battleText.SetActive(true);
            }

to this:

foreach (KeyValuePair<GameObject, int> battleText in BattleUnitTexts)
{
	if (battleText.Key)
		battleText.Key.SetActive(true);
}

Now, you should be ready to create the ExpandLosingUnits function! Code shown below:

public void ExpandLosingUnits(int losingPlayerNumber)
{
	if (losingPlayerNumber != -1)
	{
		Debug.Log("Expanding the units of player number: " + losingPlayerNumber.ToString());
		List<GameObject> losingPlayerTanks = new List<GameObject>();
		List<GameObject> losingPlayerInf = new List<GameObject>();
		Vector3 winngingPlayerPosition = new Vector3(0, 0, 0);

		foreach (KeyValuePair<uint, int> units in UnitNetIdsAndPlayerNumber)
		{
			if (units.Value == losingPlayerNumber)
			{
				GameObject unit = NetworkIdentity.spawned[units.Key].gameObject;
				if (unit.gameObject.tag == "tank")
					losingPlayerTanks.Add(unit);
				else if (unit.gameObject.tag == "infantry")
					losingPlayerInf.Add(unit);
			}
			else
				winngingPlayerPosition = NetworkIdentity.spawned[units.Key].gameObject.transform.position;
		}
		Vector3 temp;
		int playerXMultiplier = 0;
		if (losingPlayerInf.Count > 1)
		{
			if (losingPlayerInf[0].transform.position.x > winngingPlayerPosition.x)
				playerXMultiplier = 1;
			else
				playerXMultiplier = -1;
		}
		else if (losingPlayerTanks.Count > 1)
		{
			if (losingPlayerTanks[0].transform.position.x > winngingPlayerPosition.x)
				playerXMultiplier = 1;
			else
				playerXMultiplier = -1;
		}
		if (playerXMultiplier != 0)
			ExpandUnitsForBattleResults(losingPlayerTanks, losingPlayerInf, playerXMultiplier);
		//remove battle texts for losing player
		List<GameObject> losingPlayerBattleTextToDestroy = new List<GameObject>();
		foreach (KeyValuePair<GameObject, int> battleText in BattleUnitTexts)
		{
			if (battleText.Value == losingPlayerNumber)
			{
				losingPlayerBattleTextToDestroy.Add(battleText.Key);
			}
		}
		foreach (GameObject textToDestroy in losingPlayerBattleTextToDestroy)
		{
			BattleUnitTexts.Remove(textToDestroy);
			GameObject textObject = textToDestroy;
			Destroy(textObject);
			textObject = null;
		}
		losingPlayerBattleTextToDestroy.Clear();
	}
}

ExpandLosingUnits does the following:

  • Takes the player number as an argument
  • If the losing player value isn’t -1 (meaning there was a draw), executes everything in the function
  • Creates new lists that will store the losing player’s tanks and infantry objects
  • Creates a new vector3 winngingPlayerPosition that will be used to determine where the losing players units are in relation to the winner’s (are the loser’s units to the right or left of the winner’s units)
  • Goes through each keyvaluepair in UnitNetIdsAndPlayerNumber, the units on the land
    • if the unit’s player number matches the losing player’s player number, store that unit in either the tank or infantry list
    • stores the transform.position value of one of the winner’s units in winngingPlayerPosition
  • The next bit determines whether the loser’s units are to the right or left of the winner’s units.
    • This will be tracked with a “multiplier” called playerXMultiplier
    • If the loser is to the right of the winner (the X value of a loser’s unit is greater than the x value of a winner’s unit) the playerXMultiplier value is set to 1.
    • If the x value of the loser is less than the winner’s, the loser is to the left of the winner and playerXMultiplier is set to (positive) 1.
  • If
  • Takes the player number as an argument
  • If the losing player value isn’t -1 (meaning there was a draw), executes everything in the function
  • Creates new lists that will store the losing player’s tanks and infantry objects
  • Creates a new vector3 winngingPlayerPosition that will be used to determine where the losing players units are in relation to the winner’s (are the loser’s units to the right or left of the winner’s units)
  • Goes through each keyvaluepair in UnitNetIdsAndPlayerNumber, the units on the land
    • if the unit’s player number matches the losing player’s player number, store that unit in either the tank or infantry list
    • stores the transform.position value of one of the winner’s units in winngingPlayerPosition
  • The next bit determines whether the loser’s units are to the right or left of the winner’s units.
    • This will be tracked with a “multiplier” called playerXMultiplier
    • If the loser is to the right of the winner (the X value of a loser’s unit is greater than the x value of a winner’s unit) the playerXMultiplier value is set to 1.
    • If the x value of the loser is less than the winner’s, the loser is to the left of the winner and playerXMultiplier is set to (positive) 1.
  • If playerXMultiplier is not 0, ExpandUnitsForBattleResults is called with the losingPlayerTanks and losingPlayerInf lists and the playerXMultiplier value provided as arguments. ExpandUnitsForBattleResults will be created next!
  • Now the battle unit texts will be dealt with
    • If the battle text belongs to the losing player, its gameobject is saved in a list losingPlayerBattleTextToDestroy.
    • After losingPlayerBattleTextToDestroy is populated, all the gameobjects in it are destroyed.

So, now, about that ExpandUnitsForBattleResults function. That will be what actually “expands” and moves the unit sprites to the correct positions. Code is shown below:

void ExpandUnitsForBattleResults(List<GameObject> tanks, List<GameObject> inf, int playerXMultiplier)
{
	Vector3 temp;
	if (inf.Count > 1)
	{
		for (int i = 1; i < inf.Count; i++)
		{
			if (i == 1)
			{
				temp = inf[i].transform.position;
				temp.x += (0.65f * playerXMultiplier);
				inf[i].transform.position = temp;
			}
			else if (i == 2)
			{
				temp = inf[i].transform.position;
				temp.y -= 0.8f;
				inf[i].transform.position = temp;
			}
			else if (i == 3)
			{
				temp = inf[i].transform.position;
				temp.y -= 0.8f;
				temp.x += (0.65f * playerXMultiplier);
				inf[i].transform.position = temp;
			}
			else if (i == 4)
			{
				temp = inf[i].transform.position;
				temp.y += 0.8f;
				inf[i].transform.position = temp;
			}
		}
	}
	if (tanks.Count > 1)
	{
		for (int i = 1; i < tanks.Count; i++)
		{
			if (i == 1)
			{
				temp = tanks[i].transform.position;
				temp.x += (0.95f * playerXMultiplier);
				tanks[i].transform.position = temp;
			}
			else if (i == 2)
			{
				temp = tanks[i].transform.position;
				temp.y += 0.6f;
				tanks[i].transform.position = temp;
			}
			else if (i == 3)
			{
				temp = tanks[i].transform.position;
				temp.y += 0.6f;
				temp.x += (0.95f * playerXMultiplier);
				tanks[i].transform.position = temp;
			}
		}
	}
}

ExpandUnitsForBattleResults goes through the tank and infantry unit lists and moves them to specific spots relative to the first unit. These spots are similar but not the same (due to space issues) as the spaces the units are moved when a player first clicks on a unit to select them. The playerXMultiplier is used to make the unit either on the left or right of the land tile.

That takes care of expanding retreating units, for now. Later this will be updated for when both players have to retreat (in the draw scenario). But for now, let’s move on.

SpawnUnitDeadIcon in UnitScript.cs

One new exciting thing that will happen is that when a unit is “killed” it will be marked with a sprite showing the unit crossed out. It will be this sprite, specifically

Save the sprite to your Sprites directory. Then, drag the sprite into any scene in the Unit editor. Set the following for it:

Setting the sorting layer to 3 will be important to make sure it renders over a unit.

Then, drag the soldier-dead-icon into the OfflinePrefabs>UnitPrefabs directory.

Duplicate the soldier-dead-icon prefab, and rename it to tank-dead-icon.

Select the tank-dead-icon and set its scale values to X:0.25,y:0.25.

In UnitScript.cs, create some new variables to store the new dead icon sprite prefab.

[Header("Unit Outlines and Icons")]
[SerializeField] GameObject unitDeadIconPrefab;
GameObject unitDeadIconObject;

Save UnitScript.cs. You will need to comment out the errors in GameplayMAnager.cs to get the UnitScript section of the unit prefabs to update. When they do, go through each soldier and tank prefab and add the appropriate dead icon prefab to them.

Once that is done, remove any dead-icon prefabs from the scene and then head on back into UnitScript.cs. It’s time to create SpawnUnitDeadIcon, which is rather simple:

public void SpawnUnitDeadIcon()
{
	if (!unitDeadIconObject)
	{
		unitDeadIconObject = Instantiate(unitDeadIconPrefab, transform.position, Quaternion.identity);
		unitDeadIconObject.transform.SetParent(gameObject.transform);
	}
}

Then, in GamePlayer.cs, go to the UnitsLostFromBattle function, and add the following to the end.

GameplayManager.instance.HandleAreUnitsLostCalculated(GameplayManager.instance.unitsLostCalculated, true);

Phew, well, let’s saved everything, comment out [SyncVar(hook = nameof(HandleUnitsLostFromRetreat))] public bool unitsLostFromRetreat = false; in GameplayManager.cs for now to get rid of any errors, then build and run to test. It should look like this with expanded units and units marked as killed.

One thing I am now noticing is that the units look to be “behind” the battle site outline. This is because both the units and the outline are using the “default” sorting layer and are both “2” in the order in the layer. To get around this, I created a new sorting layer called “Units” and made it the second layer after default.

I then when through each infantry and tank prefab, as well as their respectice outline prefabs, and changed their sorting layer to Units and set the order to 0.

The dead-icon prefabs can be set to Units and be 1 in the order so they appear on top of the units. They should now appear like this:

HandleUnitsLostFromRetreat

The last hook function to create, at least for upadting the Battle Results Panel, is HandleUnitsLostFromRetreat. This hook function will execute if any units are lost because they couldn’t retreat, and will simply activate that text to indicate that to the players.

The code is shown below:

public void HandleUnitsLostFromRetreat(bool oldValue, bool newValue)
{
	if (isServer)
	{
		unitsLostFromRetreat = newValue;
	}
	if (isClient && newValue && !localUnitsLostFromRetreat)
	{
		Debug.Log("Updating HandleUnitsLostFromRetreat to true");
		if (!retreatingUnitsDestroyed.activeInHierarchy)
			retreatingUnitsDestroyed.SetActive(true);
		localUnitsLostFromRetreat = true;
	}
}

In GameplayManager.cs’s CanPlayerRetreatFromBattle function, I had HandleUnitsLostFromRetreat called but commented out. Now, just uncomment those lines and it should work!

GameplayManager.instance.HandleUnitsLostFromRetreat(GameplayManager.instance.unitsLostFromRetreat, true);

Now when you build and save and test, you should see the battle results set properly!

Revealing Units Near Battle Site

Now that an inability to retreat can cause units to be killed, the players should be able to know what units are on what tiles surrounding the battle site. This will help them plan out attacks and card playing strategies better, and help them anticipate what units will be lost for the losing player. Right now, though, all the surround units are invisible when the player’s are picking cards. When I was testing things out, I would often go into the unity editor and manually reactivate the unit sprites so I could see what tiles could be retreated to and what to expect in the battle results.

So, to make sure that players also have that information, and to not force them to have to memorize unit layouts before the battles, I wanted to add a feature to the Choose Cards UI to display units on the land tiles surrounding the battle site.

In the Gameplay scene, activate the choose cards panel. Duplicate the confirmChooseCard button, rename it to showUnitsButton, and make sure it is active in the hierarchy.

Select the button, and then in the Inspector window set the following for the RectTransform values.

Then, change the text of the button to “Show Nearby Units.” The button should look like this now in the game scene:

The click functions for the button will now be created to show the units. Add two new variables to GameplayManager.cs. One is to store the button object, and the other a boolean to flag when the player is viewing nearby units or not.

[SerializeField] private GameObject showNearybyUnitsButton;
public bool showingNearbyUnits = false;

Then, two new functions will be added, ShowUnitsOnMap and HideUnitsOnMap. The code is shown below:

public void ShowUnitsOnMap()
{
	if (!showingNearbyUnits)
	{
		bool isPlayerviewingTheirHand = LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().isPlayerViewingTheirHand;
		if (!isPlayerViewingOpponentHand && !isPlayerviewingTheirHand)
		{
			//unhide the units on nearby land
			GameObject battleSite = NetworkIdentity.spawned[currentBattleSite].gameObject;
			GameObject allLand = GameObject.FindGameObjectWithTag("LandHolder");
			foreach (Transform landObject in allLand.transform)
			{
				float disFromBattle = Vector3.Distance(landObject.transform.position, battleSite.transform.position);
				if (disFromBattle < 3.01f)
				{
					LandScript landScript = landObject.gameObject.GetComponent<LandScript>();
					landScript.UnHideUnitText();
					landScript.UnHideBattleHighlight();
					if (landScript.UnitNetIdsAndPlayerNumber.Count > 0)
					{
						foreach (KeyValuePair<uint, int> unitOnLand in landScript.UnitNetIdsAndPlayerNumber)
						{
							GameObject unitObject = NetworkIdentity.spawned[unitOnLand.Key].gameObject;
							if (!unitObject.activeInHierarchy)
								unitObject.SetActive(true);
						}
					}
				}
			}
			showNearybyUnitsButton.GetComponentInChildren<Text>().text = "Hide Nearby Units";
			showingNearbyUnits = true;
		}
	}
	else
		HideUnitsOnMap();
}
void HideUnitsOnMap()
{
	HideNonBattleUnits();
	HideNonBattleLandTextAndHighlights();
	showNearybyUnitsButton.GetComponentInChildren<Text>().text = "Show Nearby Units";
	showingNearbyUnits = false;
}

ShowUnitsOnMap and HideUnitsOnMap do the following:

  • If the player is not already viewing nearby units or their own or their opponents cards
    • get all land objects from the LandHolder object
    • iterate through each land object
      • if the land object is one tile away, unhide units and unit texts
    • change the text of the button
    • set showingNearbyUnits to true
  • If showingNearbyUnits is true, call HideUnitsOnMap
    • HideUnitsOn map makes calls to HideNonBattleUnits and HideNonBattleLandTextAndHighlights to hide any units or unit text that is not on the battle site

I experienced some issues when nearby units were being displayed and when the player was viewing their hand or their opponent’s hand, so I added a check in PlayerHand.cs’s HidePlayerHandOnScreen to call ShowUnitsOnMap if it is the Choose Cards phase.

if (GameplayManager.instance.showingNearbyUnits)
	GameplayManager.instance.ShowUnitsOnMap();

Back in the Unity Edtior, make sure that the On Click function for the showUnitsButton is set to GameplayManager.cs’s ShowUnitsOnMap function.

Make sure that the showUnitsButton is attached to the GameplayManager object.

Then, when you build and run to test, you should see units appear on the map surrounding the battle site when you click the button

Ending Battle Results

Everything for the Battle Results phase should now be done. The main thing now will be to do some “cleanup” of things after the battle is over. Each player’s played card should be “discarded” and put into their discard pile. Any units lost should be removed from the game.

In GamePlayer.cs’s CheckIfAllPlayersAreReadyForNextPhase function, a new check will be added to advance the game phase from Battle Results to Retreat Units.

if (Game.CurrentGamePhase == "Battle Results")
{
	Game.CurrentGamePhase = "Retreat Units";
	Debug.Log("Game phase changed to Retreat Units");
	//CheckWhichPlayersNeedToRetreat();
	DestroyUnitsLostInBattle();
	MovePlayedCardToDiscard();
	RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
	return;
}

As part of advancing the phase, two new functions will be called, DestroyUnitsLostInBattle and MovePlayedCardToDiscard. So, let’s crete them!

DestroyUnitsLostInBattle

DestroyUnitsLostInBattle will be a server function in GamePlayer.cs that will go through the unit network ids that have been saved in GameplayManager.cs’s unitNetIdsLost list. The code is shown below:

[Server]
void DestroyUnitsLostInBattle()
{
	Debug.Log("Executing DestroyUnitsLostInBattle on the server");
	if (GameplayManager.instance.unitNetIdsLost.Count > 0)
	{
		Debug.Log("unitNetIdsLost is greater than 0, must destroy this many units: " + GameplayManager.instance.unitNetIdsLost.Count.ToString());
		foreach (uint unitNetId in GameplayManager.instance.unitNetIdsLost)
		{
			UnitScript unitNetIdScript = NetworkIdentity.spawned[unitNetId].gameObject.GetComponent<UnitScript>();

			foreach (GamePlayer gamePlayer in Game.GamePlayers)
			{
				if (gamePlayer.ConnectionId == unitNetIdScript.ownerConnectionId && gamePlayer.playerNumber == unitNetIdScript.ownerPlayerNumber)
				{
					if (gamePlayer.playerUnitNetIds.Contains(unitNetId))
						gamePlayer.playerUnitNetIds.Remove(unitNetId);
					break;
				}
			}
			GameObject unitToDestroy = NetworkIdentity.spawned[unitNetId].gameObject;
			if (unitToDestroy)
				NetworkServer.Destroy(unitToDestroy);
			Debug.Log("Server destroyed unit with network id: " + unitNetId.ToString());
			unitToDestroy = null;
		}
	}
}

DestroyUnitsLostInBattle goes through each unit in unitNetIdsLost and checks which player that unit belonged to. When that player is found, the unit’s network id is removed from the player’s playerUnitNetIds list. playerUnitNetIds is used by the server to make sure that the player is only taking actions on “valid” units and so on. After the unit is removed from playerUnitNetIds, the unit gameobject is then destroyed using NetworkServer.Destroy. This destroys the gameobject on the server, and then all the clients will destroy the gameobject as well.

MovePlayedCardToDiscard

After a player plays a card in a battle, it will be added to their discard pile. To do this, first some new functions will be added to the PlayerHand.cs script.

In PlayerHand.cs, first a server function called MoveCardToDiscard to discard will be created.

[Server]
public void MoveCardToDiscard(uint cardtoDiscardNetId)
{
	Debug.Log("Executing MoveCardToDiscard to discard card with network id: " + cardtoDiscardNetId.ToString());
	if (HandNetId.Contains(cardtoDiscardNetId))
		HandNetId.Remove(cardtoDiscardNetId);
	if (!DiscardPileNetId.Contains(cardtoDiscardNetId))
		DiscardPileNetId.Add(cardtoDiscardNetId);

	// If cards in the hand still remain, have player remove cards locally and stuff
	if (HandNetId.Count > 0)
	{
		RpcMoveCardToDiscard(cardtoDiscardNetId);
	}
}

MoveCardToDiscard takes a card’s network id as an argument. It then checks if that card exists in the HandNetId list, and if it does, removes it. It then adds the card to the DiscardPileNetId list. If there are still cards left in the Hand list, a call to the ClientRpc function RpcMoveCardToDiscard is made. Code for RpcMoveCardToDiscard is shown below:

[ClientRpc]
void RpcMoveCardToDiscard(uint cardtoDiscardNetId)
{
	GameObject cardToDiscard = NetworkIdentity.spawned[cardtoDiscardNetId].gameObject;
	if (cardToDiscard)
	{
		if (Hand.Contains(cardToDiscard))
			Hand.Remove(cardToDiscard);
		if (!DiscardPile.Contains(cardToDiscard))
			DiscardPile.Add(cardToDiscard);

		// If the card is not a child of the PlayerCardHand object, set it as a child of the PlayerCardHand object
		if (!cardToDiscard.transform.IsChildOf(this.transform))
			cardToDiscard.transform.SetParent(this.transform);
		if (cardToDiscard.activeInHierarchy)
			cardToDiscard.SetActive(false);
	}
}

RpcMoveCardToDiscard will tell the clients to find the discarded card based on its network id, and then add it to the DiscardPile list of gameobjects. It will then make sure that the card is a child object of the PlayerHand object and no longer the child of any player battle panels or anything like that.

Advancing the Game Phase

Now, finally (I think?), some preliminary code can be added to advance the phase from Battle Results to Retreat Units. Nothing too fancy, just making sure to set the gamephase text and deactivate other UI panels and stuff.

In GameplayManager.cs’s ChangeGamePhase function, add a check for when the phase changes to Retreat Units

if (currentGamePhase == "Battle Results" && newGamePhase == "Retreat Units")
{
	MouseClickManager.instance.canSelectPlayerCardsInThisPhase = false;
	currentGamePhase = newGamePhase;
	StartRetreatUnits();
}

Then, create a StartRetreatUnits function that will set the game phase text and call ActivateRetreatUnitsUI. For now, ActivateRetreatUnitsUI will simply deactivate older phase UI’s and reset the camera to “normal.”

void StartRetreatUnits()
{
	SetGamePhaseText();
	ActivateRetreatUnitsUI();
}
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);

	//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;
}

Everything should be good to test now. I created a battle scenario where player 2 should lose 2 tanks

When I advance the phase, I see that the tanks are no longer visible on the clients

If I look at the GamePlayer object for the player, I see that the list of the unit network ids they own is now only 8 units long instead of 10.

And, if you go to the PlayerHand objects for each player, you can see that one card is now in the discard pile list

So, it all works? Amazing!

Next Steps…

Next, I’ll need to create the Retreat Units phase. That should do the following:

  • Check if any players actually need to retreat
    • If all units of the retreating players were destroyed, there is nothing to retreat. So, skip retreat units phase…
  • Make sure that the retreating player retreats all their units
  • Re-arrange the units that are expanded and change some things in terms of what happens when the retreating player selects their units to move
  • A bunch of cleanup from the battle phases. Losts of syncvar booleans and local booleans to set back to false in prep for the next battle. Will be fun to keep track of all that!
  • After Retreat Units is over (or if it is skipped), do the following:
    • Check if there are any more battles to fight
      • If there are, go to the next battle in the list
      • If there AREN’T, go to the next Unit Movement phase
  • Try to figure out how to deal with draw scenarios and both players have to retreat…

Smell ya later nerds!

3 comments

  1. Pingback: spiraldynamics
  2. Pingback: Spiral Dynamics
  3. Pingback: vxi.su

Comments are closed.