Make a CCG – Spells & Abilities

Any card can have special abilities – by this I mean that it can cause one or more of our “Game Actions” to trigger based on special criteria. Spell cards are unique in that they must have at least one ability in order to serve a purpose. In this lesson, we will begin implementing spells, and by necessity, will also create an ability system that we can apply to our other cards as well.

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.

Ability

As you have already seen, a Card is a Container, therefore we can add new “Abilities” such as Taunt as an Aspect of the card. Some abilities may be more complex and could potentially be partially defined by aspects of their own. If a card has more than one complex ability, then we need a way to differentiate between the aspects of abilities, which are themselves aspects. Let’s take a look at how to solve this:

[csharp]
public class Ability : Container, IAspect {
public IContainer container { get; set; }
public Card card { get { return container as Card; } }
public string actionName { get; set; }
public object userInfo { get; set; }
}
[/csharp]

In the code above, I defined the class Ability as a subclass of the Container class, but also made it implement the IAspect interface by adding its own IContainer field. This allows the class to be both an aspect and a container at the same time. We can add the Ability as an aspect to a Card, and then add aspects to the Ability that are separate from the aspects of the Card.

For convenience I added a “card” property which wraps the “container” field and casts it accordingly. Many abilities will want to know about the card that they are on, so this might come in handy.

I also added an “actionName” which is the name of a “Game Action” class that we will instantiate when the ability is triggered. The “userInfo” will then be used to help configure the instantiated object and will hold whatever information might be needed. For example, if the Game Action was a “Damage Action” then the information could hold the “amount” of damage to apply.

Target Selectors

In the previous post we mentioned a way to target cards. The purpose of targeting cards was for abilities to know what to operate on, but not all abilities will use manual targeting. Some ability targets might be random, while others could even target entire groups of cards. We will be making new aspects of an Ability to determine what to select, but each aspect that serves this purpose will need to conform to a new interface:

[csharp]
public interface ITargetSelector : IAspect {
List SelectTargets (IContainer game);
}
[/csharp]

Because a target selector will know what ability it is tied to, and by extension will know what card it is part of, we wont need to provide much in the way of parameters. We will provide the game system container, because in this case I decided to put the selection logic in the classes themselves rather than have one continuosly growing system class for the variety of selection modes. Controller logic like this will need access to the other systems to grab references to the various players and their cards.

Target System

Even though the logic for selecting cards will be found in the instances of the target selectors themselves, I decided to do a little refactoring on the Target System. I wanted to make the “GetMarks” method public, and to make its first parameter a “Card” instead of a “Target”. This way the “Target Selector” instances will be able to reuse the same candidate finding code that a “Target” will use:

[csharp]
// Change method definition to this
public List GetMarks (Card source, Mark mark)
[/csharp]

This refactor also requires that we propogate the parameter type change into the method declarations for “GetPlayers” and “GetCards” as well. The “GetPlayers” method wont need to make its own local reference to a card anymore, so delete that line, and update the definition for a “pair” appropriately.

These changes will also require you to update the “AutoTarget” and “OnValidatePlayCard” methods which had invoked the “GetMarks” method using the “Target” instead of the “Card”. If you struggle to complete these changes, please refer to the completed project or the code in my repository.

Manual Target Selector

Our first fully implemented “target selector” will be the “manual” selector – it is designed to work with the card’s “target” aspect created in the previous lesson, such that any ability using this variant will apply its ability based on the “selected” field of the target.

[csharp]
public class ManualTarget : Aspect, ITargetSelector {
public List SelectTargets (IContainer game) {
var card = (container as Ability).card;
var target = card.GetAspect ();
var result = new List ();
result.Add (target.selected);
return result;
}
}
[/csharp]

Random Target Selector

Our next variant of a “target selector” will pick a target at random from the list of candidates based on a match to its Mark.

[csharp]
public class RandomTarget : Aspect, ITargetSelector {
public Mark mark;
public int count = 1;

public List SelectTargets (IContainer game) {
var result = new List ();
var system = game.GetAspect ();
var card = (container as Ability).card;
var marks = system.GetMarks (card, mark);
if (marks.Count == 0)
return result;
for (int i = 0; i < count; ++i) { result.Add (marks.Random ()); } return result; } } [/csharp]

The “Mark” field here is the same as we used with a Card’s Target aspect – it defines an alliance and zones to match against. I also provided a “count” field, such that the selector can pick one or more matching targets at random.

All Target Selector

Our final variant (for this lesson at least) will pick all of the candidates that match a given mark type.

[csharp]
public class AllTarget : Aspect, ITargetSelector {
public Mark mark;

public List SelectTargets (IContainer game) {
var result = new List ();
var system = game.GetAspect ();
var card = (container as Ability).card;
var marks = system.GetMarks (card, mark);
result.AddRange (marks);
return result;
}
}
[/csharp]

Ability Loader

The Ability class holds data relevant to constructing “Game Action” instances. It also can “configure” those instances using custom information. There are a few ways to extend a “Game Action” with the ability to be configured by the ability data. One option would be to add a configuration method to the base class itself, and then override it in each of the subclasses as needed. Another option, which I have chosen here, is to make the conformance “optional” by implementation of an interface wherever needed.

[csharp]
public interface IAbilityLoader {
void Load (IContainer game, Ability ability);
}
[/csharp]

Damage Action

Our first Game Action to provide support as an Ability Loader will be the Damage Action. First things first, update the class declaration so that it conforms to our “IAbilityLoader” interface.

[csharp]
public class DamageAction : GameAction, IAbilityLoader
[/csharp]

Because we will use the “Load” method to configure our action, we don’t need to provide any values in the constructor. Instead, we will instantiate the action with a default empty constructor.

[csharp]
public DamageAction() {

}
[/csharp]

Finally, implement the new interface with the following “Load” method:

[csharp]
public void Load (IContainer game, Ability ability) {
var targetSelector = ability.GetAspect ();
var cards = targetSelector.SelectTargets (game);
targets = new List ();
foreach (Card card in cards) {
var destructable = card as IDestructable;
if (destructable != null)
targets.Add (destructable);
}
amount = Convert.ToInt32 (ability.userInfo);
}
[/csharp]

Note that there is a special trick in getting a “target selector” by specifying the interface’s type as the generic type in the “GetAspect” call. It requires you to “add” the aspect using the same generic type. I will demonstrate this later. Otherwise, we would have had to loop through all of the aspects and cast them to the interface type until we found a match.

To sum up the “Load” of this action, we were able to determine the desired targets by the ability’s attached “target selector” aspect, and were able to determine the amount of damage based on the “userInfo” of the ability.

Draw Cards Action

For some additional variety, we will also make the “Draw Cards Action” conform to the ability loader. Just like with the “DamageAction”, we will need to update the class declaration to add the “IAbilityLoader” conformance. We will also add an empty constructor. Finally, we will implement the “Load” method as follows:

[csharp]
public void Load (IContainer game, Ability ability) {
player = game.GetMatch ().players [ability.card.ownerIndex];
amount = Convert.ToInt32 (ability.userInfo);
}
[/csharp]

Ability Action

Next we need “something” to take the “ability” of a card and “apply” it so that its effects actually impact the game. A combination of a new Game Action and System sounds like just the thing. Create a new “Ability Action” to hold the context of which Ability needs to trigger:

[csharp]
public class AbilityAction : GameAction {
public Ability ability;

public AbilityAction (Ability ability) {
this.ability = ability;
}
}
[/csharp]

Ability System

To handle the “Ability Action” we will add a new “Ability System”. As you might expect, it will be a type of “Aspect” which gets added to the game system container:

[csharp]
public class AbilitySystem : Aspect, IObserve {
public void Awake () {
this.AddObserver (OnPerformAbilityAction, Global.PerformNotification (), container);
}

public void Destroy () {
this.RemoveObserver (OnPerformAbilityAction, Global.PerformNotification (), container);
}

void OnPerformAbilityAction (object sender, object args) {
var action = args as AbilityAction;
var type = Type.GetType (action.ability.actionName);
var instance = Activator.CreateInstance (type) as GameAction;
var loader = instance as IAbilityLoader;
if (loader != null)
loader.Load (container, action.ability);
container.AddReaction (instance);
}
}
[/csharp]

The implementation of this action will instantiate a new Game Action based on the Ability’s “actionName” field. The system will then attempt to cast the new action as a type of “IAbilityLoader” and when successful, will configure the action using the “Load” method. The finished result gets appended to the Action System as a reaction of the Ability Action.

Cast Spell Action

Now that we have all of this “ability” foundation in place, we need a root “trigger” to begin it all. Everything so far was actually just building up to our ability to play spell cards, and have the casting of a spell apply the ability of the card.

[csharp]
public class CastSpellAction : GameAction {
public Spell spell;

public CastSpellAction (Spell spell) {
this.spell = spell;
}
}
[/csharp]

Spell System

To handle “casting a spell” we will add a new “spell system” as an aspect of the main game system container. This system will also be an observer, and will be listening for two different actions. First, we want to listen for the “Perform” phase of a “PlayCardAction”. In this handler, we will check whether or not the played card was a type of spell or not. If the card is a spell, we add a reaction of a “CastSpellAction”. We also handle the “CastSpellAction” notification, and play the ability on the card accordingly. In this case we observed the “Prepare” phase of the action, because I wouldn’t consider the casting of the spell to be complete until the ability has been applied.

[csharp]
public class SpellSystem : Aspect, IObserve {

public void Awake () {
this.AddObserver (OnPeformPlayCard, Global.PerformNotification (), container);
this.AddObserver (OnPrepareCastSpell, Global.PrepareNotification (), container);
}

public void Destroy () {
this.RemoveObserver (OnPeformPlayCard, Global.PerformNotification (), container);
this.RemoveObserver (OnPrepareCastSpell, Global.PrepareNotification (), container);
}

void OnPeformPlayCard (object sender, object args) {
var action = args as PlayCardAction;
var spell = action.card as Spell;
if (spell != null) {
var reaction = new CastSpellAction (spell);
container.AddReaction (reaction);
}
}

void OnPrepareCastSpell (object sender, object args) {
var action = args as CastSpellAction;
var ability = action.spell.GetAspect ();
var reaction = new AbilityAction (ability);
container.AddReaction (reaction);
}
}

[/csharp]

Note the extra level of indirection here: we play a card, cast a spell, and finally apply the card’s ability. Extra actions such as “casting a spell” are all opportunities to trigger other card abilities. Defining the moment as a separate action makes it easy to respond to “specifically”, because spells aren’t the only card that can use the other steps like “play a card” or “apply card ability”.

This first pass of the spell system is assuming only a single ability per spell card. It wouldn’t be hard to add support for multiple abilities by looping through the card’s aspects and trying to cast each as an Ability. Other considerations involve checking the intended trigger of a card’s ability. Perhaps a spell card could have an ability that triggers while you hold it in your hand instead of as you cast it. If I don’t get around to adding that feature, then I hope you will take it as a personal challenge to try on your own.

Minion System

For additional variety, and to show that the ability system can be reused on non-spell cards, let’s also do a quick update of the Minion System. We will simulate Hearthstone’s battlecry ability, so that minions with abilities will trigger their ability when they are summoned. This is really only a placeholder version, and ideally would include another level of indirection, such as by another action called “Battlecry”, just like we had another level of indirection on spells to actually “cast” the spell. Add the following method:

[csharp]
void PostSummonAbility (SummonMinionAction action) {
var card = action.minion;
var ability = card.GetAspect ();
if (ability != null)
container.AddReaction (new AbilityAction (ability));
}
[/csharp]

Then invoke it at the end of the “OnPerformSummonMinion” method:

[csharp]
PostSummonAbility (summon);
[/csharp]

Game Factory

We added two new systems. Let’s not forget to instantiate them and add them to the game system container! Add the following inside the “Create” method:

[csharp]
game.AddAspect ();
game.AddAspect ();
[/csharp]

More Refactoring

At some point I think I had it in my mind that the Action System would only be able to trigger from a “PlayerIdleState” game state. This caused me to change from my “PlayerInputState” back to “PlayerIdleState” instead of directly to a “SequenceState” even if the input had triggered a game action. By itself, that wasn’t much of an issue, but because we perform certain logic when entering the “PlayerIdleState” such as the CardSystem’s “AutoTarget” feature, I ended up with a logic bug. The user could perform manual target selection for a card in an input state, the flow would revert to the idle state, and the card system would auto-target thus replacing the manually selected target with a new random target. Whoops! It turns out that it is perfectly fine to transition from an input state to a sequence state, so in my input controllers, I check whether or not an action has begun before switching back to idle.

First, open the “ClickToPlayCardController” script and swap lines 118 and 119. We want the “ConfirmState” to perform the newly created action before changing state to the reset state.

[csharp]
owner.game.Perform (action);
owner.stateMachine.ChangeState ();
[/csharp]

Next, add a conditional check in the “ResetState” before changing the game state back to “Idle” such that we only change when the Action System is not active:

[csharp]
if (!owner.game.GetAspect ().IsActive)
owner.game.ChangeState ();
[/csharp]

Next, open the “DragToAttackController” script and do something similar. In the “CompleteState” we will move the line that transitions to the Idle state after the block that performs our attack action. Even then, we will only perform the transition if the action system is not active.

[csharp]
if (owner.attacker != null && owner.defender != null) {
var action = new AttackAction (owner.attacker, owner.defender);
owner.gameContainer.Perform (action);
}

if (!owner.gameContainer.GetAspect().IsActive)
owner.gameStateMachine.ChangeState ();
[/csharp]

Deck Factory

Our feature-set is growing more complex by the minute. In order to demonstrate the wide variety of possibilities, I hand crafted some new placeholder code which builds a deck inspired by the basic deck of a mage in Hearthstone. Apart from a minion’s race (like “Beast” or “Murloc”), “Charge”, “Polymorph”,and the “Aura” ability on the Raid Leader, I think we have a fully functioning equivalent here. Not bad!

There is a lot of code here, but I consider most of it to be temporary (read as “not that important”). I will probably use this factory in a finished version, but the internals should be driven by some external resource (like a json file) rather than be manually crafted in code. Essentially I have created 15 methods that each create a fully formed card. I add two copies of each to a deck and return the final result. There are a few special cases worth pointing out:

  • Card1 – See “Arcane Missiles” card in Hearthstone. Unique because it applies damage three times in a row, each to a potentially different target. This is achieved using our “RandomTarget” variant of the Target Selectors.
  • Card3 – See “Arcane Explosion” card in Hearthstone. Unique because it applies damage to an entire group of cards – every enemy card in the battlefield. This is achieved using our “AllTarget” variant of the Target Selectors.
  • Card7 – See “Arcane Intellect” card in Hearthstone. Uniqe because it uses a different ability effect – drawing cards. This does not require a Target Selector at all.
  • Card10 – See “Fireball” card in Hearthstone. Unique because it applies to a manually selected target. This is achieved using our “ManualTarget” variant of the Target Selectors.
  • Card14 – See “Nightblade” card in Hearthstone. Unique because it is an ability on a Minion whereas most of the other examples are on Spell cards.

Most of the code is pretty straight forward, but one important note exists for the Target Selectors. Note that I create them first, then add them as an Aspect to the ability using the generic type of “ITargetSelector” so that I can quickly grab it again later without needing to know the specific implementing type of Target Selector that had been added.

[csharp]
public static class DeckFactory {
public static List Create () {
List deck = new List ();
Func[] builder = new Func[] {
Card1, Card2, Card3, Card4, Card5,
Card6, Card7, Card8, Card9, Card10,
Card11, Card12, Card13, Card14, Card15
};
foreach (Func func in builder) {
deck.Add(func());
deck.Add(func());
}
return deck;
}

static Card Card1 () {
var card = CreateCard (“Shoots A Lot”, “3 damage to random enemies.”, 1);
var ability = AddAbility (card, typeof(DamageAction).Name, 1);
var targetSelector = new RandomTarget ();
targetSelector.mark = new Mark (Alliance.Enemy, Zones.Active);
targetSelector.count = 3;
ability.AddAspect (targetSelector);
return card;
}

static Card Card2 () {
return CreateMinion (“Grunt 1”, string.Empty, 1, 2, 1);
}

static Card Card3 () {
var card = CreateCard (“Wide Boom”, “1 damage to all enemy minions.”, 2);
var ability = AddAbility (card, typeof(DamageAction).Name, 1);
var targetSelector = new AllTarget ();
targetSelector.mark = new Mark (Alliance.Enemy, Zones.Battlefield);
ability.AddAspect (targetSelector);
return card;
}

static Card Card4 () {
return CreateMinion (“Grunt 2”, string.Empty, 2, 3, 2);
}

static Card Card5 () {
var card = CreateMinion (“Rich Grunt”, “Draw a card when summoned.”, 2, 1, 1);
AddAbility (card, typeof(DrawCardsAction).Name, 1);
return card;
}

static Card Card6 () {
return CreateMinion (“Grunt 3”, string.Empty, 2, 2, 3);
}

static Card Card7 () {
var card = CreateCard (“Card Lovin'”, “Draw 2 cards”, 3);
AddAbility (card, typeof(DrawCardsAction).Name, 2);
return card;
}

static Card Card8 () {
var card = CreateMinion (“Grunt 4”, “Taunt”, 3, 2, 2);
card.AddAspect ();
return card;
}

static Card Card9 () {
var card = CreateMinion (“Grunt 5”, “Taunt”, 3, 1, 3);
card.AddAspect ();
return card;
}

static Card Card10 () {
var card = CreateCard (“Focus Beam”, “6 damage”, 4);
var ability = AddAbility (card, typeof(DamageAction).Name, 6);
ability.AddAspect (new ManualTarget());
var target = card.AddAspect ();
target.allowed = new Mark (Alliance.Any, Zones.Active);
target.preferred = new Mark (Alliance.Enemy, Zones.Active);
return card;
}

static Card Card11 () {
return CreateMinion (“Grunt 6”, string.Empty, 4, 2, 7);
}

static Card Card12 () {
var card = CreateMinion (“Grunt 7”, “Taunt”, 5, 2, 7);
card.AddAspect ();
return card;
}

static Card Card13 () {
var card = CreateMinion (“Grunt 8”, “Taunt”, 4, 3, 5);
card.AddAspect ();
return card;
}

static Card Card14 () {
var card = CreateMinion (“Grunt 9”, “3 Damage to Opponent”, 5, 4, 4);
var ability = AddAbility (card, typeof(DamageAction).Name, 3);
var targetSelector = new AllTarget ();
targetSelector.mark = new Mark (Alliance.Enemy, Zones.Hero);
ability.AddAspect (targetSelector);
return card;
}

static Card Card15 () {
return CreateMinion (“Big Grunt”, string.Empty, 6, 6, 7);
}

static T CreateCard (string name, string text, int cost) where T : Card, new() {
var card = new T();
card.name = name;
card.text = text;
card.cost = cost;
return card;
}

static Minion CreateMinion (string name, string text, int cost, int attack, int hitPoints) {
var card = CreateCard (name, text, cost);
card.attack = attack;
card.hitPoints = card.maxHitPoints = hitPoints;
card.allowedAttacks = 1;
return card;
}

static Ability AddAbility (Card card, string actionName, object userInfo) {
var ability = card.AddAspect ();
ability.actionName = actionName;
ability.userInfo = userInfo;
return ability;
}
}
[/csharp]

Game View System

Now that we have added a more specialized Deck creation, we can remove the equivalent placeholder code from the Game View System. This means we can delete the entire “Temp_AddTargeting” method, and can also reduce the “Temp_SetupSinglePlayer” method like so:

[csharp]
void Temp_SetupSinglePlayer() {
var match = container.GetMatch ();
match.players [0].mode = ControlModes.Local;
match.players [1].mode = ControlModes.Computer;

foreach (Player p in match.players) {
var deck = DeckFactory.Create ();
foreach (Card card in deck) {
card.ownerIndex = p.index;
}
p [Zones.Deck].AddRange (deck);

var hero = new Hero ();
hero.hitPoints = hero.maxHitPoints = 30;
hero.allowedAttacks = 1;
hero.ownerIndex = p.index;
hero.zone = Zones.Hero;
p.hero.Add (hero);
}
}
[/csharp]

Demo

Go ahead and press “Play” to try out all of the new features. We have spells! We have different target selectors for abilities! We have different ability effects! We even have minions with abilities! Play a few games to help make sure you see it all. By the way, I even lost a couple of times – granted I was only playing for fun, but the new cards help the A.I. feel more challenging even if it isn’t actually any more intelligent.

I think the game is much more fun. It would definitely benefit from additional art, such as adding some “viewers” for the application of the various spell abilities. This would also help it be more obvious when the computer is playing a spell – it would be nice to know which spell is being played. It wouldn’t actually be hard to add a preview much like we do for summoning minions. Perhaps that would be a good exercise for my readers.

Summary

Wow, what a long lesson. At least the end result should be pretty satisfying! Ultimately this lesson’s purpose was to begin the implementation of spell cards. Spell cards by themselves don’t actually do anything though. In order to make the results more exciting we also implemented ability effects – what does the spell “do” when it is cast? We provided two different effects, the ability to cause damage, and to draw cards. The ability system was made to be reusable and was even applied to the summoning of minions. In addition, the abilities support a variety of target selection types, such as manual, random, and all target selection.

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!

7 thoughts on “Make a CCG – Spells & Abilities

  1. This ability system doesn’t seem to be very data driven and requires you to assemble cards using code instead of unity’s component system. Do you plan on making a sort of json parser which creates all the cards at runtime?

    1. Short answer, yes. Longer answer – to me this is still a prototype project and I am more interested in “playing” with ideas and seeing what I enjoy working with. That is why I am not forcing myself to adapt any kind of hard coded data patterns yet. The first prototype I made began from the opposite direction – starting from parsed JSON and restricting myself to patterns that were easier to model with the data. I like to do things and re-do things to get new perspectives. With that said, there is nothing I have done here that “requires” assembling cards with code – in the sense of the ability to automate it from an external resource like a database or JSON file. I am simply postponing that as long as possible, because I want to explore other architectures while feeling less constrained.

  2. Just had to say this at some point, thank you for your tutorial series.
    It fills the gaps that Unity learn leaves from an architectural standpoint.

  3. I’ve hit a bit of a roadblock, and I’m hoping to gather some insights.

    From what I can gather, it appears that the current implementation only allows for a single ability per card. When I attempted to add a second ability aspect, I encountered an error stating that the card’s container already contains an ability aspect. This raises the question: is it possible for a card to have more than one ability?

    In my design, I envision cards with multiple abilities, giving players the freedom to choose which ability to activate. However, I want to make sure I’m on the right track before I change things and dive into implementing a list of abilities for each card.

    Additionally, I’m curious about the differences in the implementation of abilities like “Taunt” and spell abilities. Could you provide some insights into these distinctions? I believe understanding these variations will help me better understand how I can approach this.

    Best regards,

    1. The aspect container can accept multiple aspects of the same type as long as they have unique keys. You can see more on that here:

      https://theliquidfire.com/2017/08/28/make-a-ccg-aspect-container-test-runner/

      The issue is that the design of cards has added abilities without a key, and therefore a key is automatically generated based on the name of the “type”. This was by design, because I can look for “any” ability by assuming it has been added the same way.

      To have multiple abilities, I might try something like creating a “Compound” Ability, which doesn’t do anything on its own, except to manage multiple other abilities. The abilities it adds could be on children containers that only it knows about. When it comes time to handle itself as an ability, it would need to route the behavior to its child abilities. This is of course easier said than done, because each ability has different requirements around how you select targets etc, so some may be easier to combine than others.

      The reason Taunt is treated differently than spell abilities is based on the game design. Taunt is sort of self-manageable, so basically a minion either has that flag or it doesn’t. When the game plays the minion, it can look for this flag and render the card on the board accordingly.

      Spells are far more varied, and may require you to specify a target, or may auto select a target. They could do just about anything, and so they require a lot more control which is provided thanks to the composition of its own aspect container.

Leave a Reply to jump Cancel reply

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