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.
It is now time to work on ending the game. Ending!? Playing a complete game? Making something that could actually be called a game? Yes, it is a surprise to me as well that this is happening. So, let’s see what should cause a game to end:
- One player loses all their units
- The game as is requires a player to have units to move and fight with. If you have no units, you lose
- A player base is captured
- Bases can be “captured” by winning a battle on a player base tile:
- If you capture a player’s base, you win!
- If you fail to defend your base and it is captured, you lose…
- Bases can be “captured” by winning a battle on a player base tile:
Let’s get to it then!
Some “Game Over” UI
First things first, a new UI will be created to be the “Game Over” UI that tells players the game is over. It will tell you if you’ve won or if you’ve lost, and give you a button to then quit the game. This will all remain in the Gameplay scene. To start, duplicate the BattleResultsPanel:

Rename the BattleresultsPanel to EndGamePanel, and then rename the button and child panel to endGame and EndGameResults respectively.

Select the endGame button and change its anchor to bttom center, its Pos X to 0, and its Pos Y to 30.

Then, set the Text of the endGame button to “Quit Game” and the font size to 50.

Now, select the EndGameResults panel and set the following for its rect transform:
- Anchor: middle center
- Pos X: 0
- Width: 500

Now, create two new empty game objects called GameWinnerTextObjects and GameLoserTextObjects

For all the old text objects that were copied when BattleResultsPanel was duplicated, delete everything except for BattleWinner, WinnerName, and VictoryCondition. Make all 3 of those text objects children of GameWinnerTextObjects.

Rename those text objects to YouWinText, VanguishedText, and ContinuedGoodFortune.

Select YouWinText and set its width to 490 and its height to 75

Then, set the text to “You Won!” and the font size to 75.

Select VanguishedText and set the following:
- Pos Y: 100
- Width: 490
- Height: 150
- Text: “All your enemies were vanguished”
- Font Size: 60

Then, select ContinuedGoodFortune and set the following:
- Pos Y: -130
- Width: 490
- Height: 300
- Text: “May your good fortune continue into the next fight”
- Font Size: 75

You should now see the following as the “winner” Game Over UI

Now, select those three child text objects, duplicate them, and move them under GameLoserTextObjects. Rename them to YouLoseText, ReasonForLoss, BetterLuckNextTime:

All of the position and size values for the text objects should remain the same. For YouLoseText, set the text to “You Lost!” For ReasonForLoss, set the text to “All your units were killed” and the font size to 75. Set the text for BetterLuckNextTime to “may your luck be better next time…”

You can then deactivate both the GameWinnerTextObjects and GameLoserTextObjects.
Adding GameOver UI to GameplayManager.cs
As the heading suggests, the Game Over UI stuff will be added to GameplayManager.cs so that it can later be manipulated through scripting. Add the following variables to GameplayManager.cs:
[Header("End Game UI")]
[SerializeField] private GameObject EndGamePanel;
[SerializeField] private GameObject GameLoserTextObjects;
[SerializeField] private Text ReasonForLossText;
[SerializeField] private GameObject GameWinnerTextObjects;

Then, back in the Unity Editor, attach the objects to their corresponding variables. After they are attached, make sure to deactivate the EndGamePanel.

Ending the Game: All Units Are Killed
Now that the UI has been created, it’s time make use of it by making a end game scenario. The first scenario will be to detect when a player has lost all their units, and end the game when that happens.
This will be detected in GamePlayer.cs’s DestroyUnitsLostInBattle function. Start by creating a new boolean variable called didPlayerLoseAllUnits before the foreach loop that goes through each unit lost:

Then, after the lost unit is removed from the player’s unit lists, create a check to see if the player has any remaining units by getting a count of the playerUnitNetIds list. If the count is 0, the player has no more remaining units and didPlayerLoseAllUnits can be set to true.
if (gamePlayer.playerUnitNetIds.Count == 0)
{
Debug.Log("No more units remaining for: " + gamePlayer.PlayerName);
didPlayerLoseAllUnits = true;
}

Then, outside of the foreach loop, create a check for if didPlayerLoseAllUnits was ever set to true. If so, make a call to a (not yet created) function called CheckForWinnerFromLostUnits.
if (didPlayerLoseAllUnits)
{
Debug.Log("At least one player has no more remaining units after the battle.");
CheckForWinnerFromLostUnits();
}

CheckForWinnerFromLostUnits will be used to check if a player has lost all their units or not. Before creating CheckForWinnerFromLostUnits, some SyncVars and hook functions will need to be created. At the top of GamePlayer.cs, add the following:
[Header("End of Game Statuses")]
[SyncVar(hook = nameof(HandlePlayerWin))] bool didPlayerWin = false;
[SyncVar(hook = nameof(HandlePlayerEliminatedFromUnitsLost))] bool wasPlayerEliminatedFromUnitsLost = false;
[SyncVar(hook = nameof(HandlePlayerEliminatedFromBaseCaptured))] bool wasPlayerEliminatedFromBaseCaptured = false;

Then the hook functions can be created. For right now, leave them as “empty” functions that just set the SyncVar to the newValue passed. Later, these will be used to call the GameOver UI stuff and end the game for the player when they are set.
void HandlePlayerWin(bool oldValue, bool newValue)
{
if (isServer)
didPlayerWin = newValue;
if (newValue && hasAuthority)
{
}
}
void HandlePlayerEliminatedFromUnitsLost(bool oldValue, bool newValue)
{
if (isServer)
wasPlayerEliminatedFromUnitsLost = newValue;
if (newValue && hasAuthority)
{
}
}
void HandlePlayerEliminatedFromBaseCaptured(bool oldValue, bool newValue)
{
if (isServer)
{
wasPlayerEliminatedFromBaseCaptured = newValue;
}
if (newValue && hasAuthority)
{
}
}

Now, it’s time to create the CheckForWinnerFromLostUnits function. The code for it is shown below:
[Server]
void CheckForWinnerFromLostUnits()
{
Debug.Log("Executing CheckForWinnerFromLostUnits on the server.");
List<GamePlayer> playersWithNoUnits = new List<GamePlayer>();
List<GamePlayer> playersWithUnits = new List<GamePlayer>();
foreach (GamePlayer gamePlayer in Game.GamePlayers)
{
if (gamePlayer.playerUnitNetIds.Count == 0)
playersWithNoUnits.Add(gamePlayer);
else
playersWithUnits.Add(gamePlayer);
}
if (playersWithUnits.Count == 1)
{
Debug.Log("Only one player still has units. That player wins the game. Player is: " + playersWithUnits[0].PlayerName);
playersWithUnits[0].HandlePlayerWin(playersWithUnits[0].didPlayerWin, true);
}
else
{
Debug.Log("More than 1 player with units remain. Game still continues...");
}
foreach (GamePlayer loser in playersWithNoUnits)
{
loser.HandlePlayerEliminatedFromUnitsLost(loser.wasPlayerEliminatedFromUnitsLost, true);
}
}

CheckForWinnerFromLostUnits does the following:
- Creates two new lists, playersWithNoUnits and playersWithUnits
- Loops through each player in Game.GamePlayers
- if the player’s playerUnitNetIds list is 0, add them to playersWithNoUnits
- If the playerUnitNetIds list is greater than 0, add them to playersWithUnits
- If the playersWithUnits list has a count of 1, that means only 1 player has remaining units. That player is then the winner
- call HandlePlayerWin for that player and set didPlayerWin to true
- If playersWithUnits’ count is greater than 1, then more than 1 player has units remaining and no winner is declared
- After the winner is/is not declared, all players in the playersWithNoUnits list have their wasPlayerEliminatedFromUnitsLost value set to true
Ending The Game
After a winner is declared, the game will end for everything. When a loser is declared, the game will end for the loser. A loser declared doesn’t necessarily mean there is a winner. In this game’s current format, a loser always means there is a loser because their are only 2 players, but in some theoretical future where there can be more than 2 players, that may not be case where only 1 player is eliminated and the other 2 are not.
Anyway, the loser/winner stuff will be handle in GameplayManager.cs to actually bring up the Game Over UI and so on.
In HandlePlayerWin, when newValue is true and the player has authority on that GamePlayer object, LocalPlayerVictory will be called. In HandlePlayerEliminatedFromUnitsLost, when newValue is true and the player has authority, LocalPlayerEliminatedByUnitsLost will be called.

So, in GameplayManager.cs, the LocalPlayerVictory and LocalPlayerEliminatedByUnitsLost can be created:
public void LocalPlayerVictory()
{
Debug.Log("Executing LocalPlayerVictory");
GameOverUI(false);
GameWinnerTextObjects.SetActive(true);
}
public void LocalPlayerEliminatedByUnitsLost()
{
Debug.Log("Executing LocalPlayerEliminatedByUnitsLost");
GameOverUI(true);
GameLoserTextObjects.SetActive(true);
ReasonForLossText.text = "all your units were killed";
}

These functions are pretty simple right now. The both set GameWinnerTextObjects or GameLoserTextObjects to true, depending on if the local player won or lost. If they lost, the text for ReasonForLossText is also set for the reason the player lost.
Both of them call a function called GameOverUI. This will be what actually activates the EndGamePanel. The code for GameOverUI is shown below:
void GameOverUI(bool isLoser)
{
//Deactivate all UI panels
if (UnitPlacementUI.activeInHierarchy)
UnitPlacementUI.SetActive(false);
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(false);
GamePhaseText.text = "Game Over";
EndGamePanel.SetActive(true);
if (!isServer && isLoser)
{
//NetworkClient.Disconnect();
LocalGamePlayerScript.QuitGame();
}
else if (isServer && isLoser)
LocalGamePlayerScript.HostLostGame();
}

GameOverUI takes 1 argument, isLoser. As the name suggests, if the player lost, this will be true. If they won, it will be false. All other UI panels are disabled and then the EndGamePanel is activated.
An important thing is if you are not the server and the loser of the game, LocalGamePlayerScript.QuitGame() will be called. If you are the server and the loser, LocalGamePlayerScript.HostLostGame(). These functions will be created next, but the main difference will be that QuitGame actually disconnects from the game, whereas HostLostGame just removes you from the Game.GamePlayer’s list. This is so that if the server loses but other players remain and no one has won yet, the game will continue.
Anyway…
Quiting the Game
So, when the non-server loses the game, they should quit the game by disconnecting from the server. So, a new function in GamePlayer.cs called QuitGame will be created. QuitGame will allow for the client to quit the game when they lose, and it will also be used by the server to close down the server when the host player clicks the endGame button in the Game Over UI. The code for QuitGame is shown below:
public void QuitGame()
{
if (hasAuthority)
{
if (isServer)
{
Game.StopHost();
Game.HostShutDownServer();
}
else
{
Game.StopClient();
Game.HostShutDownServer();
}
}
}
So, if the player is the host/server, StopHost will be called from “Game” (which is the NetworkManager). If they are the client, StopClient will be called.
Additionally, both will call HostShutDownServer from the NetworkManager. This is to make sure that the NetworkManager is destroyed and restarted properly so that players can go from being in one game and then joining a new game without restarting the client. As far as I can tell, this works in the game. But in order for it to work, HostShutDownServer needs to be added to NetworkManagerCC.cs (haven’t had to update that one in a while…). The code is shown below:
public void HostShutDownServer()
{
GameObject NetworkManagerObject = GameObject.Find("NetworkManager");
Destroy(NetworkManagerObject);
Shutdown();
Start();
}

So, HostShutDownServer finds the network manager object, destroys it, calls Shutdown, and then Start, to restart it.
Also note that GamePlayers.Clear()
is highlighted in OnStopServer
. I added this, though I’m not sure it is 100% required.
The next thing to add to GamePlayer.cs will be the HostLostGame function to remove the server from the game when they lose, but let the game continue. The code is shown below:
public void HostLostGame()
{
if (Game.GamePlayers.Contains(this))
Game.GamePlayers.Remove(this);
}
HostLostGame simply removes the player from the Game.GamePlayers list.
Quit the Game with the endGame Button
Finally, the endGame button will be made so it can quit the game. In GameplayManager.cs, a new QuitGame function will be created. Code shown below:
public void QuitGame()
{
try
{
LocalGamePlayerScript.QuitGame();
}
catch
{
Debug.Log("LocalGamePlayerScript no longer exists.");
}
//LocalGamePlayerScript.QuitGame();
SceneManager.LoadScene("TitleScreen");
}

The use of SceneManager.LoadScene to go back to the TitleScreen scene will require the using UnityEngine.SceneManagement;
reference to be included at the top of GameplayManager.cs.

Then, in the Unity Editor, add GameplayManager.cs’s QuitGame function to endGame’s on click:

Some Last Minute Fixes…
In order for this to work correctly, a few more things need to be added. In GamePlayer.cs’s CheckIfAllPlayersAreReadyForNextPhase function, just after the call DestroyUnitsLostInBattle under if (Game.CurrentGamePhase == "Battle Results")
, a new check will be added to see if any player’s have their didPlayerWin value set to true. If they do, the CurrentGamePhase will be set to “Game Over.” RpcAdvanceToNextPhase will then be called followed by a “return,” which makes it so the game doesn’t proceed to other game phases.
//Check if a player won the game after the battle. end the game if so
foreach (GamePlayer player in Game.GamePlayers)
{
if (player.didPlayerWin)
{
Game.CurrentGamePhase = "Game Over";
Debug.Log("Changing phase to Game Over");
RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
return;
}
}

If you save everything now, build and run, you should see the player lose the game when they lose all their units, like in this video!
Amazing! It works!
Ending the Game: Capturing a Player’s Base
The other win scenario for CardConquest is for a player to capture another player’s base. You will capture a player base if you win a battle on the opposing player’s base land tile. To know when a player base has been captured, the game first needs to know when a battle is taking place on a player base.
In GameplayManager.cs, a new SyncVar boolean called isPlayerBaseDefense will be created. When a battle is being fought on one of the player’s bases, isPlayerBaseDefense. There will also be a hook function that will take actions for UI and other things to make sure that the player’s are aware they are fighting a player base battle.
[Header("Player Base Defense")]
[SyncVar(hook = nameof(HandleIsPlayerBaseDefense))] public bool isPlayerBaseDefense = false;

For now, the hook function HandleIsPlayerBaseDefense will just be an empty hook function that sets isPlayerBaseDefense to the new value that is passed to it.
public void HandleIsPlayerBaseDefense(bool oldValue, bool newValue)
{
if (isServer)
{
isPlayerBaseDefense = newValue;
}
if (newValue)
{
Debug.Log("HandleIsPlayerBaseDefense: isPlayerBaseDefense set to true.");
}
else
{
Debug.Log("HandleIsPlayerBaseDefense: isPlayerBaseDefense set to false.");
}
}

When a player base is fought, I want the defending player to get a bit of a bonus in the fight. I want their battle score to be increased by 2 as part of their “defenses” on the base. Because of that, it seemed to make sense to have the game detect if it is a player base defense in GamePlayer.cs’s CmdSetGamePlayerArmy function where player’s have their battle scores set at the beginning of a battle.
The below code will be added to CmdSetGamePlayerArmy:
if (battleSite.transform.position == requestingPlayer.myPlayerBasePosition)
{
Debug.Log(requestingPlayer.PlayerName + " is defending their base. +2 to battle score");
requestingPlayer.playerBattleScore += 2;
GameplayManager.instance.HandleIsPlayerBaseDefense(GameplayManager.instance.isPlayerBaseDefense, true);
}

The new code checks to see if the requestingPlayer’s myPlayerBasePosition value matches the battle site’s transform.position. If they match, that means the requesting player is defending their own base. Their battle score is then increased by 2, and isPlayerBaseDefense in GameplayManager.cs is set to true.
Base Defense UI
Now that the base defense is being detected, the next thing to do is to create some UI to let both player’s know that it is happening. The UI will also say that this can end the game if the defending player loses.
The UI will be some simple text that will appear in both the Choose Cards and Battle Results phases.
In the unity editor, create a new empty object under the ChooseCardsPanel of GameplayUI. Rename it to PlayerBaseDefenseObjects. Then, create two new text objects called PlayerBaseDEfenseText and DefeatEnemyText.

Select PlayerBaseDEfenseText and set the following for it:

Then, select DefeatEnemyText and set the following:

Then, make sure that the PlayerBaseDefenseObjects parent object is deactivated in the hierarchy.

PlayerBaseDefenseObjects and its child objects can be duplicated and moved to the BattleResultsPanel. They can then be renamed to BattleResultsBaseDefenseObjects, PlayerBaseDefenseText (that stayed the same…), and WinOrLoseSuccess

These new UI objects can now be added to GameplayManager.cs so that they are appropriately activated and their text updated for base battles. In GameplayManager.cs, add the following variables:
[SerializeField] public GameObject PlayerBaseDefenseObjects;
[SerializeField] private Text DefeatEnemyText;
[SerializeField] public GameObject BattleResultsBaseDefenseObjects;
[SerializeField] private Text WinOrLoseDefenseText;

The player base UI for the Choose Cards phase will be activated through a new function called ActivatePlayerBaseBattlePanel. Code is shown below:
void ActivatePlayerBaseBattlePanel()
{
if (isPlayerBaseDefense)
{
PlayerBaseDefenseObjects.SetActive(true);
Debug.Log("ActivatePlayerBaseBattlePanel: Player base battle. Activating the PlayerBaseBattlePanel");
GameObject battleSite = NetworkIdentity.spawned[currentBattleSite].gameObject;
if (battleSite.transform.position == LocalGamePlayerScript.myPlayerBasePosition)
{
Debug.Log("Player is DEFENDING their own base");
DefeatEnemyText.text = "Defeat the enemy or lose the game!";
}
else
{
Debug.Log("Player is ATTACKING enemy base");
DefeatEnemyText.text = "defeat the enemy and win the game!";
}
}
else
{
Debug.Log("ActivatePlayerBaseBattlePanel: No player base battle. Deactivating the PlayerBaseBattlePanel");
PlayerBaseDefenseObjects.SetActive(false);
}
}

So if isPlayerBaseDefense is true, PlayerBaseDefenseObjects will be activated. The function checks to see if you are attacking or defending the base based on the myPlayerBasePosition value in LocalGamePlayerScript, then sets the DefeatEnemyText accordingly. If isPlayerBaseDefense, then PlayerBaseDefenseObjects is deactivated.
ActivatePlayerBaseBattlePanel will be called from isPlayerBaseDefense’s hook function, HandleIsPlayerBaseDefense, with the following code:
if (isClient)
ActivatePlayerBaseBattlePanel();

To make sure that isPlayerBaseDefense is set back to false after a battle, it can be done in GamePlayer.cs’s with the following:
GameplayManager.instance.HandleIsPlayerBaseDefense(GameplayManager.instance.isPlayerBaseDefense, false);

BattleResultsBaseDefenseObjects will need to be activated and deactivated and all that in the Battle Results phase. this will be done in the SetOpponentBattleScoreAndCard function of GameplayManager.cs with the following code:
if (isPlayerBaseDefense)
{
Debug.Log("SetOpponentBattleScoreAndCard: Battle was a base defense.");
BattleResultsBaseDefenseObjects.SetActive(true);
}
else
{
BattleResultsBaseDefenseObjects.SetActive(false);
}

The text for WinOrLoseDefenseText will depend on if the player won their defense/attack or not. This logic will be done in UpdateResultsPanel with the following:
if (isPlayerBaseDefense)
{
Debug.Log("UpdateResultsPanel: battle was a base defense");
if (reasonForWinning != "Draw: No Winner")
{
if (LocalGamePlayerScript.myPlayerBasePosition == NetworkIdentity.spawned[currentBattleSite].gameObject.transform.position)
{
Debug.Log("UpdateResultsPanel: Local player was defending their base!");
if (winnerOfBattleName == LocalGamePlayerScript.PlayerName && winnerOfBattlePlayerConnId == LocalGamePlayerScript.ConnectionId && winnerOfBattlePlayerNumber == LocalGamePlayerScript.playerNumber)
{
Debug.Log("Player defended their base!");
WinOrLoseDefenseText.text = "Defense Successful!";
}
else if (loserOfBattleName == LocalGamePlayerScript.PlayerName && loserOfBattlePlayerConnId == LocalGamePlayerScript.ConnectionId && loserOfBattlePlayerNumber == LocalGamePlayerScript.playerNumber)
{
Debug.Log("player failed to defend their base!");
WinOrLoseDefenseText.text = "Defense Failed...";
}
}
else
{
Debug.Log("UpdateResultsPanel: Local player attacking enemy base");
if (winnerOfBattleName == LocalGamePlayerScript.PlayerName && winnerOfBattlePlayerConnId == LocalGamePlayerScript.ConnectionId && winnerOfBattlePlayerNumber == LocalGamePlayerScript.playerNumber)
{
Debug.Log("Player captured enemy base");
WinOrLoseDefenseText.text = "Enemy base captured!";
}
else if (loserOfBattleName == LocalGamePlayerScript.PlayerName && loserOfBattlePlayerConnId == LocalGamePlayerScript.ConnectionId && loserOfBattlePlayerNumber == LocalGamePlayerScript.playerNumber)
{
Debug.Log("Failed to capture enemy base");
WinOrLoseDefenseText.text = "Failed to capture enemy base";
}
}
}
else
{
WinOrLoseDefenseText.text = "Stalement. Attacker must retreat";
LandScript battleSiteScript = NetworkIdentity.spawned[currentBattleSite].gameObject.GetComponent<LandScript>();
if (LocalGamePlayerScript.myPlayerBasePosition == battleSiteScript.gameObject.transform.position)
{
Debug.Log("UpdateResultsPanel: Player was defending their own base. Do not expand units for retreat. Expand other player's units.");
GamePlayer opposingPlayer = GameObject.FindGameObjectWithTag("GamePlayer").GetComponent<GamePlayer>();
battleSiteScript.ExpandLosingUnits(opposingPlayer.playerNumber);
}
else
{
Debug.Log("UpdateResultsPanel: Player was ATTACKING opposing base. Expand their units for retreat.");
battleSiteScript.ExpandLosingUnits(LocalGamePlayerScript.playerNumber);
}
}
}

This big, new section of code does the following after check isPlayerDefense to be true:
- Checks to see if the defense battle was a draw or not. There will be more on base defense draws later, but for now, if it is NOT a draw:
- check to see if the local game player was defending based on its myPlayerBasePosition value being equal to the battle site’s transform. position. If they equal:
- Check to see if the local game player won or lost the defense. Set the WinOrLoseDefenseText text accordingly
- If they do not equal, they are attacking the base and not defending
- check if the player won or lost the attack. Update the WinOrLoseDefenseText text to reflect that
- check to see if the local game player was defending based on its myPlayerBasePosition value being equal to the battle site’s transform. position. If they equal:
- if the base defense was a draw:
- check to see if the player was defending their own base. If they were defending:
- find the opposing game player and then call ExpandLosingUnits on the battle site’s LandScript with the opposing player’s player number
- this will make sure to ONLY expand the opposing player’s units to retreat and not both like a normal draw
- find the opposing game player and then call ExpandLosingUnits on the battle site’s LandScript with the opposing player’s player number
- if the player was attacking the base
- call ExpandLosingUnits on the battle site’s LandScript.cs for their own units, but not the opposing (defending) player’s
- check to see if the player was defending their own base. If they were defending:
The defense draw scenario and not expanding the defending units isn’t 100% complete now. That will be touched on more later. However, the battle defense UI should be good to test now in game. That is, of course, AFTER those UI objects are attached to the GameplayManager object in the Unity Editor (I definitely did NOT forget to do that before testing…)
I now see the following during Choose Cards of a battle defense:

Note the defending player’s score was increased from a normal 4 to 6! Then, I see this during battle results:

So, everything is working, except… When you are choosing your cards or viewing the opponent’s cards, the “Player Base Defense” text appears over top of them. Like the other UI elements in Choose Cards, the player base defense stuff will need to be hidden and then unhidden during base defense battles. This will be done in PlayerHand.cs. First, in the ShowPlayerHandOnScreen function, add the following:
if (GameplayManager.instance.isPlayerBaseDefense)
GameplayManager.instance.PlayerBaseDefenseObjects.SetActive(false);

Then, in the HidePlayerHandOnScreen function, add:
if (GameplayManager.instance.isPlayerBaseDefense)
{
GameplayManager.instance.PlayerBaseDefenseObjects.SetActive(true);
GameplayManager.instance.BattleResultsBaseDefenseObjects.SetActive(true);
}

Now, when you are viewing your cards or your opponent’s in a base defense battle, the UI text will be hidden with the rest of the ui.
Determine If Base Was Captured
After a base defense battle, the game will need to check if the attacking player captured the player base and ended the game. In GamePlayer.cs, a new server function called CheckForWinnerFromPlayerBaseBattle will be created. Code is shown below:
[Server]
void CheckForWinnerFromPlayerBaseBattle()
{
Debug.Log("Executing CheckForWinnerFromPlayerBaseBattle on the server");
bool didPlayerLoseBaseDefense = false;
GameObject battleSite = NetworkIdentity.spawned[GameplayManager.instance.currentBattleSite].gameObject;
GamePlayer defendingPlayer = null;
foreach (GamePlayer player in Game.GamePlayers)
{
if (player.myPlayerBasePosition == battleSite.transform.position)
{
Debug.Log("CheckForWinnerFromPlayerBaseBattle: found defending player: " + player.PlayerName);
defendingPlayer = player;
break;
}
}
//Check if the defending player lost the base defense. If they did, set wasPlayerEliminatedFromBaseCaptured to true;
if (defendingPlayer)
{
if (defendingPlayer.PlayerName == GameplayManager.instance.loserOfBattleName && defendingPlayer.ConnectionId == GameplayManager.instance.loserOfBattlePlayerConnId && defendingPlayer.playerNumber == GameplayManager.instance.loserOfBattlePlayerNumber)
{
Debug.Log("Player lost their base defense: " + defendingPlayer.PlayerName);
defendingPlayer.HandlePlayerEliminatedFromBaseCaptured(defendingPlayer.wasPlayerEliminatedFromBaseCaptured, true);
didPlayerLoseBaseDefense = true;
}
Debug.Log("Defending player: " + defendingPlayer.PlayerName + " successfully defending their base.");
}
// Check to see if more than 1 player remaining in game. If only 1 remains, declare that player the winner of the game
if (didPlayerLoseBaseDefense)
{
List<GamePlayer> remainingPlayers = new List<GamePlayer>();
foreach (GamePlayer player in Game.GamePlayers)
{
if (!player.wasPlayerEliminatedFromBaseCaptured && !player.wasPlayerEliminatedFromUnitsLost)
{
Debug.Log("CheckForWinnerFromPlayerBaseBattle: Player who was no eliminated by player base capture or lost units: " + player.PlayerName);
remainingPlayers.Add(player);
}
}
if (remainingPlayers.Count == 1)
{
Debug.Log("Only 1 player remaining. Naming the following player the winner of the game: " + remainingPlayers[0].PlayerName);
remainingPlayers[0].HandlePlayerWin(remainingPlayers[0].didPlayerWin, true);
}
}
}

CheckForWinnerFromPlayerBaseBattle does the following:
- Creates a bool called didPlayerLoseBaseDefense to track if the defending player lost
- Gets the land object for the battle site
- Loops through each GamePlayer in Game.GamePlayers. Checks to see if a gameplayer’s myPlayerBasePosition matches the transform.position of the battle site. If they match, that player is saved as the defendingPlayer
- If there is a defendingPlayer, checks that player’s information against the “loserOfBattleX” values in GameplayManager
- if the values match, the defendingPlayer lost the base defense
- the defendingPlayer’s wasPlayerEliminatedFromBaseCaptured is set to true
- didPlayerLoseBaseDefense is set to true
- If didPlayerLoseBaseDefense is true, the game checks to see if there is a winner of the game:
- A winner is declared if only 1 player remains
- All game player’s are looped through. If a gameplayer does not have either wasPlayerEliminatedFromBaseCaptured or wasPlayerEliminatedFromUnitsLost set to true, they are added to remainingPlayers
- if remainingPlayers has a count of 1, then only 1 player remains. That player is the winner of the game!
Now CheckForWinnerFromPlayerBaseBattle needs to be called somewhere. The winner of the battle won’t be known until the Battle Results phase, so it makes sense to declare the winner/loser of the game, and then end the game, after the Battle Results phase. Under the if (Game.CurrentGamePhase == "Battle Results")
check in the CheckIfAllPlayersAreReadyForNextPhase function of GamePlayer.cs, add the call:
// Check if the previous battle was a base defense. If so, check if the defending player lost the base defense
if (GameplayManager.instance.isPlayerBaseDefense)
{
Debug.Log("Server: Previous battle was a player base defense. Checking if the defending player lost");
CheckForWinnerFromPlayerBaseBattle();
}

I added the check between calls for MovePlayedCardToDiscard and DestroyUnitsLostInBattle so that way a winner/loser will be declared from the base defense before any are delcared for all units being destroyed.
End of Game UI for Base Capture
Now that the game detects when a base is captured, it will need to activate the end of game UI for a base being captured. In GamePlayer.cs, in the HandlePlayerEliminatedFromBaseCaptured hook function, a call will be made to GameplayManager.cs:
GameplayManager.instance.LocalPlayerEliminatedByBaseCaptured();

LocalPlayerEliminatedByBaseCaptured hasn’t been created in GameplayManager.cs yet, but it can be added now. It will just set the ReasonForLossText and activate the GameOver UI:
public void LocalPlayerEliminatedByBaseCaptured()
{
Debug.Log("Executing LocalPlayerEliminatedByBaseCaptured");
GameOverUI(true);
GameLoserTextObjects.SetActive(true);
ReasonForLossText.text = "Your base was captured";
}

If you save everything and build and run, you should see something similar to the below video!
Draw Scenario in a Base Defense
This was touched on briefly before, but it is possible for there to be a “draw” on a base defense. The tie breakers for a battle with the same battle score is first the highest card value, and then the number of infantry. Normally this meant that both players had the same army composition and played the same card. In a base defense, because the defending player has a +2 from their base, a draw can happen if both players play the same card, have the same number of infantry, and the attacking player has an additional tank in their army compared to the defending player. The extra tank gives the attacker the same +2 to their army score as the defender gets from their base.
After a normal draw, though, both players are required to retreat. An issue now occurs in a base defense in that there is no land available for the defender to retreat, so in the draw, the defender loses all their units, as you can see in the below image:

I don’t want to punish the defender that much for getting a draw in a defense, because then the next turn the attacker would most likely be able to capture a player base with no units on it. It gives a big advantage to the attacker, which I don’t want the game to do.
So, the game will need to be modified to make sure that the defending player is not flagged as a player that needs to retreat after a draw in a player base defense. First, in CheckWhichPlayersNeedToRetreat in GamePlayer.cs, the check that adds gameplayers to the playersToRetreat list in the Draw: No Winner
check will be changed to the following:
if (gamePlayer.myPlayerBasePosition == battleSiteLandScript.gameObject.transform.position)
{
Debug.Log("CheckWhichPlayersNeedToRetreat: Player: " + gamePlayer.PlayerName + " was defending their base. They do not need to retreat on a draw.");
gamePlayer.doesPlayerNeedToRetreat = false;
}
else
{
gamePlayer.doesPlayerNeedToRetreat = true;
playersToRetreat.Add(gamePlayer);
}

The new check just makes sure that if a player’s myPlayerBasePosition value matches the battle site’s transform.position, that they are not flagged as a player that needs to retreat. This should prevent the player from being included in the call for CanPlayerRetreatFromBattle, which will kill any units that can’t retreat.
Now, if you build and run, you should see that the defending units are no longer killed when there is a draw on a base defense:

The Expanding Unit Issue
The other thing you can see in the screenshot above is an issue with units being expanded. The defending units are expanded even though the player is not marked as needing to retreat, and the attacker is expanded twice. This is because on a draw, LandScript.cs’s ExpandForTie is called. That explains the defending units being expanded. The attacker is expanded twice, once from ExpandForTie and the second time because I already implemented a half fix to expand the attacking units in a base defense draw earlier.
To finish the fix, go to GameplayManager.cs’s UpdateResultsPanel function. If there is a draw on a battle, ExpandForTie is called. This check just needs to include that it won’t be called if there is a player defense as well:
if (reasonForWinning == "Draw: No Winner" && !isPlayerBaseDefense)
{
unitsLost.text = "No units lost";
NetworkIdentity.spawned[currentBattleSite].gameObject.GetComponent<LandScript>().ExpandForTie();
}

You should now see the following after a draw on a base defense:

The issue now is that while the wasn’t flagged to retreat, in the retreat units phase, they are still prompted to retreat and can’t ready up and end the turn until they do. Since it is impossible to retreat from your own base, this breaks the game. This can be fixed by modifying the CheckIfPlayerNeedsToRetreat function in GameplayManager.cs. The check for else if (reasonForWinning == "Draw: No Winner")
can be changed to the following:
else if (reasonForWinning == "Draw: No Winner")
{
if (isPlayerBaseDefense)
{
Debug.Log("Base defense detected. Checking if player was defending their own base.");
GameObject battleSite = NetworkIdentity.spawned[currentBattleSite].gameObject;
if (battleSite.transform.position == LocalGamePlayerScript.myPlayerBasePosition)
{
Debug.Log("Player defending their own base in a draw. They do not need to retreat.");
MouseClickManager.instance.canSelectUnitsInThisPhase = false;
doesPlayerNeedToRetreatText.text = "Attacker";
doesPlayerNeedToRetreatText.text += " retreating";
ChangePlayerReadyStatus();
}
else
{
Debug.Log("Player was ATTACKING opposing base. They must retreat.");
doesPlayerNeedToRetreatText.text = "Retreat Your Units";
MouseClickManager.instance.canSelectUnitsInThisPhase = true;
}
}
else
{
Debug.Log("Local player needs to retreat. Reason: Battle was a draw. Both players retreat");
doesPlayerNeedToRetreatText.text = "Retreat Your Units";
MouseClickManager.instance.canSelectUnitsInThisPhase = true;
}
}

So, now, if the battle was a base defense and ended in a draw, the game checks if the local player was defending their base. If they were, they can not select their units and ChangePlayerReadyStatus is called so they do not need to ready up. Build and save, and everything should work? Hopefully! It did for me.
Base Defense With No Units
In order for a base to be captured, there needs to first be a battle fought on that base. A battle is only detected if two different players have their units on the same land tile. Right now, if a player puts their units onto an opposing player’s base, but the opposing player has no units on their own base, no battle is detected. So, I want to add a detection for when a player has units on an opposing player’s base and declare a battle, regardless of whether there are any defending units.
This will be done in the CheckForPossibleBattles function of GamePlayer.cs. First, a new dictionary will be created that contains each player’s base coordinates as the key, and their player number as the value:
//Get a list of the Vector3 coordinates for all player bases
Dictionary<Vector3, int> playerBaseAndPlayerNumber = new Dictionary<Vector3, int>();
foreach (GamePlayer player in Game.GamePlayers)
{
playerBaseAndPlayerNumber.Add(player.myPlayerBasePosition, player.playerNumber);
}

Later in CheckForPossibleBattles, it will check each land tile that has more than 1 unit on it for a possible battle. I also want to add a check to check each player base by checking if the landtile exists as a key in the playerBaseAndPlayerNumber dictionary. If the land tile is a player base, it will then check if there are any units on the base that don’t match the player number of the player whose base it is. This will be added before the check for if there are more than one units on the land tile, and that original check will changed from an if to an else if
if (playerBaseAndPlayerNumber.ContainsKey(landObject.transform.position))
{
Debug.Log("CheckForPossibleBattles: Checking for units on a player base");
int basePlayerNumber = playerBaseAndPlayerNumber[landObject.transform.position];
if (landScript.UnitNetIdsAndPlayerNumber.Count > 0)
{
Debug.Log("CheckForPossibleBattles: At least one unit on player base: " + landObject.transform.position.ToString());
foreach (KeyValuePair<uint, int> units in landScript.UnitNetIdsAndPlayerNumber)
{
if (basePlayerNumber != units.Value)
{
Debug.Log("CheckForPossibleBattles opposing player discovered on player base at: " + landObject.transform.position.ToString()); ;
wasBattleDetected = true;
int battleNumber = GameplayManager.instance.battleSiteNetIds.Count + 1;
GameplayManager.instance.battleSiteNetIds.Add(battleNumber, landObject.GetComponent<NetworkIdentity>().netId);
break;
}
}
}
else
Debug.Log("CheckForPossibleBattles: NO UNITS on player base: " + landObject.transform.position.ToString());
}
else if (landScript.UnitNetIdsAndPlayerNumber.Count > 1)

If you build and run now, you should see that a battle is detected when an opposing player puts their units on a player base, even if the opposing player has no units there.

When the battle starts, the defender will still have a battle score of 2 from their base defenses.

Great! but wait, now there is a new issue…
Discarding Cards when there are no units
If the defender wins in the above scenario, the game seems to work fine, but I did notice that the defender’s played card isn’t acutally discarded. You can actually see it in the Unity Editor after the battle if you go to the Scene view and zoom out

And if you go to the defending player’s card hand and discard net id lists, you will see that the card was never removed from the net id list and added to the discard by the server. Locally, it was removed from the player’s hand (when they first selected the card) but was never added to the local Discard pile list:

This is because of how the MovePlayedCardToDiscard function in GamePlayer.cs works. To get a list of player’s that need to discard their cards, the code below is used:
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))
battlePlayers.Add(gamePlayer);
}
}
The code gets alist of all the player numbers on the battle site from the units on the battle site. It then uses the player numbers from the units to build the battlePlayerNumbers list. Then, it gets the gameplayer objects of the battlePlayers from the battlePlayerNumbers list. This is all to say that if a player does not have a unit on a battle site, they won’t discard their card. I did this to try and make the game “flexible” to the unlikely idea that I will ever make this game support more than 2 players.
To fix this, another check can be added to MovePlayedCardToDiscard that will add the defending player to the battlePlayers list when they are defending with no units.
else if (GameplayManager.instance.isPlayerBaseDefense)
{
if (gamePlayer.myPlayerBasePosition == battleSiteLandScript.gameObject.transform.position)
{
Debug.Log("MovePlayedCardToDiscard: Battle was a base defense and the defending player was found");
if (!battlePlayerNumbers.Contains(gamePlayer.playerNumber))
battlePlayers.Add(gamePlayer);
}
}

So, while all the gameplayers are being looped through, if the gameplay is not added to battlePlayers because they don’t have a unit on the battle site, the game checks to see if the battle was a player base defense. If it was a base defense, then the game checks if the player’s myPlayerBasePosition value is equal to the battle site’s transform.position. Then, if the battlePlayernumbers list does not contain that player, the player is added to battlePlayers.
With all that, I think the game is…done? At least playable to some extent? Let’s see a long and boring video of it to completion!
A Beta Release???
So, now that CardConquest is a…a…playable game? I have added a “release” to the public GitHub. I *think* you can download the zip archive from the most recent release and then extract it locally. Double click on CardConquest-BlogVersion.exe and the game should run???
If you are going to try and play and host the game over the Internet, you will need to know your external IP address, and then you will need to figure out how to do port forwarding on your router. The port used by mirror should be TCP 7777. I haven’t tested it over the Internet, but it at least seems to work over my LAN between two different computers!
To get it so a player can join your hosted game, I had to disable the Windows Firewall and run the CardConquest-BlogVersion.exe as an administrator (Right Click -> Run as Admin). I assume that’s because Windows is restricting applications from listening on ports when running with regular user privileges. Idk
I also found a game breaking bug I am trying to fix… it is a beta release after all!
Next Steps…
Well, the game is “done” in that it is playable for right now. However, there are still a lot of things that I want to add to the game to make it better. No idea if I ever will, but it would be nice to do so some day! I might take a bit of a break from the game first. BUT, here is a incomplete list of things I’d like to add:
- “Reinforcements” – let players choose if they want units from an adjacent land tile to reinforce their armies and increase their player army score
- Special abilities for cards – right now, the cards are pretty simple. I’d like to add “abilities” to the cards that affect the battles. An example would be, if you play this card and ONLY played infantry units in your army, all infantry have a power of 2 instead of 1. Or something like, if the opposing player has no tanks in their army, you get +1 to your battle score if you have a at least 1 tank. Stuff like that
- Different decks/cards – players have the same cards right now. I’d like for there to be at least two different card sets so players kind of play differently based on what cards they have. The cards would have different powers, attack/defense values, and different abilities (if I ever add those)
- Allow players to choose from multiple player types/card deck types – After I’ve added at least two card decks, maybe I will create more and also allow the players to “pick” which deck they want while they are in the lobby. Basically, character selection that affects what cards you have in the game
- More than 2 players…
All those are kind of long shots, but maybe I’ll get there some day!