Make a CCG – Targeting

Many cards require a “target” as part of their play requirements. For example, a spell might heal an injured ally, or a minion’s battlecry ability may deal damage to an opponent. Sometimes the target(s) can be chosen automatically, and other times they require the user to manually pick. In this lesson we will begin the process of supporting manually targeted play actions.

Go ahead and open the project from where we left off, or download the completed project from last week here. I have also prepared an online repository here for those of you familiar with version control.

Alliance

In order to be able to determine the rules about what is considered a “valid” or even “desirable” target, we need a way to describe the options for a target as data. I created a new enum called “Alliance” to indicate the sides of a match from the perspective of any given card. For example, even if it is your turn, an opponent’s card will understand that its own “Ally” cards are cards owned by the opponent player.

The enum is declared as a bitmask so that with a single value I can indicate any specific side, or any combination of sides. If you aren’t familiar with bitmasks, I have a special tutorial here. For better readability, I also provided an extension method “Contains” to see if a given enum holds the specified bit or not.

[Flags]
public enum Alliance {
	None = 0,
	Ally = 1 << 0,
	Enemy = 1 << 1,
	Any = Ally | Enemy
}

public static class AllianceExtensions {
	public static bool Contains (this Alliance source, Alliance target) {
		return (source & target) == target;
	}
}

Zones

We already have an enum to describe the various collections of a player’s cards. Let’s turn it into a bitmask as well, so that we can easily define groups of zones with a single value, just like we did for the Alliance enum.

[Flags]
public enum Zones {
	None = 0,
	Hero = 1 << 0,
	Weapon = 1 << 1,
	Deck = 1 << 2,
	Hand = 1 << 3,
	Battlefield = 1 << 4,
	Secrets = 1 << 5,
	Graveyard = 1 << 6,
	Active = Hero | Battlefield
}

public static class ZonesExtensions {
	public static bool Contains (this Zones source, Zones target) {
		return (source & target) == target;
	}
}

Note that this wont “break” any of our existing code, although it might change the expectations of its usage. For example, the indexer in the “Player” class will only provide a list of cards given a single zone type. A programmer who isn’t familiar with the code (or yourself at some point in the future) may believe you could use a value with several bits enabled to select the cards from multiple lists simultaneously. On one hand, that might actually be a nice feature. On the other hand, the implementation of such a feature may result in the frequent creation of new and unneeded List objects, and you may prefer to avoid the extra triggers of garbage collection as much as possible.

Mark

To minimally define any candidate target, I need both an “Alliance”, and a “Zone” to pick from. For example, a card which reads that it can heal “Any Target”, might be defined by an “Alliance” of “Any” and a “Zones” of “Active”. Another card might read “Deal ‘X’ damage.” and could use an “Alliance” of “Enemy” and a “Zones” of “Battlefield”. Therefore I created a new object that can hold this pair of information.

public class Mark {
	public Alliance alliance;
	public Zones zones;

	public Mark (Alliance alliance, Zones zones) {
		this.alliance = alliance;
		this.zones = zones;
	}
}

Target

The idea of a card which works with a “Target” is a bit more involved than merely specifying a “Mark”. I created it as an Aspect so that it can be attached to a card, and it has several fields to help define it.

public class Target : Aspect {
	public bool required;
	public Mark preferred;
	public Mark allowed;
	public Card selected;
}

The “required” field provides a way to indicate whether a manual target must be specified as part of the card’s play requirements. If the value is “true” then the target “must” be specified when playing the card. If the value is “false” then the card can be played even if a target is not available.

The “preferred” and “allowed” Mark fields indicate the difference between what can be considered a “valid” target, and what should be considered an “ideal” target. For example, a spell card may allow you to apply damage to any minion, but as a general rule it would only make sense to apply that card to enemies. It is only in special cases that you would want to target an ally, such as if damaging an ally minion caused a beneficial side-effect through some sort of other ability. The primary purpose of the “preferred” Mark is to help the A.I. determine how to play its cards.

The “selected” Card field holds the manually selected target that was chosen while playing the source card. In the future, a Game Action can be triggered and then refer to the selection here to know what to apply the Action against.

Target System

Now that we can model a target, we need a system to actually work with it. For example, given a target for an “Enemy Minion”, we need to be able to select the candidate cards of the Opponent’s Battlefield Zone which are in play at that time. Other systems could then highlight the cards to help a user know what to select, or could be passed to an A.I. to pick from at random (or via additional rules if desired).

public class TargetSystem : Aspect, IObserve {
	// ... Add Next Code Here
}

To begin with, we define a new class called TargetSystem. It is a subclass of “Aspect” because it will be attached to the same container as our other game systems. It will also implement the “IObserve” interface, because we will allow this system to validate attempts to play cards based on the input target provided by a user.

public void Awake () {
	this.AddObserver (OnValidatePlayCard, Global.ValidateNotification<PlayCardAction> ());
}

public void Destroy () {
	this.RemoveObserver (OnValidatePlayCard, Global.ValidateNotification<PlayCardAction> ());
}

void OnValidatePlayCard (object sender, object args) {
	var playCardAction = sender as PlayCardAction;
	var target = playCardAction.card.GetAspect<Target> ();
	if (target == null || (target.required == false && target.selected == null))
		return;
	var validator = args as Validator;
	var candidates = GetMarks (target, target.allowed);
	if (!candidates.Contains(target.selected)) {
		validator.Invalidate ();
	}
}

We need to add the “Awake” and “Destroy” methods required by the “IObserve” interface and implement them to register and unregister for the validation notification related to playing a card. The notification handler can then invalidate a play action, if needed, based on targeting rules. If the card doesn’t even have a “Target” Aspect, or if a target is not required and has not been selected, then we just abort early. If a target is required, then the target’s selection must be found in the list of allowed candidates. Any selection outside of the candidate list will lead us to invalidate the action.

public void AutoTarget (Card card, ControlModes mode) {
	var target = card.GetAspect<Target> ();
	if (target == null)
		return;
	var mark = mode == ControlModes.Computer ? target.preferred : target.allowed;
	var candidates = GetMarks (target, mark);
	target.selected = candidates.Count > 0 ? candidates.Random() : null;
}

I have provided a method called “AutoTarget” which can randomly pick a valid target (if available) for any given card. This can be used at the beginning of a local user’s turn to determine which of the cards in his hand can be played (in case you want to highlight them for a better UI experience), and it can also be used at the beginning of an A.I. turn so that it knows whether or not it holds any playable cards. I pass in the “ControlModes” enum of the current player because the A.I. should only be interested in “preferred” matches, whereas a human player only needs to know what he is “allowed” to do, regardless of it is is a good idea or not.

List<Card> GetMarks (Target source, Mark mark) {
	var marks = new List<Card> ();
	var players = GetPlayers (source, mark);
	foreach (Player player in players) {
		var cards = GetCards (source, mark, player);
		marks.AddRange (cards);
	}
	return marks;
}

The “GetMarks” method creates a List of all candidate cards that match the specified Mark of a target. It begins by getting a List of players that match the mark’s alliance flags, and then loops over the players, appending each player’s cards from each of the matching zones.

List<Player> GetPlayers (Target source, Mark mark) {
	var card = source.container as Card;
	var dataSystem = container.GetAspect<DataSystem> ();
	var players = new List<Player> ();
	var pair = new Dictionary<Alliance, Player> () {
		{ Alliance.Ally, dataSystem.match.players[card.ownerIndex] }, 
		{ Alliance.Enemy, dataSystem.match.players[1 - card.ownerIndex] }
	};
	foreach (Alliance key in pair.Keys) {
		if (mark.alliance.Contains (key)) {
			players.Add (pair[key]);
		}
	}
	return players;
}

The “GetPlayers” method knows how to produce a List of Player objects that match the “Alliance” of a given “Mark”. The resulting list could potentially be anywhere from 0 to 2 players depending on the bit mask used. Note that the “Ally” and “Enemy” are chosen based on the perspective of the “Card” rather than by which player is the current player. This can be an important issue, because a card’s ability can be potentially “triggered” even if its owning player is not the active player.

List<Card> GetCards (Target source, Mark mark, Player player) {
	var cards = new List<Card> ();
	var zones = new Zones[] { 
		Zones.Hero, 
		Zones.Weapon, 
		Zones.Deck, 
		Zones.Hand, 
		Zones.Battlefield, 
		Zones.Secrets, 
		Zones.Graveyard 
	};
	foreach (Zones zone in zones) {
		if (mark.zones.Contains (zone)) {
			cards.AddRange (player[zone]);
		}
	}
	return cards;
}

The “GetCards” method knows how to produce a List of Card objects that match the “Zones” of a given “Mark” for a given “Player”. Note that unlike the Player indexer, this method will always build a new list that contains all of the cards in all of the compatible zones that had been specified.

Game Factory

You know the drill (I hope). We added a new game system, so we need to add it to the game container in the factory’s Create method:

game.AddAspect<TargetSystem> ();

Card System

We will need to update the Card System’s refresh method. We will want to pass a “ControlModes” parameter based on the mode of the current player because we want targeted cards for the A.I. to only be considered playable if they match a “preferred” mark mode. We also need to apply the “Auto Targeting” feature of our new system before we attempt to validate whether or not a card can actually be played.

public void Refresh (ControlModes mode) {
	var match = container.GetMatch ();
	var targetSystem = container.GetAspect<TargetSystem> ();
	playable.Clear ();
	foreach (Card card in match.CurrentPlayer[Zones.Hand]) {
		targetSystem.AutoTarget (card, mode);
		var playAction = new PlayCardAction (card);
		if (playAction.Validate ())
			playable.Add (card);
	}
}

Player Idle State

Now we need to update the “Enter” method of the Player Idle State so that it passes along the control mode of the current player to our CardSystem’s Refresh method:

public override void Enter () {
	var mode = container.GetMatch ().CurrentPlayer.mode;
	container.GetAspect<AttackSystem> ().Refresh ();
	container.GetAspect<CardSystem> ().Refresh (mode);
	if (mode == ControlModes.Computer)
		container.GetAspect<EnemySystem> ().TakeTurn ();
	this.PostNotification (EnterNotification);
}

Click To Play Card Controller

Our current input flow requires a user to click a card to preview it, and then click again to confirm and play a card. Now that we have added manual targeting, our flow will need to be modified for any card that requires a target. We will be adding two new states for this.

private class ShowTargetState : BaseControllerState {
	public override void Enter () {
		base.Enter ();
		owner.StartCoroutine (HideProcess ());
	}

	IEnumerator HideProcess () {
		var handView = owner.activeCardView.GetComponentInParent<HandView> ();
		yield return owner.StartCoroutine (handView.LayoutCards (true));
		owner.stateMachine.ChangeState<TargetState> ();
	}
}

Our first new state, “ShowTargetState”, is responsible for hiding the “previewed” card so that it isn’t blocking our view and preventing us from actually selecting the target. It is basically a copy of the “CancellingState” but once the cards are hidden it will transition to our next new state:

private class TargetState : BaseControllerState, IClickableHandler {
	public void OnClickNotification (object sender, object args) {
		var target = owner.activeCardView.card.GetAspect<Target> ();
		var cardView = (sender as Clickable).GetComponent<BattlefieldCardView> ();
		if (cardView != null) {
			target.selected = cardView.card;
		} else {
			target.selected = null;
		}
		owner.stateMachine.ChangeState<ConfirmState> ();
	}
}

Our other new state, “TargetState”, accepts a click notification. The click notification handler will be looking for objects that are a type of “BattlefieldCardView” such as the Hero and Minion Card views. If one was clicked, the card that is represented by the view is assigned as the Target’s selection. Note that no validation is done at this time, because our business rules are applied by the “TargetSystem” and it is best not to have duplicate code scattered around your project. Even if we create an invalid “Play” action based on a bad target, the action will know not to performed, and we can potentially even use that failure moment as an opportunity to inform a user of thier mistake.

private class ConfirmOrCancelState : BaseControllerState, IClickableHandler {
	public void OnClickNotification (object sender, object args) {
		var cardView = (sender as Clickable).GetComponent<CardView> ();
		if (owner.activeCardView == cardView) {
			var target = owner.activeCardView.card.GetAspect<Target> ();
			if (target != null) {
				owner.stateMachine.ChangeState<ShowTargetState> ();
			} else {
				owner.stateMachine.ChangeState<ConfirmState> (); 
			}
		} else {
			owner.stateMachine.ChangeState<CancellingState> ();
		}
	}
}

The “ConfirmOrCancelState” already existed, but has been modified. It used to be that a second click of the active card would lead straight to the “ConfirmState” and trigger the play action. Now, we need to check for the “Target” aspect of a card first. If found, we need to transition to the “ShowTargetState” instead.

container.AddAspect (new ShowTargetState ()).owner = this;
container.AddAspect (new TargetState ()).owner = this;

Finally, we need to prepopulate the state machine with its states and configure them. We add the new states in the “Awake” method where we had added the other states.

Prefabs

Select the “Hero View” and “Minion View” prefabs in the “Project” pane and add a “Clickable” component to them. This is required in order for them to listen to click notifications in the “Click To Play Card Controller”. Don’t forget to save the project, or your prefab changes will be lost.

Game View System

Our engine now supports targeting, but we still need to create cards that include the feature. Open the “GameViewSystem” class and add the following:

void Temp_AddTargeting(Card card) {
	var random = UnityEngine.Random.Range (0, 3);
	var target = card.AddAspect<Target> ();
	var text = string.IsNullOrEmpty (card.text) ? "" : card.text + ". ";
	switch (random) {
	case 0:
		target.required = false;
		target.allowed = target.preferred = new Mark (Alliance.Ally, Zones.Active);
		card.text = text + "Ally Target if available";
		break;
	case 1:
		target.required = true;
		target.allowed = target.preferred = new Mark (Alliance.Enemy, Zones.Active);
		card.text = text + "Enemy Target required";
		break;
	case 2:
		target.required = true;
		target.allowed = target.preferred = new Mark (Alliance.Enemy, Zones.Battlefield);
		card.text = text + "Enemy Minion Target required";
		break;
	default:
		// Don't add anything
		Debug.LogError("Shouldn't have gotten here");
		break;
	}
}

Keep in mind this is just placeholder code from demonstration purposes only – this is emphasized by the fact that I gave the method name a prefix of “Temp”. For the purposes of this lesson, I am adding a random target aspect to every card in the deck. I created a few different kinds so that you could see our feature working with a little variety. I also made sure to update the card text so you would know which kind of targeting is expected. Remember that you wont be able to play a card if you can’t provide the right target, so this is important info to present.

Temp_AddTargeting (card);

We will invoke the new method inside of the “Temp_SetupSinglePlayer” method’s inner-most “for loop”, immediately following the condition where we add “Taunt” to a third of our cards.

Demo

Go ahead and run the scene. It may take a bit before you get a card you can play, because now we have targeting requirements on top of the mana requirement. Try to play a card using the wrong type of target and note that it wont succeed. Then try to play a card by clicking on a valid target – the minion will be summoned as expected.

Summary

There are plenty of abilities that choose their own target, but being able to pick your own is pretty important too. In this lesson we laid the foundation for our game engine to support manually targeted cards as a play requirement. This extra step for targeting doesn’t actually serve a purpose yet, but it will! This is an important step we needed to take before being able to add targeted abilities like “Battlecry”, and will be necessary for other card types like “Spells” which also often require a manually chosen target.

You can grab the project with this lesson fully implemented right here. If you are familiar with version control, you can also grab a copy of the project from my repository.

If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *