CardConquest GameDev Blog #20: Choosing a Card for Battles

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 this rousing round of CardConquest game development, I want to allow players to select what card they will player for a battle. The card the player chooses will affect their army’s “power” number, and the player with the most power will win the battle! This will be a fairly important component of the game!

The tasks for this post will be:

  • Focus on the battle site
    • Zoom in on the specific land object the battle occurs on
    • Hide all units/UI from other land objects not related to the battle site. Don’t want to distract the player with superfluous information!
  • New UI elements to display battle information:
    • Units involved for each player
    • What each player’s power number is before either selects a card
    • For the local player, display what their power number will be after a card is selected
  • Make the local player’s cards clickable
    • When the card is clicked, highlight it and prompt the player if that is the card they want to select
  • After each player selects their card and readys up, determine who won the battle
    • Calculate each player’s power with their cards
    • Determine tie breakers and stuff?

So, anyway, let’s get going!

Focus on the Battle Site

“Focusing” on the battle site will involve moving the camera and “zooming” in by changing the camera’s size. The camera will be position so that the land tile for the battle will be in between two UI elements that will be created later.

For right now, the game will need to know what land tile to focus in on. When the game detects a battle, and then the battle “starts” and the Choose Cards phase begins, the network ID of the battle site is stored in GameplayManager.cs’s currentBattleSite variable.

So, to trigger the game to zoom the camera in on the battle site, a hook function will be added to currentBattleSite. The hook function will be called HandleCurrentBattleSiteUpdate.

The actual code for the HandleCurrentBattleSiteUpdate hook function is shown below:

public void HandleCurrentBattleSiteUpdate(uint oldValue, uint newValue)
{
	if (isServer)
	{
		currentBattleSite = newValue;
	}
	if (isClient)
	{
		//later stuff
	}
}

This looks a little different than how I’ve done hook functions before. I was reading about SyncVars and hook functions and found this,: SyncVar hooks don't fire on the server when the server changes the field. They only fire on the client when the client receives the new value. Thus the server player will not have the hook called.

So, I started making my hook functions so that when the server would call the hook to update the SyncVar’s value. The server calls HandleCurrentBattleSiteUpdate, and the currentBattleSite is updated to the value in newValue. That will update the currentBattleSite syncvar, which will then cause the clients to execute the hook, and the clients will execute anything under (isClient). I guess this will cause the syncvar to stay in sync between client and server better? Idk, but it seems to work like this.

From the isClient section of the hook, a function will be called to zoom onto the battle site. This will be done from a new function called ZoomOnBattleSite. The code is shown below:

void ZoomOnBattleSite()
{
	Debug.Log("Starting ZoomOnBattleSite for battle site with network id: " + currentBattleSite.ToString());
	GameObject battleSite = NetworkIdentity.spawned[currentBattleSite].gameObject;
	Vector3 newCameraPosition = battleSite.transform.position;
	Camera.main.orthographicSize = 5f;
	newCameraPosition.x += 2.15f;
	newCameraPosition.z = -10f;
	Camera.main.transform.position = newCameraPosition;
}

ZoomOnBattleSite does the following:

  • Finds the GameObject for the battle site land tile by searching for its network id, stored in currentBattleSite.
  • Sets a Vector3 variable newCameraPosition to the transform.position of the battle site
  • changes the size of the camera to 5.
  • Adds 2.15 to the x value of the newCameraPosition
  • Adds -10 to the z value of the newCameraPosition. Making the Z negative will make sure the camera can see the rest of the game objects
  • Sets the camera’s transform.position to newCameraPosition

Make sure that ZoomOnBattleSite is then called in the HandleCurrentBattleSiteUpdate hook function under the isClient check:

That should “zoom” the game onto the battle site. The next thing to do is to change how the currentBattleSite syncvar is changed in GamePlayer.cs.

In the CheckIfAllPlayersAreReadyForNextPhase function of GamePlayer.cs, change this:

GameplayManager.instance.currentBattleSite = battles.Value;

To this:

GameplayManager.instance.HandleCurrentBattleSiteUpdate(GameplayManager.instance.currentBattleSite, battles.Value);

So now the currentBattleSite syncvar is updated by calling the hook function HandleCurrentBattleSiteUpdate. Is this really the “correct” way to do this? I have no idea, but it seems to work!

Save all the scripts, build and run, and then create a battle scenario. In “Battles Detected” you should see the zoomed out view, like below:

And then when both player’s click “Start Battles” and the game advances to “Choose Cards,” the camera should “zoom in”, like below:

The game zoomed out!

I did notice that on the server player, ZoomOnBattleSite runs twice:

This is because the server player returns true for both the isServer and isClient checks. So, the isClient part will run when the server first calls HandleCurrentBattleSiteUpdate to update the value, and it will also run HandleCurrentBattleSiteUpdate again when the update for currentBattleSite is pushed to all clients.

So, I added a boolean variable to keep track of if the game has already zoomed in:

private bool localZoomedInOnBattleSite = false;

I then changed ZoomOnBattleSite so it only zooms in if localZoomedInOnBattleSite is false.

void ZoomOnBattleSite()
{
	if (!localZoomedInOnBattleSite)
	{
		Debug.Log("Starting ZoomOnBattleSite for battle site with network id: " + currentBattleSite.ToString());
		GameObject battleSite = NetworkIdentity.spawned[currentBattleSite].gameObject;
		Vector3 newCameraPosition = battleSite.transform.position;
		Camera.main.orthographicSize = 5f;
		newCameraPosition.x += 2.15f;
		newCameraPosition.z = -10f;
		Camera.main.transform.position = newCameraPosition;

		localZoomedInOnBattleSite = true;
	}
	
}

ZoomOnBattleSite will on run once on the server. now I just need to remember to reset localZoomedInOnBattleSite to false when the phase changes…

Hiding Units and Unit Text

The next step will be to hide any units and unit text that isn’t the current battle site. It’s distracting! Information overload! The player should only be concerned with the current battle, so I figured I would remove all that extra info.

First I’m going to hide all unit sprites that aren’t on the current battle site. To do this, a new function called HideNonBattleUnits will be added to GameplayManager.cs. The code is shown below.

void HideNonBattleUnits()
{
	GameObject[] PlayerUnitHolders = GameObject.FindGameObjectsWithTag("PlayerUnitHolder");
	foreach (GameObject unitHolder in PlayerUnitHolders)
	{
		foreach (Transform unitChild in unitHolder.transform)
		{
			UnitScript unitChildScript = unitChild.gameObject.GetComponent<UnitScript>();
			if (unitChildScript.currentLandOccupied.GetComponent<NetworkIdentity>().netId != currentBattleSite)
				unitChild.gameObject.SetActive(false);
			else
				unitChild.gameObject.SetActive(true);
		}
	}
}

HideNonBattleUnits does the following:

  • Finds all PlayerUnitHolder objects based on the PlayerUnitHolder tag
  • Iterates through every PlayerUnitHolder object
    • Iterates through every unit object that is a child of the PlayerUnitHolder object
      • For each unit, checks if the network id of the “currentLandOccupied” value matches the network id of currentBattleSite
        • If they don’t match, de-activate the unit object
        • If the do match, make sure the unit object is active

If you build and run the game now, you should see the other units not on the battle site disappear.

The issue you can see now, though, is that all the unit text objects and the battle site highlight and numbers are still visible on other land objects. The next step will be to hide those!

First, two new functions will be added to LandScript.cs to hide and unhide the battle highlights. These are quite simple functions named HideBattleHighlight and UnHideBattleHighlight that will simply set the battle outline object to active or not active. Code for both shown below:

public void HideBattleHighlight()
{
	if (battleOutlineObject)
	{
		battleOutlineObject.SetActive(false);
	}
}
public void UnHideBattleHighlight()
{
	if (battleOutlineObject)
	{
		battleOutlineObject.SetActive(true);
	}
}

Then, back in GameplayManager.cs, create a new function called HideNonBattleLandTextAndHighlights. Code shown below:

void HideNonBattleLandTextAndHighlights()
{
	GameObject allLand = GameObject.FindGameObjectWithTag("LandHolder");
	foreach (Transform landObject in allLand.transform)
	{
		LandScript landScript = landObject.gameObject.GetComponent<LandScript>();
		if (landObject.gameObject.GetComponent<NetworkIdentity>().netId != currentBattleSite)
		{
			landScript.HideUnitText();
			landScript.HideBattleHighlight();
		}
		else
		{
			landScript.UnHideUnitText();
			landScript.UnHideBattleHighlight();
		}
	}
}

HideNonBattleLandTextAndHighlights does the following:

  • Finds the landholder object by its “LandHolder” tag
  • iterates through all child land objects in LandHolder
    • Checks if the network id of the land object matches currentBattleSite
      • if they don’t match, call HideUnitText and HideBattleHighlight from the land objects LandScript
      • If they do match, call UnHideUnitText and UnHideBattleHighlight from the land objects LandScript

Save all scripts, build and run, and you should see the something like this when you get to the Choose Cards phase:

Yay! It worked

Fixing Unhiding Text after Viewing Cards

If you try and view cards during the Choose Cards phase, you will first notice that the cards aren’t displaying correctly. That will be fixed later! The second thing you may notice is that after you hide the cards, the Unit and Battle Site text will re-appear on land tiles that aren’t the current battle site:

This is because in PlayerHand.cs, when the player stops viewing their or an opponent’s hand, the HidePlayerHandOnScreen function is called. In that function, landScript.UnHideUnitText() is called to unhide all unit text.

So, then HidePlayerHandOnScreen can be modified so that if the current phase is Choose Cards, it only runs landScript.UnHideUnitText() on the land object that is the current battle site. The code for that check is shown below:

if (GameplayManager.instance.currentGamePhase.StartsWith("Choose Card"))
{
	GameObject landHolder = GameObject.FindGameObjectWithTag("LandHolder");
	foreach (Transform landChild in landHolder.transform)
	{
		if (landChild.gameObject.GetComponent<NetworkIdentity>().netId == GameplayManager.instance.currentBattleSite)
		{
			LandScript landScript = landChild.GetComponent<LandScript>();
			landScript.UnHideUnitText();
		}
	}
}
else
{
	GameObject landHolder = GameObject.FindGameObjectWithTag("LandHolder");
	foreach (Transform landChild in landHolder.transform)
	{
		LandScript landScript = landChild.GetComponent<LandScript>();
		landScript.UnHideUnitText();
	}
}

If the current game phase is Choose Cards, HidePlayerHandOnScreen will now iterate through every land object and only run landScript.UnHideUnitText() on the land object for the current battle site.

It should all work now!

New UI Elements to Display Battle Information

To display information about the battle to the player’s, I want to create a UI panel that will list out the battle scores and so on for that player.

To get started, load the Gameplay scene in the Unity editor. Expand the GameplayUI canvas, and then activate the ChooseCardsPanel. Create a new UI panel as a child of ChooseCardsPanel, and rename it to PlayerBattlePanel.

Select the PlayerBattlePanel, and then in the Inspector window, set the following for the panel:

  • Anchor: Middle left
  • Pos X: 200
  • Pos Y: 0
  • Width: 350
  • Height: 550

In your scene, you should see the following:

Create a new child text object under PlayerBattlePanel and name it PlayerName.

Set the PlayerName’s RectTransform values to the following:

  • Anchor: Middle Center
  • Pos x: 0
  • Pos y: 200
  • Width: 300
  • Height: 100

Under the “Text” section of PlayerName, set the following:

  • Font: ThaleahFat
  • Font Size: 35
  • Alignment: Centered
  • Color: White

You don’t need to modify the actual text right now. That will be changed later by the script. It will be used to say, <Player Name>'s Battle Power if you want to test out the size now.

Next, create a new text object called TanksText.

Configure TanksText with the following:

Create a new text object called TankPower.

Configure TankPower as follows:

Note: to get the color to be Yellow, set the RGB value to R:255,G:255,B:0.

If you look at the panel in the scene now, you should see the following:

In addition to displaying the power from Tanks, I will want to display the power from infantry and then the combined power of tanks + infantry. So, to do that, select the TanksText and TankPower text objects, and hit “ctrl+d” TWICE to duplicate the TanksText and TankPower twice.

Rename the “TanksText (1)” and “TankPower (1)” objects to InfText and InfPower, respectively.

With BOTH InfText and InfPower selected, change the “Pos Y” value of the RectTransform to 85.

In the scene, you should now see that the InfText and InfPower objects moved down in the panel.

Now you just need to change the text of InfText from “Power From Tanks:” to “Power From Infantry:”.

Next, rename “TanksText (2)” and “TankPower (2)” to “TotalArmyText” and “ArmyPower”, respectively.

With both TotalArmyText and ArmyPower selected, change the Pos Y value to 45.

Then, select the TotalArmyText object and change the text to “Power Of Army:”

Now, the panel should look like this in the scene:

Moving on! Select the TotalArmyText and use ctrl+d to duplicate it. Rename it to YourCard.

Configure YourCard to the following:

For the last piece, select both TotalArmyText and ArmyPower and duplicate them with ctrl+d. Rename the duplicates to CardText and CardPower.

Select both CardText and CardPower and set the Pos Y value to -235.

Then, select CardText, and set the text to “Power With Card:” After that, the panel should look like this:

And that’s…it for the panel! Now, it’s time to save the PlayerBattlePanel as a prefab. I want it as a prefab because I am going to spawn and reposition them with the GameplayManager.cs script. I don’t really need to do it like this, but, it’s what I did.

Under the OfflinePrefabs directory, create a new directory called “UIPrefabs.”

Then, drag and drop the PlayerBattlePanel to the UIPrefabs directory to save it as a prefab.

After PlayerBattlePanel has been saved as a prefab, you can delete it from the Gameplay scene.

Creating the Panels

Now, a bunch of code and stuff will need to be created to actually create and then update the PlayerBattlePanels for each player.

To begin, some variables will be added to GamePlayer.cs to keep track of each GamePlayer’s battle army information.

public SyncList<uint> playerArmyNetIds = new SyncList<uint>();
[SyncVar] public int playerArmyNumberOfInf;
[SyncVar] public int playerArmyNumberOfTanks;
[SyncVar] public int playerBattleScore;
[SyncVar(hook = nameof(HandleBattleScoreSet))] public bool isPlayerBattleScoreSet = false;

Then, in GameplayManager.cs, create a new function called SetGamePlayerArmy

void SetGamePlayerArmy()
{
	LocalGamePlayerScript.SetGamePlayerArmy();
}

Make sure to call SetGamePlayerArmy from HandleCurrentBattleSiteUpdate.

Notice that squiggly line under LocalGamePlayerScript.SetGamePlayerArmy(); in SetGamePlayerArmy? Well, that’s because a SetGamePlayerArmy function needs to be added to GamePlayer.cs! Let’s go ahead and do that.

The code for SetGamePlayerArmy in GamePlayer.cs is rather simple:

public void SetGamePlayerArmy()
{
	if (hasAuthority)
		CmdSetGamePlayerArmy();
}

Now, CmdSetGamePlayerArmy will need to be created in GamePlayer.cs. Code below:

[Command]
void CmdSetGamePlayerArmy()
{
	NetworkIdentity networkIdentity = connectionToClient.identity;
	GamePlayer requestingPlayer = networkIdentity.GetComponent<GamePlayer>();
	Debug.Log("Executing CmdSetGamePlayerArmy for: " + requestingPlayer.PlayerName + ":" + requestingPlayer.ConnectionId);
	//Clear out any previous army data
	requestingPlayer.playerArmyNetIds.Clear();
	requestingPlayer.playerArmyNumberOfInf = 0;
	requestingPlayer.playerArmyNumberOfTanks = 0;
	requestingPlayer.playerBattleScore = 0;

	GameObject battleSite = NetworkIdentity.spawned[GameplayManager.instance.currentBattleSite].gameObject;
	LandScript battleSiteScript = battleSite.GetComponent<LandScript>();

	foreach (KeyValuePair<uint, int> battleUnits in battleSiteScript.UnitNetIdsAndPlayerNumber)
	{
		if (battleUnits.Value == requestingPlayer.playerNumber)
		{
			requestingPlayer.playerArmyNetIds.Add(battleUnits.Key);
			if (NetworkIdentity.spawned[battleUnits.Key].gameObject.tag == "infantry")
				requestingPlayer.playerArmyNumberOfInf++;
			else if (NetworkIdentity.spawned[battleUnits.Key].gameObject.tag == "tank")
				requestingPlayer.playerArmyNumberOfTanks++;
		}
	}
	requestingPlayer.playerBattleScore = requestingPlayer.playerArmyNumberOfInf;
	requestingPlayer.playerBattleScore += (requestingPlayer.playerArmyNumberOfTanks * 2);
	HandleBattleScoreSet(requestingPlayer.isPlayerBattleScoreSet, true);
}

CmdSetGamePlayerArmy will go through and determine which units on a battle site belong to what player, and then determine a player’s battle score based on the units they have on the battle site. CmdSetGamePlayerArmy does the following:

  • Retrieves the GamePlayer script for the player making the request to CmdSetGamePlayerArmy
  • Clears out any previous data from the requesting player’s army information
  • Gets the battle site land object based on the network id stored in currentBattleSite
  • Gets the land script of the battle site
  • Iterates through each key value pair in the battle site’s UnitNetIdsAndPlayerNumber dictionary that stores each unit and the player the unit belongs to
    • Checks if the unit in the dictionary belongs to the requesting player. The unit’s network idea is the key and the player number of the owning player is the value
    • If the player owns the unit:
      • Adds the unit’s network id to the requesting player’s playerArmyNetIds list
      • Checks if the unit is an infantry or a tank based on its tag
        • if infantry, add to requesting player’s playerArmyNumberOfInf value
        • if tank, add to requesting player’s playerArmyNumberOfTanks value
  • With the requesting player’s army information set, the battle score is calculated:
    • infantry is worth 1 battle point, so add the number of infantry to the player’s battle score
    • tanks are worth 2 battle points, so multiple playerArmyNumberOfTanks by 2 and then add to the player’s battle score
  • Finally, call the HandleBattleScoreSet hook function to update isPlayerBattleScoreSet to true

So, now, the HandleBattleScoreSet hook function will need to be created. Code below:

void HandleBattleScoreSet(bool oldValue, bool newValue)
{
	if (isServer)
	{
		this.isPlayerBattleScoreSet = newValue;
	}
	if (isClient && newValue)
	{
		Debug.Log("Running HandleBattleScoreSet as a client.");
		GameplayManager.instance.CheckIfAllPlayerBattleScoresSet();
	}    
	
}

Alright! So HandleBattleScoreSet sets the value for isPlayerBattleScoreSet if run on the server, and if run on the client and the newValue is true, will make a call to CheckIfAllPlayerBattleScoresSet in GameplayManager.cs.

CheckIfAllPlayerBattleScoresSet doesn’t exist yet! Time to create it in GameplayManager.cs. Code below…

public void CheckIfAllPlayerBattleScoresSet()
{
	Debug.Log("Executing CheckIfAllPlayerBattleScoresSet");
	bool haveAllBattleScoresBeenSet = false;
	if (!LocalGamePlayerScript.isPlayerBattleScoreSet)
	{
		Debug.Log("CheckIfAllPlayerBattleScoresSet: LocalGamePlayer not ready");
		return;
	}
	else
		haveAllBattleScoresBeenSet = LocalGamePlayerScript.isPlayerBattleScoreSet;

	GameObject[] allGamePlayers = GameObject.FindGameObjectsWithTag("GamePlayer");
	foreach (GameObject gamePlayer in allGamePlayers)
	{
		GamePlayer gamePlayerScript = gamePlayer.GetComponent<GamePlayer>();
		if (!gamePlayerScript.isPlayerBattleScoreSet)
		{
			haveAllBattleScoresBeenSet = false;
			Debug.Log("CheckIfAllPlayerBattleScoresSet: " + gamePlayerScript.PlayerName + " not ready");
			break;
		}
		else
		{
			haveAllBattleScoresBeenSet = gamePlayerScript.isPlayerBattleScoreSet;
		}
	}
	if (haveAllBattleScoresBeenSet)
	{
		Debug.Log("CheckIfAllPlayerBattleScoresSet: all gameplayers are ready!");
		CreateBattlePanels();
	}
}

CheckIfAllPlayerBattleScoresSet goes through each GamePlayer object, including the LocalGamePlayer, and checks if its isPlayerBattleScoreSet value is true. If it is true for all GamePlayers, then CreateBattlePanels() will be called.

CreateBattlePanels will do what you expect it to. It will create the PlayerBattlePanel objects! Yay! But first, some variables need to be added to GameplayManager.cs to store the PlayerBattlePanel prefab, the spawned objects, and some of the child objects.

[SerializeField] private GameObject playerBattlePanelPrefab;
public GameObject localPlayerBattlePanel;
public GameObject opponentPlayerBattlePanel;
private GameObject localCardText;
private GameObject localCardPower;
private GameObject opponentCardText;
private GameObject opponentSelectCardText;
private GameObject opponentCardPower;

And now, finally, the code for CreateBattlePanels

void CreateBattlePanels()
{
	Debug.Log("Executing CreateBattlePanels");
	// Spawn the local player's battle panel
	if (!localPlayerBattlePanel)
	{
		localPlayerBattlePanel = Instantiate(playerBattlePanelPrefab);
		localPlayerBattlePanel.transform.SetParent(ChooseCardsPanel.GetComponent<RectTransform>(), false);
		// set the values for the local player's battle panel
		foreach (Transform childTransform in localPlayerBattlePanel.transform)
		{
			if (childTransform.name == "PlayerName")
			{
				childTransform.GetComponent<Text>().text = LocalGamePlayerScript.PlayerName + "'s Battle Power";
			}
			if (childTransform.name == "TankPower")
			{
				int tankPower = LocalGamePlayerScript.playerArmyNumberOfTanks * 2;
				childTransform.GetComponent<Text>().text = tankPower.ToString();
			}
			if (childTransform.name == "InfPower")
			{
				childTransform.GetComponent<Text>().text = LocalGamePlayerScript.playerArmyNumberOfInf.ToString();
			}
			if (childTransform.name == "ArmyPower")
			{
				childTransform.GetComponent<Text>().text = LocalGamePlayerScript.playerBattleScore.ToString();
			}
			if (childTransform.name == "CardText")
			{
				localCardText = childTransform.gameObject;
				localCardText.SetActive(false);
			}
			if (childTransform.name == "CardPower")
			{
				localCardPower = childTransform.gameObject;
				localCardPower.SetActive(false);
			}
		}
	}

	//Spawn the opponent's battle panel
	if (!opponentPlayerBattlePanel)
	{
		opponentPlayerBattlePanel = Instantiate(playerBattlePanelPrefab);
		opponentPlayerBattlePanel.transform.SetParent(ChooseCardsPanel.GetComponent<RectTransform>(), false);
		opponentPlayerBattlePanel.GetComponent<RectTransform>().anchoredPosition = new Vector3(770f, 0f, 0f);
		GamePlayer opponentPlayerScript = GameObject.FindGameObjectWithTag("GamePlayer").GetComponent<GamePlayer>();
		foreach (Transform childTransform in opponentPlayerBattlePanel.transform)
		{
			if (childTransform.name == "PlayerName")
			{
				childTransform.GetComponent<Text>().text = opponentPlayerScript.PlayerName + "'s Battle Power";
			}
			if (childTransform.name == "TankPower")
			{
				int tankPower = opponentPlayerScript.playerArmyNumberOfTanks * 2;
				childTransform.GetComponent<Text>().text = tankPower.ToString();
			}
			if (childTransform.name == "InfPower")
			{
				childTransform.GetComponent<Text>().text = opponentPlayerScript.playerArmyNumberOfInf.ToString();
			}
			if (childTransform.name == "ArmyPower")
			{
				childTransform.GetComponent<Text>().text = opponentPlayerScript.playerBattleScore.ToString();
			}
			if (childTransform.name == "YourCard")
			{
				opponentSelectCardText = childTransform.gameObject;
				opponentSelectCardText.GetComponent<Text>().text = "Opponent's card:";
				opponentSelectCardText.SetActive(false);
			}
			if (childTransform.name == "CardText")
			{
				opponentCardText = childTransform.gameObject;
				opponentCardText.SetActive(false);
			}
			if (childTransform.name == "CardPower")
			{
				opponentCardPower = childTransform.gameObject;
				opponentCardPower.SetActive(false);
			}
		}
	}
	
}

CreateBattlePanels is long, but pretty straightforward. It simply creates the panels and then updates the information. The opponentPlayerBattlePanel needs to be repositioned a bit. I got the new values by tinkering with the panels manually in Unity.

Save all the scripts. In the Gameplay scene, make sure to deactivate the ChooseCardsPanel. Then, make sure that the PlayerBattlePanel prefab is attached to the GameplayManager object.

Save the scene. Go to the TitleScreen scene, build and run. Create a battle scenario, and you should see the PlayerBattlePanel’s spawn!

Some UI Panel Cleanup

One thing I forgot to do in the blog version of my game was make sure that different phase UI panels are deactivated as necessary. The following lines are check and disable the BattlesDetected and ChooseCards panels.

if (BattlesDetectedPanel.activeInHierarchy)
	BattlesDetectedPanel.SetActive(false);
if (ChooseCardsPanel.activeInHierarchy)
	ChooseCardsPanel.SetActive(false);

These need to be added to the following functions in GameplayManager.cs:

  • ActivateUnitPlacementUI
  • ActivateUnitMovementUI

Then in ActivateBattlesDetectedUI, only add the lines to deactivate the ChooseCardsPanel.

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

Making Cards Clickable

In the Choose Cards phase, the players will be selecting what card they want to use for the battle. The card will give each player additional battle power, as well as some other effects that will be discussed/coded in a later post.

To allow for players to click on Units and Land tiles, new “layers” were added to the game and then applied to the corresponding prefabs. So, to make cards clickable, a new “PlayerCard” layer will be added. In the Unity editor, click on the “Layers” drop down and then click “Edit Layers…”

Expand the “Layers” section, and then add a new layer called PlayerCards.

After the layer has been created, go through each card prefab and add the PlayerCards layer to the prefab.

Next, the PlayerCards layer will need to be added to MouseClickManager.cs as a LayerMask variable.

[SerializeField] private LayerMask playerCardLayer;

Go back into the Unity editor and make sure to add the PlayerCards layer to the MouseClickManager object.

Allowing Player to Click Cards

In previous posts I had made it so the players can only click on units in certain phases by creating and setting a boolean variable called canSelectUnitsInThisPhase in MouseClickManager.cs. Similarly for clicking cards, I only want players to be able to click on cards in certain phases, and specifically in the “Choose Cards” phase. So, first, I added a canSelectPlayerCardsInThisPhase boolean variable to MouseClickManager.cs.

public bool canSelectPlayerCardsInThisPhase = false;

Then in GameplayManager.cs’s ChangeGamePhase function, canSelectPlayerCardsInThisPhase will be set to “false” EXCEPT for when the phase is changed to Choose Cards. Then it is set to true.

Detecting Player Clicking on Card

Now MouseClickManager.cs will be used to detect when a player clicks on a card. First, a new GameObject variable called cardSelected will be used to store the card a player clicks on.

public GameObject cardSelected;

Then, under MouseClickManager.cs’s Update function, within the check for Input.GetMouseButtonDown(0) (left mouse click), create a new section that detects if a player clicks on a card.

if (canSelectPlayerCardsInThisPhase)
{
	RaycastHit2D rayHitCard = Physics2D.Raycast(mousePosition2d, Vector2.zero, Mathf.Infinity, playerCardLayer);
	if (rayHitCard.collider != null)
	{
		SelectCardClicked(rayHitCard.collider.gameObject);
	}
}

Same as what was done with a player clicks on a unit, a RaycastHit2D is cast and used to determine if a player clicked on anything in the PlayerCards layer. If the player did click on a card, the SelectCardClicked function will be called and the gameobject of the clicked card is provided as an argument. SelectCardClicked will now need to be created.

public void SelectCardClicked(GameObject cardClicked)
{
	if (cardClicked)
	{
		if (cardClicked.GetComponent<NetworkIdentity>().hasAuthority)
		{
			Card cardScript = cardClicked.GetComponent<Card>();
		}
	}
}

SelectCardClicked doesn’t really do anything right now. It checks to make sure a gameobject was actually passed, and then checks if the player has network authority over the card. If the player does have authority, then the Card script is retrieved for the card.

Now, some code and other things will need to be added to Card.cs to execute when the card is clicked on.

Creating a Card Highlight

When a card is clicked by a player, I want the card to be highlighted so that the player knows what card they currently have selected. I created a new card-outline sprite that is six pixels of yellow on the outside of a card.

Save the card-outline sprite in your Assets>Sprites directory. Then, in the Unity editor, drag the card-outline sprite into a scene. Select the card-outline object in the scene, and then set the “Scale” values for the object to X:1.75 and Y:1.75.

The card-outline can now be saved as a prefab. First, in the OfflinePrefabs directory, create a new directory called CardPrefabs.

Then, drag and drop the card-outline object into the CardPrefabs directory to save it as a prefab. After it is saved as a prefab, it can be deleted from the scene.

Coding the Card Selection

Open Card.cs and add a few new variables at the top.

public bool currentlySelected = false;
[SerializeField] GameObject cardOutlinePrefab;
public GameObject cardOutlineObject;

These will be used to store if the card is currently select or not, and store the card-outline prefab and its spawned object.

Next, a function called CardClickedOn will be created. This will flip the currentlySelected boolean, and then create or destroy the card-outline highlight based on if the card is being selected or unselected.

public void CardClickedOn()
{
	currentlySelected = !currentlySelected;
	if (currentlySelected)
	{
		if (!cardOutlineObject)
		{
			cardOutlineObject = Instantiate(cardOutlinePrefab, transform.position, Quaternion.identity);
			cardOutlineObject.transform.SetParent(this.transform);
			Vector3 cardScale = new Vector3(1f, 1f, 0f);
			cardOutlineObject.transform.localScale = cardScale;
		}
	}
	else if (!currentlySelected)
	{
		if (cardOutlineObject)
		{
			Destroy(cardOutlineObject);
			cardOutlineObject = null;
		}
	}
}

Now, in MouseClickManager.cs, you can add the call to CardClickedOn in the SelectedCardClicked function.

Back in the Unity editor, go through each card prefab and make sure to add the card-outline prefab to the CardOutlinePrefab variable.

One final thing(s) to add to the card prefabs that I almost forgot about: colliders and rigidbodies. In order for there to be a “collision” between the RaycastHit2d and the card, the card’s need colliders. So, for each card prefab, add a box collider 2d and a rigidbody 2d. Make sure that the “Body Type” of the rigidbody 2d is set to kinematic.

Also, open the card-outline prefab again and make sure its “Sorting Layer” is set to “PlayerCards”

If you build and run the game now, you should be able to “Select” cards and highlight them. However, there are a few glaring issues:

  • The PlayerBattlePanels are still showing on the screen
  • The cards are not positioned correctly and too big for the zoomed in view
  • The player can select multiple cards. They should only be able to select one card for the battle

So, let’s resolve those issues one at a time

Hiding the PlayerBattlePanels When Viewing Cards and Repositioning Cards

Hiding the PlayerBattlePanels and repositioning the cards will all take place in PlayerHand.cs, so these can be resolved at the same time.

In the ShowPlayerHandOnScreen function of PlayerHand.cs, a new check will be added to see if it is the Choose Card phase. IF it is, the cards will be positioned and scaled differently than if it is a different phase. Additionally, if it is the Choose Card phase, the PlayerBattlePanels will be deactivated if they exist.

if (GameplayManager.instance.currentGamePhase.StartsWith("Choose Card"))
{
	Vector3 cardLocation = Camera.main.transform.position;
	cardLocation.x -= 7f;
	cardLocation.z = 0f;
	Vector3 cardScale = new Vector3(1.5f, 1.5f, 0f);
	foreach (GameObject playerCard in Hand)
	{
		if (!playerCard.activeInHierarchy)
		{
			playerCard.SetActive(true);
		}
		playerCard.transform.position = cardLocation;
		playerCard.transform.localScale = cardScale;
		cardLocation.x += 3.5f;
	}
	if (GameplayManager.instance.localPlayerBattlePanel && GameplayManager.instance.opponentPlayerBattlePanel)
	{
		GameplayManager.instance.localPlayerBattlePanel.SetActive(false);
		GameplayManager.instance.opponentPlayerBattlePanel.SetActive(false);
	}
}
else
{
	Vector3 cardLocation = new Vector3(-10f, 1.5f, 0f);
	Vector3 cardScale = new Vector3(1.75f, 1.75f, 0f);
	foreach (GameObject playerCard in Hand)
	{
		if (!playerCard.activeInHierarchy)
		{
			playerCard.SetActive(true);
		}
		playerCard.transform.position = cardLocation;
		playerCard.transform.localScale = cardScale;
		cardLocation.x += 4.5f;
	}
}

HidePlayerHandOnScreen will now need to be updated to activate the PlayerBattlePanels back. HidePlayerHandOnScreen already has a check to see if it is the Choose Card phase, so a check will be added to see if the PlayerBattlePanels exist, and then reactivate them.

if (GameplayManager.instance.localPlayerBattlePanel && GameplayManager.instance.opponentPlayerBattlePanel)
{
	GameplayManager.instance.localPlayerBattlePanel.SetActive(true);
	GameplayManager.instance.opponentPlayerBattlePanel.SetActive(true);
}

Save everything, build and run, and you should see the PlayerBattlePanels are hidden and the cards rescaled and positioned.

Allow Only One Cards to be Selected

Since I only want players to be able to select one card for the battle, I want to make it so when they are looking at their hand and select a card, and previously selected card is unselected. This is done with the following code added to SelectCardClicked.

if (cardSelected != cardClicked && cardSelected)
{
	Card cardSelectedScript = cardSelected.GetComponent<Card>();
	cardSelectedScript.CardClickedOn();
}
if (cardSelected == cardClicked)
{
	cardSelected = null;
}
else
{
	cardSelected = cardClicked;
}

Basically, what this does now is check if the card that was clicked on matches what’s stored in cardSelected. cardSelected will store any previously clicked on cards. So if they don’t match, and there is a card in cardSelected, the CardClickedOn function will be called for the cardSelected card to unselect it. If cardSelected and cardClicked are equal, then the player clicked on the same card again, so that card is unselected. Finally, cardSelected is set to the new cardClicked.

If you build and run now, you should see that only one card is every highlighted at a time. However, if you click on a card and highlight it, then click on “Hide Hand,” that card remains selected. When you open your hand again, that card will still have its highlight. I want any selected card to be unselected when the hand is hidden, so the HidePlayerHandPressed function in GameplayManager.cs needs to be updated. The code is shown below:

if (currentGamePhase.StartsWith("Choose Card"))
{
	MouseClickManager.instance.SelectCardClicked(MouseClickManager.instance.cardSelected);
}

When the palyer hides their hand in the Choose Card phase, SelectCardClicked from MouseClickManager.cs will be called. GameplayManager.cs passes the cardSelected variable of MouseClickManager as an argument, so any selected card should be deselected. If no card has been selected, then SelectCardClicked should do nothing when it is called since the first check is to make sure the GameObject argument isn’t null.

Selecting a Card for Battle

Now that player’s can click on a card to select them, there needs to be a way for the player to choose that card as their battle card. First, I will want to create a new button that the player presses when they want to select a card. In the Gameplay scene, duplicate the confirmChooseCard button under ChooseCardPanel and rename it to selectThisCard.

For now, you can remove the OnClick function from selectThisCard

In selectThisCard’s RectTransform, set the following:

  • Anchor: middle center
  • Pos x: 0
  • Pos Y: 215

Expand selectThisCard and select its Text child object. Set the text to “Select this card”

In GameplayManager.cs, add a variable for the selectThisCard button.

[SerializeField] private GameObject selectThisCardButton;

Then in the Unity Editor, add the button to the GameplayManager object.

Make sure that the selectThisCard button is deactivated in the scene. You can also deactivate confirmChooseCard. In GameplayManager.cs, a new function called ToggleSelectThisCardButton will be created to activate and deactivate the selectThisCard button when a player has a card selected/highlighted. The code is shown below.

public void ToggleSelectThisCardButton()
{
	bool isACardSelected = false;
	if (MouseClickManager.instance.cardSelected)
		isACardSelected = true;
	else
		isACardSelected = false;
	
	if (isACardSelected)
		selectThisCardButton.SetActive(true);
	else
		selectThisCardButton.SetActive(false);
}

ToggleSelectThisCardButton will check to see if cardSelected in MouseClickManager holds a card or not. If it does, the selectThisCard button is activated. If it does not, it is deactivated.

ToggleSelectThisCardButton will now need to be called. It will be called from SelectCardClick in MouseClickManager.cs to make sure that it is called whenever a card is selected or unselected.

The selectThisCard button should now appear when you have a card selected and disappear when you unselect a card.

Submit Selected Card to Server

When a player clicks on the “Select This Card” button, they should then submit the selected card to the server for their battle card. The server will validate that the player is selecting a card from their hand, and if it is, allowing the selection.

In GameplayManager.cs, a new function called SelectThisCard will be added.

public void SelectThisCard()
{
	if (MouseClickManager.instance.cardSelected)
	{
		LocalGamePlayerScript.SelectThisCard(MouseClickManager.instance.cardSelected);
	}
}

If the player has a card selected, this will then make a call to SelectThisCard in the LocalPlayerScript and pass the selected card as an argument. In GamePlayer.cs, SelectThisCard will need to be added.

public void SelectThisCard(GameObject playerCard)
{
	if (hasAuthority)
	{
		CmdPlayerSelectCardForBattle(playerCard.GetComponent<NetworkIdentity>().netId);
	}

}

SelectThisCard in GamePlayer.cs takes a GameObject playerCard as an argument. The selectedCard variable from MouseClickManager.cs will be passed as an arugment for this. SelectThisCard will then first check if the player has authority over the GamePlayer object, and then call a comman function CmdPlayerSelectCardForBattle with the card’s network id as an argument.

CmdPlayerSelectCardForBattle will now need to be created, shown below:

[Command]
void CmdPlayerSelectCardForBattle(uint playerCardNetworkId)
{
	NetworkIdentity networkIdentity = connectionToClient.identity;
	GamePlayer requestingPlayer = networkIdentity.GetComponent<GamePlayer>();
	Debug.Log("Executing CmdPlayerSelectCardForBattle for: " + requestingPlayer.PlayerName + ":" + requestingPlayer.ConnectionId);
	if (requestingPlayer.playerCardHandNetIds.Contains(playerCardNetworkId))
	{
		Debug.Log("Player card: " + playerCardNetworkId + " is in " + requestingPlayer.PlayerName + "'s hand.");
		//requestingPlayer.playerBattleCardNetId = playerCardNetworkId;
		HandleUpdatedPlayerBattleCard(requestingPlayer.playerBattleCardNetId, playerCardNetworkId);
	}
	else
	{
		Debug.Log("Player card: " + playerCardNetworkId + " IS NOT in " + requestingPlayer.PlayerName + "'s hand.");
	}
}

CmdPlayerSelectCardForBattle does the following:

  • Retrieves the GamePlayer script for the requesting player
  • checks if the requested card is in the requesting player’s playerCardHandNetIds list
    • if it is, HandleUpdatedPlayerBattleCard is called to update the value of the player’s playerBattleCardNetId

Now a couple of things that CmdPlayerSelectCardForBattle needs are missing. The playerCardHandNetIds list and the playerBattleCardNetId variable do not exist yet, and neither does the HandleUpdatedPlayerBattleCard hook function. These will all need to be created and added to GamePlayer.cs. First, the variables.

public SyncList<uint> playerCardHandNetIds = new SyncList<uint>();
[SyncVar(hook = nameof(HandleUpdatedPlayerBattleCard))] public uint playerBattleCardNetId;

playerCardHandNetIds will hold the network IDs of all the cards in a player’s hand. So, playerCardHandNetIds will need to be set when the player’s PlayerHand is created. The following line needs to be added to the CmdSpawnPlayerCards function in GamePlayer.cs.

requestingPlayer.playerCardHandNetIds.Add(playerCard.GetComponent<NetworkIdentity>().netId);

The next thing to create will be the HandleUpdatedPlayerBattleCard hook function. When a player selects a card and clicks the selectThisCard button, the network ID of that card will be added to playerCardNetworkId. When playerCardNetworkId is changed, the hook function HandleUpdatedPlayerBattleCard is called. The code for HandleUpdatedPlayerBattleCard is shown below.

void HandleUpdatedPlayerBattleCard(uint oldValue, uint newValue)
{
	if (isServer)
	{
		playerBattleCardNetId = newValue;
	}
	if (isClient)
	{
		Debug.Log("Running HandleUpdatedPlayerBattleCard as a client");
		if (hasAuthority)
		{
			GameplayManager.instance.HidePlayerHandPressed();
			RemoveSelectedCardFromHandAndReposition(newValue);
		}
	}
}

After playerBattleCardNetId is set, if it is running on the LocalGamePlayer object, it will first call HidePlayerHandPressed from GameplayManager.cs to hide the player’s hand. The next thing it will do is call RemoveSelectedCardFromHandAndReposition. That function doesn’t exist yet, but before it is created, a few new things will need to be added to allow it to work.

The first is creating a new GameObject variable in GamePlayer.cs called selectedCard.

public GameObject selectedCard;

SelectedCard will be used to store the card the player had selected when they clicked the selectThisCard button.

Next, in Card.cs, a new boolean variable called isClickable will be created.

public bool isClickable = true;

A new function will be created in PlayerHand.cs called AddCardBackToHand. The code is shown below.

public void AddCardBackToHand(GameObject cardToAdd)
{
	if (Hand.Contains(cardToAdd))
		return;
	Hand.Add(cardToAdd);
	cardToAdd.transform.SetParent(this.gameObject.transform);
	cardToAdd.transform.localScale = new Vector3(1.5f, 1.5f, 0f);
	cardToAdd.SetActive(false);
	Hand = Hand.OrderByDescending(o => o.GetComponent<Card>().Power).ToList();
}

AddCardBackToHand takes a GameObject argument. It will be used to add a card back into a player’s hand. This will be necessary because when RemoveSelectedCardFromHandAndReposition is eventually created, it will remove the selected card from the player’s hand. If a player then selects a card after their first selection, the new card will replace the first selection and the first selection will be added back to the player’s hand with AddCardBackToHand.

The last thing that will need to be added is a function called ShowPlayerCardScore in GameplayManager.cs.

public void ShowPlayerCardScore()
{
	if (LocalGamePlayerScript.selectedCard)
	{
		localCardText.SetActive(true);
		int playerScoreWithCard = LocalGamePlayerScript.playerBattleScore + LocalGamePlayerScript.selectedCard.GetComponent<Card>().Power;
		localCardPower.GetComponent<Text>().text = playerScoreWithCard.ToString();
		localCardPower.SetActive(true);
		showPlayerHandButton.GetComponentInChildren<Text>().text = "Change Card";
		if (!confirmCardButton.activeInHierarchy)
			confirmCardButton.SetActive(true);
	}
}

ShowPlayerCardScore will get the power value of the player’s selected card, calculate the player’s new combined battle score, and then update the PlayerBattlePanel to display that information.

Finally, the RemoveSelectedCardFromHandAndReposition can be added.

void RemoveSelectedCardFromHandAndReposition(uint SelectedCardNetId)
{
	Debug.Log("Executing RemoveSelectedCardFromHandAndReposition for card with network id:" + SelectedCardNetId.ToString());
	// if a card is already selected by the player, remove it as their selected card and add it back to their Hand
	if (selectedCard)
	{
		myPlayerCardHand.GetComponent<PlayerHand>().AddCardBackToHand(selectedCard);
		selectedCard.GetComponent<Card>().isClickable = true;
		selectedCard = null;
	}
	if (!selectedCard)
	{
		// set selectedCard and remove from the PlayerHand's Hand list
		selectedCard = NetworkIdentity.spawned[SelectedCardNetId].gameObject;
		myPlayerCardHand.GetComponent<PlayerHand>().Hand.Remove(selectedCard);
		selectedCard.GetComponent<Card>().isClickable = false;

		// move the card to be in the local player battle panel
		//selectedCard.transform.position = new Vector3(-5.25f, -1.5f, 0);
		//selectedCard.transform.localScale = new Vector3(1f, 1f, 1);
		selectedCard.SetActive(true);
		selectedCard.transform.SetParent(GameplayManager.instance.localPlayerBattlePanel.transform);
		selectedCard.transform.localPosition = new Vector3(-27f, -110f, 1f);
		selectedCard.transform.localScale = new Vector3(70f, 70f, 1f);
		GameplayManager.instance.ShowPlayerCardScore();
	}
}

RemoveSelectedCardFromHandAndReposition does the following:

  • Checks if the player had previously selected a card. If yes:
    • add the previously selected card back to the player’s hand with a call to AddCardBackToHand in PlayerHand.cs
    • Sets the card’s isClickable value to true
    • sets selectedCard to null so that there is no card stored in selectedCard anymore
  • If selectedCard is null:
    • uses the SelectedCardNetId argument to find the card the player selected by its network id. That card is then added to selectedCard
    • the selectedCard is removed from the player’s hand
    • selectedCard’s isClickable value is set to false
    • the selectedCard is set to active
    • the selectedCard is made a child object of the local player’s PlayerBattlePanel object
    • selectedCard is resized and repositioned so it appears within the local player’s PlayerBattlePanel object. The values for the resize and repositioned were found by manually playing around with things in the unity editor
    • ShowPlayerCardScore is called from GameplayManager.cs to update the player’s PlayerBattlePanel

One last upate is needed to make the isClickable value usefull. What all of the above does is take the card object the player selected and move it to the PlayerBattlePanel. Since that card is now out on the screen at all times, the player could click on it like the other cards in their hand, and it would spawn a card outline and all that jazz. So, the isClickable value was added so that MouseClickManager.cs would check that, and if the card was selected and out, the player couldn’t click on it again.

So, in MouseClickManager.cs, update SelectCardClicked so that all the CardClickedOn calls fall under a check for cardScript.isClickable being true.

And then the last, and maybe most important thing to add, is to add an OnClick function to the selectThisCard object. Add GameplayManager to selectThisCard, and then select the SelectThisCard function.

Save EVERYTHING, build and run, and then go through to get a battle scenario. Select your card, and you should see something like this:

Determine the Winner of the Battle

Now that the whole selecting a card thing is done, the “confirm card” button will appear and player’s can ready up for the next phase. When the phase changes from Choose Card, the server will calculate which player won the battle.

First, though, I’m going to make a few more cosmetic changes. Right now, in Choose Cards, the button to view your hand and select your card just says “Cards In Hand.” To make it more obvious to players that they need to click that button to choose their card, I want to change the button to say “Select Card.”

So, in GameplayManager.cs’s ActivateChooseCards function, add the following line:

showPlayerHandButton.GetComponentInChildren<Text>().text = "Select Card";

Then, to update the ready button to change from “confirm card” to “unready” when the player readies up, add the following to UpdateReadyButton

if (currentGamePhase.StartsWith("Choose Card"))
{
	if (LocalGamePlayerScript.ReadyForNextPhase)
	{
		Debug.Log("Local Player is ready to go to next phase.");
		confirmCardButton.GetComponentInChildren<Text>().text = "Unready";
		if (showPlayerHandButton.activeInHierarchy)
		{
			showPlayerHandButton.GetComponentInChildren<Text>().text = "Cards In Hand";
		}
		//Make cards in hand unclickable
		foreach (GameObject playerCard in LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().Hand)
		{
			Card playerCardScript = playerCard.GetComponent<Card>();
			playerCardScript.isClickable = false;
		}
	}
	else
	{
		Debug.Log("Local Player IS NOT ready to go to next phase.");
		confirmCardButton.GetComponentInChildren<Text>().text = "Confirm Card";
		if (showPlayerHandButton.activeInHierarchy)
		{
			if(LocalGamePlayerScript.selectedCard)
				showPlayerHandButton.GetComponentInChildren<Text>().text = "Change Card";
			else
				showPlayerHandButton.GetComponentInChildren<Text>().text = "Select Card";
		}
		//Make cards in hand clickable again
		foreach (GameObject playerCard in LocalGamePlayerScript.myPlayerCardHand.GetComponent<PlayerHand>().Hand)
		{
			Card playerCardScript = playerCard.GetComponent<Card>();
			playerCardScript.isClickable = true;
		}
	}
}

This actually does a lot, so let me spell it out

  • If the local player is ready:
    • set the confirmCardButton text to unready
    • change the showPlayerHandButton text back to “cards in hand”. This is to indicate to the player they no longer need to choose a card
    • Go through each card in the player’s hand and set isClickable to false. This prevents the player from clicking on cards when viewing their hand after they readied up
  • If the local player is NOT ready
    • set the text of confirmCardButton to “Confirm Card”
    • Check if the player has a card selected
      • If Yes: set showPlayerHandButton text to “change card”
      • If No: set showPlayerHandButton text to “Select Card”
    • Go through each card in the player’s hand and set isClickable to true

With all that out of the way, the server will now need to check who won the battle after both players ready up.

Under GamePlayer.cs’s CheckIfAllPlayersAreReadyForNextPhase function, a new check for Choose Cards will be added.

if (Game.CurrentGamePhase.StartsWith("Choose Card"))
{
	DetermineWhoWonBattle();
	Game.CurrentGamePhase = "Battle Results";
	Debug.Log("Game phase changed to Battle Results");
	RpcAdvanceToNextPhase(allPlayersReady, Game.CurrentGamePhase);
	return;
}

You see that the new game phase is set to Battle Results. First, though, DetermineWhoWonBattle is called by the server. That function will need to be added now.

[Server]
void DetermineWhoWonBattle()
{
	Debug.Log("Executing DetermineWhoWonBattle on server");
	GameplayManager.instance.winnerOfBattleName = "";
	GameplayManager.instance.winnerOfBattlePlayerNumber = -1;
	GameplayManager.instance.winnerOfBattlePlayerConnId = -1;
	GameplayManager.instance.reasonForWinning = "";
	GamePlayer player1 = null;
	GamePlayer player2 = null;

	foreach (GamePlayer gamePlayer in Game.GamePlayers)
	{
		if (gamePlayer.playerNumber == 1)
			player1 = gamePlayer;
		else
			player2 = gamePlayer;
	}
	Card player1Card = NetworkIdentity.spawned[player1.playerBattleCardNetId].gameObject.GetComponent<Card>();
	Card player2Card = NetworkIdentity.spawned[player2.playerBattleCardNetId].gameObject.GetComponent<Card>();

	int player1BattleScore = 0;
	int player2BattleScore = 0;
	//Calculate player 1 score
	player1BattleScore = player1.playerBattleScore;
	player1BattleScore += player1Card.Power;
	//Calculate player2 score
	player2BattleScore = player2.playerBattleScore;
	player2BattleScore += player2Card.Power;

	if (player1BattleScore > player2BattleScore)
	{
		Debug.Log("Player 1 wins battle. Player1 score: " + player1BattleScore.ToString() + " Player2 score: " + player2BattleScore.ToString());
		GameplayManager.instance.winnerOfBattleName = player1.PlayerName;
		GameplayManager.instance.winnerOfBattlePlayerNumber = player1.playerNumber;
		GameplayManager.instance.winnerOfBattlePlayerConnId = player1.ConnectionId;
		GameplayManager.instance.reasonForWinning = "Battle Score";
	}
	else if (player1BattleScore < player2BattleScore)
	{
		Debug.Log("Player 2 wins battle. Player1 score: " + player1BattleScore.ToString() + " Player2 score: " + player2BattleScore.ToString());
		GameplayManager.instance.winnerOfBattleName = player2.PlayerName;
		GameplayManager.instance.winnerOfBattlePlayerNumber = player2.playerNumber;
		GameplayManager.instance.winnerOfBattlePlayerConnId = player2.ConnectionId;
		GameplayManager.instance.reasonForWinning = "Battle Score";
	}
	else if (player1BattleScore == player2BattleScore)
	{
		//First tie breaker: Player with highest card value wins
		if (player1Card.Power > player2Card.Power)
		{
			Debug.Log("Player 1 wins first tie breaker: Higher card power");
			GameplayManager.instance.winnerOfBattleName = player1.PlayerName;
			GameplayManager.instance.winnerOfBattlePlayerNumber = player1.playerNumber;
			GameplayManager.instance.winnerOfBattlePlayerConnId = player1.ConnectionId;
			GameplayManager.instance.reasonForWinning = "Tie Breaker 1: Highest Card Power";
		}
		else if (player1Card.Power < player2Card.Power)
		{
			Debug.Log("Player 2 wins first tie breaker: Higher card power");
			GameplayManager.instance.winnerOfBattleName = player2.PlayerName;
			GameplayManager.instance.winnerOfBattlePlayerNumber = player2.playerNumber;
			GameplayManager.instance.winnerOfBattlePlayerConnId = player2.ConnectionId;
			GameplayManager.instance.reasonForWinning = "Tie Breaker 1: Highest Card Power";
		}
		else if (player1Card.Power == player2Card.Power)
		{
			//Checking for second tie breaker: Player with most infantry wins
			if (player1.playerArmyNumberOfInf > player2.playerArmyNumberOfInf)
			{
				Debug.Log("Player 1 wins second tie breaker: More infantry than other player");
				GameplayManager.instance.winnerOfBattleName = player1.PlayerName;
				GameplayManager.instance.winnerOfBattlePlayerNumber = player1.playerNumber;
				GameplayManager.instance.winnerOfBattlePlayerConnId = player1.ConnectionId;
				GameplayManager.instance.reasonForWinning = "Tie Breaker 2: Most Infantry";
			}
			else if (player1.playerArmyNumberOfInf < player2.playerArmyNumberOfInf)
			{
				Debug.Log("Player 2 wins second tie breaker: More infantry than other player");
				GameplayManager.instance.winnerOfBattleName = player2.PlayerName;
				GameplayManager.instance.winnerOfBattlePlayerNumber = player2.playerNumber;
				GameplayManager.instance.winnerOfBattlePlayerConnId = player2.ConnectionId;
				GameplayManager.instance.reasonForWinning = "Tie Breaker 2: Most Infantry";
			}
			else if (player1.playerArmyNumberOfInf == player2.playerArmyNumberOfInf)
			{
				Debug.Log("The battle was a tie! Same card power and same number of infantry. Player1 score: " + player1BattleScore.ToString() + " Player2 score: " + player2BattleScore.ToString());
				GameplayManager.instance.winnerOfBattleName = "tie";
				GameplayManager.instance.winnerOfBattlePlayerNumber = -1;
				GameplayManager.instance.winnerOfBattlePlayerConnId = -1;
				GameplayManager.instance.reasonForWinning = "Draw: No Winner";
			}
		}
	}
}

So, the victory conditions in DetermineWhoWonBattle are as follows:

  • Player with the highest battle score
  • if battle score is equal:
    • player with highest card score
    • if card scores are equal:
      • player with most infantry
      • If players have same number of infantry:
        • Draw. No winner

There are a LOT of squiggly lines right now and that is because a lot of variables need to be added to GameplayManager.cs

[Header("Battle Results")]
[SyncVar] public string winnerOfBattleName;
[SyncVar] public int winnerOfBattlePlayerNumber;
[SyncVar] public int winnerOfBattlePlayerConnId;
[SyncVar] public string reasonForWinning;

So now GameplayManager will store the name, player number, and connection ID of the player who won the battle. It will also store the “reason for winning,” or the victory condition that was met for their victory. All of this is set by the DetermineWhoWonBattle function run on the server.

Now, all that will be done is to advance the phase locally. In GameplayManager.cs’s ChangeGamePhase function, add the following to change from Choose Cards to Battle Results.

if (currentGamePhase.StartsWith("Choose Cards") && newGamePhase == "Battle Results")
{
	MouseClickManager.instance.canSelectUnitsInThisPhase = false;
	MouseClickManager.instance.canSelectPlayerCardsInThisPhase = false;
	currentGamePhase = newGamePhase;
	StartBattleResults();
}

StartBattleResults will need to be created as well. For right now, it will be simple and just update the GamePhase text object.

void StartBattleResults()
{
	Debug.Log("Starting Battle Results");
	SetGamePhaseText();
}

Save everything, create a battle, and advance the phase. In the console, you should see the log output say who won:

The Battle Results section of GameplayManager should also reflect the winner:

You can go through all the tie scenarios and see if they work. They seem to for me, so great! I think we’re done here…

Next Steps…

Now that the battle results are calculated, the following will need to be done:

  • Display battle results to the players
  • Determine how many, if any, units from the losing player are destroyed in the battle
    • The “Attack” and “Defense” values of the player cards will need to be calculated as they determine destroyed units
  • When the phase advances, move the players’ battle cards to their discard pile

Smell ya later nerds!