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.
[csharp]
[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;
}
}
[/csharp]
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.
[csharp]
[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;
}
}
[/csharp]
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.
[csharp]
public class Mark {
public Alliance alliance;
public Zones zones;
public Mark (Alliance alliance, Zones zones) {
this.alliance = alliance;
this.zones = zones;
}
}
[/csharp]
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.
[csharp]
public class Target : Aspect {
public bool required;
public Mark preferred;
public Mark allowed;
public Card selected;
}
[/csharp]
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).
[csharp]
public class TargetSystem : Aspect, IObserve {
// … Add Next Code Here
}
[/csharp]
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.
[csharp]
public void Awake () {
this.AddObserver (OnValidatePlayCard, Global.ValidateNotification
}
public void Destroy () {
this.RemoveObserver (OnValidatePlayCard, Global.ValidateNotification
}
void OnValidatePlayCard (object sender, object args) {
var playCardAction = sender as PlayCardAction;
var target = playCardAction.card.GetAspect
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 ();
}
}
[/csharp]
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.
[csharp]
public void AutoTarget (Card card, ControlModes mode) {
var target = card.GetAspect
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;
}
[/csharp]
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.
[csharp]
List
var marks = new List
var players = GetPlayers (source, mark);
foreach (Player player in players) {
var cards = GetCards (source, mark, player);
marks.AddRange (cards);
}
return marks;
}
[/csharp]
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.
[csharp]
List
var card = source.container as Card;
var dataSystem = container.GetAspect
var players = new List
var pair = new Dictionary
{ 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;
}
[/csharp]
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.
[csharp]
List
var cards = new List
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;
}
[/csharp]
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:
[csharp]
game.AddAspect
[/csharp]
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.
[csharp]
public void Refresh (ControlModes mode) {
var match = container.GetMatch ();
var targetSystem = container.GetAspect
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);
}
}
[/csharp]
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:
[csharp]
public override void Enter () {
var mode = container.GetMatch ().CurrentPlayer.mode;
container.GetAspect
container.GetAspect
if (mode == ControlModes.Computer)
container.GetAspect
this.PostNotification (EnterNotification);
}
[/csharp]
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.
[csharp]
private class ShowTargetState : BaseControllerState {
public override void Enter () {
base.Enter ();
owner.StartCoroutine (HideProcess ());
}
IEnumerator HideProcess () {
var handView = owner.activeCardView.GetComponentInParent
yield return owner.StartCoroutine (handView.LayoutCards (true));
owner.stateMachine.ChangeState
}
}
[/csharp]
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:
[csharp]
private class TargetState : BaseControllerState, IClickableHandler {
public void OnClickNotification (object sender, object args) {
var target = owner.activeCardView.card.GetAspect
var cardView = (sender as Clickable).GetComponent
if (cardView != null) {
target.selected = cardView.card;
} else {
target.selected = null;
}
owner.stateMachine.ChangeState
}
}
[/csharp]
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.
[csharp]
private class ConfirmOrCancelState : BaseControllerState, IClickableHandler {
public void OnClickNotification (object sender, object args) {
var cardView = (sender as Clickable).GetComponent
if (owner.activeCardView == cardView) {
var target = owner.activeCardView.card.GetAspect
if (target != null) {
owner.stateMachine.ChangeState
} else {
owner.stateMachine.ChangeState
}
} else {
owner.stateMachine.ChangeState
}
}
}
[/csharp]
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.
[csharp]
container.AddAspect (new ShowTargetState ()).owner = this;
container.AddAspect (new TargetState ()).owner = this;
[/csharp]
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:
[csharp]
void Temp_AddTargeting(Card card) {
var random = UnityEngine.Random.Range (0, 3);
var target = card.AddAspect
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;
}
}
[/csharp]
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.
[csharp]
Temp_AddTargeting (card);
[/csharp]
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!
first off – thanks for the tutorials! curious on implementing secrets – as i am going to be modifying that to do all sorts of funky stuff (playing them on opponents etc)
was wondering though i downloaded this lesson and i click on any place on the battlefield and it will summon the creature no matter what – i thought it might be a targetting check that might be not be quite right if it wasnt a newer unity change issue …basically i tested selecting my hero when it says ‘ target enemy minion’ and when it said select ally minion i selected the enemy minion and it summons it anyway.
thanks again!
In practice, working with secrets should be very similar to working with minions. When you play a minion, the card is moved to the battlefield instead of being moved to the graveyard. When you play a secret, it should move to its own zone instead of to the graveyard. You wont necessarily need to display a new view for each secret, though it will certainly help to show that a secret is in play in one form or another.
Keep in mind that there are a variety of targeting rules here, such as some moves might request a target but not require it. Others may prefer a certain type of target but work on others for unique strategies. Check the configuration for the specific card and see if your action should have been allowed or if you have found a bug.
True that would work – with some differences – cant be shown to the opponent until it activates and when it activates then the spell is cast which needs to be shown to the player so they know what is being triggered – besides that and maybe issues with triggering of triggers which trigger other triggers and the order of how things trigger – everything else i think should be the same
Ya i updated to the latest as well and am running into it – a little harder cause there is like one spell that has a manual target – that 6 damage one — when i select a target it works fine – when i click off on the board – the card goes away and im not quite sure what it does – i dont think it is used …again this is just downloading the zip that you had and running it right away.
I can confirm that there is a bug in the targeting code as it stands at the end of this tutorial. It seems that as long as there is a valid target for the card in play, the card will be played even if the target you click on is invalid. For example, if a card requires an enemy minion target, but there are no enemy minions in play, you cannot play the card as intended. But, if there’s an enemy minion in play and you attempt to play the card and select one of the heroes as the target, the card gets played anyway.
I’m not sure exactly why this is happening, but I think it has something to do with the AutoTarget method re-setting the target to something valid and overriding what gets set in the ClickToPlayCardController.
Thanks for confirming. I’ll try to look into it this week and maybe commit a fix to the repository.
Actually I think what you are talking about was already resolved in the next lesson “Spell & Abilities” – check out the section header “More Refactoring” for addressing this bug.
Ah, so it is! Thanks for the quick reply.