While a card’s stats such as cost, attack, and hit points can add a lot of strategy to how you build your deck, its set of abilities may impact your strategy even more. In this lesson we will add a new ability to our minion cards called Taunt, so that whenever it appears on the battlefield, the enemy must attack it before it can attack non-taunt minions.
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.
Card
Taunt is considered a type of “mechanic” or “ability” that can appear on minion cards. There are a variety of these including: Battlecry, Charge, and Divine Shield. You can read more about them all here. The goal for this lesson is to add the concept of these sorts of abilities to our cards in a very flexible way.
There are tons of different ways to implement your data models. In my first prototype, the card models all had additional List fields for things like abilities, effects, and mechanics. Part of the decision of how you structure your data will probably be determined by how you want to persist the data. Some structures might be better suited to working with JSON for example. For this second protoype, I am deferring the decision about how to persist the data until later and decided to architect this feature based on what would be most convenient to work with.
I am a fan of Entity-Component architecture, and want to use it again here. We have already built a custom Container-Aspect library which we have been using to hold our game systems, and it would also work well for the abilities of our cards. For example, the Card could itself be a subclass of Container, and the abilities, like Taunt, could be implemented as Aspects of the Card.
We could actually push the idea a bit further and make the Card an Aspect that appears on a generic Container instance. However, in practice, I would rather have a Deck of Cards than a Deck of Containers for which I needed to “Get The Card Aspect” repeatedly. Although this alternate approach would be more flexible, I felt that it would come at the cost of more verbose code and that I wouldn’t actually utilize the extra flexibility.
So… with my final decision made, all we have to do is add the using statement for our AspectContainer code and to make the Card class inherit from the Container class like so:
[csharp]
using TheLiquidFire.AspectContainer;
public class Card : Container {
// … pre-existing code here
}
[/csharp]
Taunt
Now we can define the class that will be attached as an aspect to our card. The mere presence of the aspect will cause different behaviour in an upcoming system – no fields needed.
[csharp]
public class Taunt : Aspect {
}
[/csharp]
Taunt System
We will create a new system to handle the concept of taunting and how it can effect our gameplay. It will be an Aspect so that we can add it as a game system alongside our other systems. It will also implement the IObserve interface because it will respond to a notification sent by our AttackSystem so that it can filter the allowed attack targets based on the presence of a Taunt minion on the battlefield.
[csharp]
public class TauntSystem : Aspect, IObserve {
public void Awake () {
this.AddObserver (OnFilterAttackTargets, AttackSystem.FilterTargetsNotification, container);
}
public void Destroy () {
this.RemoveObserver (OnFilterAttackTargets, AttackSystem.FilterTargetsNotification, container);
}
void OnFilterAttackTargets (object sender, object args) {
var candidates = args as List
if (TargetsContainTaunt (candidates) == false)
return;
for (int i = candidates.Count – 1; i >= 0; –i) {
if (candidates [i].GetAspect
candidates.RemoveAt (i);
}
}
bool TargetsContainTaunt (List
foreach (Card card in cards) {
if (card.GetAspect
return true;
}
return false;
}
}
[/csharp]
As you might have expected, I use the Awake and Destroy methods (required by IObserve) to add and remove the observer of the notification for filtering attack targets. The handler of the notification is the “OnFilterAttackTargets” method. I implemented another private method called “TargetsContainTaunt” to quickly determine whether or not the list of targets included a Taunt minion at all. If there are no Taunt minions in the list then no filtering will be necessary. If there is at least one taunt minion, then I need to loop through the list of candidates and remove any that don’t also have a Taunt aspect.
Game Factory
We added a new system, so we need to make sure it gets included in our game. Add the following to the GameFactory’s “Create” method:
[csharp]
game.AddAspect
[/csharp]
Minion View
We will also need a way to make sure a user can differentiate Taunt minions from non-Taunt minions that appear on the battlefield. I have already created sprites specific to Taunt which look like there is a shield around thier border. Inside of the “Refresh” method, change the following line:
[csharp]
avatar.sprite = isActive ? active : inactive;
[/csharp]
to this:
[csharp]
if (minion.GetAspect
avatar.sprite = isActive ? active : inactive;
} else {
avatar.sprite = isActive ? activeTaunt : inactiveTaunt;
}
[/csharp]
Game View System
Finally, we simply need to make sure that some of the cards in our deck have this new feature applied to them. Inside of the “Temp_SetupSinglePlayer” method and inside of the innermost “for” loop where we are creating and configuring cards, add the following:
[csharp]
if (i % 3 == 0) {
card.AddAspect (new Taunt ());
card.text = “Taunt”;
}
[/csharp]
This will cause about a third of our cards to be given the new “Taunt” feature. Note that I also set card text to indicate whether or not a card has the feature in a way that can be viewed while the card is still in your hand.
Demo
The game is now ready to be played with a new feature! Depending on the luck of the order of cards that you draw and which cards get Taunt, you may notice that the game is now slightly harder. Regardless, try to experiement – wait for your opponent to get a couple of minions on the battlefield with at least one taunt minion. Try to initate an attack to a non-taunt minion or the opponent hero, and see that the action will fail. You must kill taunt minions before you can attack anything else.
Summary
In this lesson we added a flexible foundation for our cards to have a variety of abilities. We also fully implemented our first ability – Taunt. This feature will filter what is considered a valid attack target based on the presence of a taunt minion – it must be targeted before non-taunt minions.
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!
Hello. I am very happy that you showing us how to create a card game. I don’t use unity, but Monogame. Can you give me a tip how to implement a minion effects like “when a spell is played create 1/1”. What is the best approach, in your opinion? I don’t know how to register an effect when a minion appears on the board.
I am glad to help where I can. I have no experience with Monogame, so I don’t know how much of what I have done here is applicable – in particular the way that I have tied game actions and animations together. In my own project, adding a feature like this could look something like the following:
– I would add a card ability to mark a card as having the triggerable summon feature (much like I did with Taunt in this lesson)
– I would add a system to watch for triggers on cards (such as the “when a spell is played” requirement)
– When a trigger is encountered, I could then add a new game action(s) to create and summon the new card as a reaction to the triggering action
If you have followed along with the whole series, hopefully this basic flow will make sense.
Thanks so much for the tutorials! I am still learning programming and Unity, but I was able to follow along well enough and even implement charge, windfury, and deadly based on your taunt mechanism.
I don’t understand some of the concepts like how notifications work or exactly what an interface is, but I guess that just means I’ll need to go back and do your previous tutorials!
My two most anticipated features are mouse-over card displays (especially now that some minions randomly have deadly!) and how to create “databases” of cards with unique and complex abilities. Thanks again!
Then again, the more I think about what could be added next, the more I realize how complex this type of game is. Good luck! XD
That is great! I consider this my greatest accomplishment of the project- to both inspire and enable another!
Are you a Patron yet? If not and you consider it so useful what Jon does here, you should definitely consider giving him at least a dollar per month for at least the months he is working on this fantastic series for a CCG.
Just my 2ct, though … 🙂
Cheers,
Daniel.
That’s very kind of you to say 🙂
Hi, I don’t know if you already fixed it, but I realized that the ManaView keeps receiving the notification for both players, which makes the mana slots update even when the computer plays. I fixed it on my project by sending the currentPlayer as args to the ManaSystem.ValueChangeNotificaiton instead of the mana itself, so if the currentPlayer index isn’t zero, the view shouldn’t update.