Make a CCG – Action System

In a game like Hearthstone, every action is significant and can serve as the trigger for a whole new sequence of actions. This includes obvious actions like attacking and casting spells, but also includes less obvious actions such as drawing cards and even changing turns! Although you may already be familiar with event-driven programming, you may not have thought about ways to sort event responders by custom criteria, or considered how to invoke the handlers over time so that you can also play animations in sequence. Let’s look at some architecture to support these ideas.

Hearthstone Triggered 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.

Action System Overview

These two new architectural challenges (sorting and async execution of our events) are going to be handled by a new system so that all actions will be handled in a similar way. We can implement the structure once and let it be reused by everything. The system itself is a little complex and was based on the documentation in the advanced rulebook.

Here are a few requirements as I understand them:

  1. All actions are performed as a sequence of phases which basically means that you will post an event before performing an action so that other cards can have an opportunity to react first, then you will perform the action itself and post another event to allow the opportunity for other cards to react afterward.
  2. All reactions occur based on the order of play of the responding cards.
  3. All reactions are resolved in a depth-first order (children nodes before sibling nodes).
  4. When the root action’s sequence is complete, other loops occur such as the death reaper event to take care of any mortally wounded cards.

To help illustrate what all of that actually means, I put together a possible game scenario. Look at the image below to get an idea of the board’s current configuration:

Sample Battle Setup

The enemy has played three cards to the battlefield: Acolyte of Pain, Backstreet Leper and Tentacle of N’Zoth. You have also played three cards to your own battlefield: Questing Adventurer, Violet Teacher, and Knife Juggler. It is your turn and you are playing Arcane Explosion from your hand. A whole series of triggered reactions will play out that will look something like the following node graph:

Demo Action Flow

  1. Play A Card – This represents the action of choosing a card in your hand to play. All a system may do here is post the action as some sort of event or notification, and then remove the card from the player’s hand. There will be two reactions to this event at nodes 2 and 3.
  2. Questing Adventurer – The text on this card reads, “Whenever you play a card, gain +1/+1”. Because of our action to “Play A Card” in step 1, this card now reacts by gaining an attack and health buff. Its ability will be played out even before the spell is cast, because it was already played to the table and therefore has an order of play with priority over the casting of the spell. There are no reactions to this event, so the next sibling event in the queue is able to proceed.
  3. Cast A Spell – A system will be in place to determine that the card which was just played is actually a kind of spell and therefore we add a new action to handle it. Each action will have an event or notification that it “Will” perform, then it will be performed and post another “Did” perform event. In this case, the Violet Teacher responds to the “Will” perform event at node 4, long before we actually apply the ability of the spell which is to “Deal 1 damage to all enemy minions” at node 7.
  4. Violet Teacher – The text on this card reads, “Whenever you cast a spell, summon a 1/1 Violet Apprentice”. Because we just cast a spell, this card now reacts by summoning a new minion.
  5. Knife Juggler – The text on this card reads, “After you summon a minion, deal 1 damage to a random enemy”. Because the Violet Teacher just summoned a Violet Apprentice, this card reacts by dealing 1 damage to a random enemy. Let’s imagine that the random enemy is the “Acolyte of Pain” which will lead to another reaction at node 6.
  6. Acolyte of Pain – The text on this card reads, “Whenever this minion takes damage, draw a card”. Because our Knife Juggler just dealt it some damage, the card will react by causing the opponent player to draw a card. This minion’s hit points will now be reduced to 2.
  7. Apply Spell Damage – Finally we can apply the ability of casting a spell which is to deal 1 damage to all enemy minions. The Acolyte of Pain will have yet another triggered reaction at node 8, and the other two enemy cards will both be put into a “mortally wounded” state.
  8. Acolyte of Pain – With its hit points reduced to 1, this card reacts a second time and the opponent is able to draw another card.

This completes the phases of the root action for playing a card, but the action system wont be done quite yet. At this point it will begin a loop for the death reaper. So long as there are reactions to this event, the loop will continue.

Death Reaper Flow 1

  1. Death Reaper – This action causes any minion in a “mortally wounded” status to be moved to the graveyard of its controlling player. There are two such minions which will react to this event at nodes 2 and 4.
  2. Backstreet Leper – The text on this card says, “Deathrattle: Deal 2 damage to the enemy hero”. So as we reap this card we will trigger this ability leading to another action at node 3.
  3. Damage Hero – Due to the deathrattle at node 2, our hero receives 2 damage.
  4. Tentacle of N’Zoth – The text on this card says, “Deathrattle: Deal 1 damage to all minions”. So as we reap this card we will trigger this ability leading to another action at node 5.
  5. Damage All – Due to the deathrattle at node 3, all active minions and heroes receive 1 damage. This causes the “Acolyte of Pain” and the newly summoned “Violet Apprentice” to become mortally wounded.

The first loop of the Death event phase has completed, but because there were reactions we now check again. Sure enough there are still two minions to reap. No additional actions causing death will occur, so the second loop of the death event phase is ended.

Death Reaper Flow 2

There are other loops that can also appear after the end of a root action such as for resolving card “Auras”, but I have not implemented this feature yet and will have to come back to it at some point in the future. Otherwise, hopefully you understand the general requirements of this new system. So now let’s see how we can implement it with code!

Notifications

I debated on whether or not to use my own notification system or native events on this project. I like my own system better for a variety of reasons, but also like for new readers to need to learn as little new code as possible before they can follow along. Ultimately Notifications won. For anyone not already familiar with this system, it is actually quite simple. Anything can post any notification, and anything can observe any notification, and can also specify to only listen to notifications posted by a particular object, or by any objects. Here are a couple of code samples to illustrate its usage:

// Required using statement - notifications are in their own namespace now
using TheLiquidFire.Notifications;

// Subscribe to a notification - such as on a MonoBehaviour OnEnable method
// Can optionally pass a third parameter to only listen to notifications posted by the specified object
this.AddObserver(OnDemoNotification, "Hello World");

// Unsubscribe from a notification - such as on a MonoBehaviour OnDisable method
this.RemoveObserver(OnDemoNotification, "Hello World");

// Post a notification - could occur anywhere and by anything
this.PostNotification("Hello World");

// Sample notification handler implementation
void OnDemoNotification (object sender, object args) {
	// Handle notification here
}

In this case, I will be using notifications as a means of posting when a game action “will” occur, as well as when it “did” occur. This is part of the “Phases” of an action that I described as a requirement earlier. One of the tricky things is that I wanted each and every action to post its own unique notification for each phase, but I also didn’t want to write nearly identicle notification code for every possible game action. This means that I wanted the base class to be able to create a unique notification for itself based on the subclass that was actually being used. That should be pretty easy using something like we did with our Aspect keys such as: this.GetType().Name;. However, that would only work for the posting of a notification, because at that point I would have an instance of the action sub class. I would still need a solution for other classes to register as observers of the notification and I would need to do this without the benefit on an instance of the class that would post the notification to observe. One of the other easy ways to do this is using generic methods, but the instance can’t use its own type as the parameter of a generic method. Unfortunately this means that I would need both implementations.

Although globals are generally considered a bad architectural habit, I still feel like there are cases where it makes sense to have some sort of global utility function. This is one such case. Since C# doesn’t support it though, I simply wrapped what I wanted in a static Global class as follows:

public static class Global {
	public static int GenerateID<T> () {
		return GenerateID (typeof(T));
	}

	public static int GenerateID (System.Type type) {
		return Animator.StringToHash (type.Name);
	}

	public static string PrepareNotification<T> () {
		return PrepareNotification (typeof(T));
	}

	public static string PrepareNotification (System.Type type) {
		return string.Format ("{0}.PrepareNotification", type.Name);
	}

	public static string PerformNotification<T> () {
		return PerformNotification (typeof(T));
	}

	public static string PerformNotification (System.Type type) {
		return string.Format ("{0}.PerformNotification", type.Name);
	}
}

The code here also demonstrates a way to generate an id based on a type. Note that this is not a Globally unique identififer. In other words, if you create two different attack “actions” both instances would share an identical id as generated by this code. The idea will be that when I post a notification for an action, I can refer to its types id instead of other options like comparing strings or attempts at casting to a sub-type, etc.

Note that I have also included a “GlobalTests” file in the Editor folder. Try implementing this from scratch on your own and see if you come up with the same unit tests or different ones.

Phase

During the overview of this system, I started out with a requirement that “All actions are performed as a sequence of phases…”. I already mentioned that I would be posting notifications as part of a phase, but there is more too it than that. Let’s take a look:

public class Phase {
	#region Fields
	public readonly GameAction owner;
	public readonly System.Action<IContainer> handler;
	public Func<IContainer, GameAction, IEnumerator> viewer;
	#endregion

	#region Constructor
	public Phase (GameAction owner, System.Action<IContainer> handler) {
		this.owner = owner;
		this.handler = handler;
	}
	#endregion

	#region Public
	public IEnumerator Flow (IContainer game) {
		bool hitKeyFrame = false;

		if (viewer != null) {
			var sequence = viewer (game, owner);
			while (sequence.MoveNext ()) {
				var isKeyFrame = (sequence.Current is bool) ? (bool)sequence.Current : false;
				if (isKeyFrame) {
					hitKeyFrame = true;
					handler (game);
				}
				yield return null;
			}
		}

		if (!hitKeyFrame) {
			handler (game);
		}
	}
	#endregion
}

Note that the first field is a reference to a “GameAction”. You can simply define an empty class by that name for now so your code will compile:

public class GameAction {

}

Next is a field named “handler”. This field is called a delegate and it can hold a reference to any method that accepts an “IContainer” as a parameter and which has a return type of “void”. If the “owner” action was to “Draw A Card”, then the handler here could cause the game model(s) to update appropriately – such as by moving a random card from the player’s deck to the players hand.

Next is a field named “viewer”. This is another type of delegate. It can hold a reference to any method that accepts both an “IContainer” and “GameAction” as parameters, and which has a return type of “IEnumerator”. If you are familiar with working with Coroutines in Unity then you should have an idea what this might be used for. Because I want my code to be testable I am not actually working directly with Coroutines, but I can use the same language features so that I can have a sort of async execution of my code. The viewer will be connected to some sort of MonoBehaviour / GameObject in Unity which handles the display of whatever is actually going on to the data behind the scenes. In the “Draw A Card” example, the viewer would animate a Canvas representing a Card from the deck to a position where you can see what you picked, and then finally show the card animating to the position on screen where the other cards in your hand are located.

The constructor for this class expects you to already know the instance of the action which is the “owner” of the phase, as well as the “handler” that should be executed. The “viewer” can be connected later (or not at all if we are merely unit testing our code) such as by observing a notification.

Finally we have the “Flow” method. Assuming we have some sort of Coroutine-like viewer, then this method will be able to “step” through the “frames”. Note that each “step” can hold its own “Current” value which could be anything. In this case I have used a convention where if you return a “true” that it indicates you wish to actually perform the logic in the handler at that time. This gives you complete control over when the model updates in respect to what you see happening on screen. For example, in an attack action, you may not want to actually reduce any hitpoints until the two combatants actually collide. Then you can trigger another animation that shows the damage but will know it wont start until the appropriate time.

In the event that we have played all the way through a viewer’s animation without performing the handler, or in a case where there wasn’t a viewer at all, I also have a backup at the end of the Flow method. If a key frame has not been hit, then we will still have a chance to perform the handler’s logic.

Game Action

Regardless of what specific action this new system is handling, we are going to need some sort of base “interface” (using the term loosely here because a base class still has its own interface without being a literal interface in the C# sense) to operate against. This base object will provide a minimum context for whatever is about to happen. Subclasses will provide specific context. For example, a subclass of a game action that changes turns could have a player index indicating which players turn is next.

public class GameAction {
	#region Fields & Properties
	public readonly int id;
	public Player player { get; set; }
	public int priority { get; set; }
	public int orderOfPlay { get; set; }
	public bool isCanceled { get; protected set; }
	public Phase prepare { get; protected set; }
	public Phase perform { get; protected set; }
	#endregion

	#region Constructor
	public GameAction() {
		id = Global.GenerateID (this.GetType ());
		prepare = new Phase (this, OnPrepareKeyFrame);
		perform = new Phase (this, OnPerformKeyFrame);
	}
	#endregion

	#region Public
	public virtual void Cancel () {
		isCanceled = true;
	}
	#endregion

	#region Protected
	protected virtual void OnPrepareKeyFrame (IContainer game) {
		var notificationName = Global.PrepareNotification (this.GetType ());
		game.PostNotification (notificationName, this);
	}

	protected virtual void OnPerformKeyFrame (IContainer game) {
		var notificationName = Global.PerformNotification (this.GetType ());
		game.PostNotification (notificationName, this);
	}
	#endregion
}

As usual, we first define any needed fields and properties. The very first of which is “id” – a “readonly” value that can only be set in a constructor, but which can be publicly read. In many cases, specific code will respond to specific events posted by notifications of the class directly, and so the id wont be needed. However, there will also be notifications that fire for all actions, regardless of their type, such as when a new sequence is beginning. Any code that needs to listen at this level can benefit from having a quick way to compare the id against any kind of id’s that are important to it.

The next few properties are both read and write, which is handy for instances where we won’t be able to assign the properties directly in a constructor. You will see why this can happen in the future. The “player” property represents the party responsible for causing an action to occur. Note that this could be the opponent of the player whose turn it actually is. Many actions will have a source “card” that is responsible for some sort of action, but not all of them (like changing a turn), and it is even possible to change the owner of a card, so this field could be a helpful back up.

There are two properties that are used in the sorting of GameActions. The “priority” is most important and is a way to override the default sorting in “orderOfPlay” – which generally is found by the reaction of a card that has been put into play. Only actions which are played as “reactions” will need these two properties.

Our remaining properties all have a “protected set” property. This means that to external classes the property will seem to be readonly. The class itself and subclasses can set the values though. For example, I have an “isCanceled” property, but I only want to provide a way for outside code to cause an action to become canceled, and not allow a way to un-cancel. Since the only way outside code can have any affect is via the “Cancel” method, that is what I have obtained.

The final two properties are for the “sequence of phases” idea that I mentioned earlier. I have a phase for “prepare” – the “will” do a thing, and a phase for “perform” – the “did” do a thing. Either one could have an animation tied to it. The “performing” of an action is probably obvious, such as an “attack” action causing one minion view to collide with another. The “preparing” of an action could help indicate that something is about to happen but might change, such as if an attacking minion lifted off the ground to indicate it was about to attack, but other actions could then take over such as switching the originally intended target, before the attack is actually carried out. Another potential use of the “prepare” viewer could be if an action wasn’t yet fully initiated. For example, you might play a minion with a battlecry that requires a target. You could then use the “prepare” phase of the battlecry to allow code to wait for the user to select the target of the battlecry action.

The handler for each phase is a protected method that should be called at the “key frame” of a viewer’s animation. Then we will post a dynamic notification built using the type of the action and our Global class. This way no matter what kind of action it is, “something” could specifically be registered as an observer and respond in any number of ways.

Action System

The final piece of the puzzle will be our very first system – yay! The idea here is that systems provide all of the functionality for models. The idea is comparable to a “system” in Entity Component System (ECS) or “controller” in Model View Controller (MVC) architectures if you are familiar with either approach. In this case, all Game Actions should be performed via this system so that we can expect a consistent pattern to the flow and resolution of events. Since this class is a bit longer, I will break it down into pieces while discussing it:

public class ActionSystem : Aspect {
	// TODO: add additional code here
}

It is important to note that this class inherits from “Aspect” – this will allow us to connect our system to a “Container” so that any system can easily grab a reference as needed. For example, a “Turn System” may exist on the same container and need a way to perform the “Change A Turn Action”, and so would grab this system from the same container to perform the action.

public const string beginSequenceNotification = "ActionSystem.beginSequenceNotification";
public const string endSequenceNotification = "ActionSystem.endSequenceNotification";
public const string deathReaperNotification = "ActionSystem.deathReaperNotification";
public const string completeNotification = "ActionSystem.completeNotification";

Next I have declared several constants to use with notifications. This makes it easy for other systems to register as observers without me needing to retype the notification name – I wouldn’t want to risk a typo as that can be a frustrating bug to track down.

GameAction rootAction;
IEnumerator rootSequence;
List<GameAction> openReactions;
public bool IsActive { get { return rootSequence != null; }}

I have a few fields and one property. The “rootAction” will be the action that is submitted to be performed when nothing else was currently playing out. It is important to keep a reference to because certain phases of the flow (like the death reaper) should only occur in relation to this action and not the reactions that play out along the way. The rootSequence is the “flow” around the rootAction and must be saved separately as it is a sort of bookmark that allows everything to play out over time. Sub-flows for an action’s reactions will automatically be “captured” within their own contexts and wont need any special care.

As phases play out, notifications will be sent. Before the notification is posted, a new “openReactions” list will be generated. Any code responding to the notification, such as to register a new reaction to the action, can be appended to this list and then sorted and performed later.

So long as the rootSequence is not null, the “IsActive” property will be true. You should not attempt to use the system to perform new actions during this time as they will simply be ignored. Reactions however can still be added.

public void Perform (GameAction action) {
	if (IsActive) return;
	rootAction = action;
	rootSequence = Sequence (action);
}

When the game is in an idle state, a player will be able to initiate the execution of a new action such as playing a card or ending his turn. This will be done using this public “Perform” method. Note that if an action is already in progress that calling this method will do nothing because it will simply return early.

public void Update () {
	if (rootSequence == null)
		return;

	if (rootSequence.MoveNext () == false) {
		rootAction = null;
		rootSequence = null;
		openReactions = null;
		this.PostNotification (completeNotification);
	}
}

The update method is what allows the action to play out over time. Although you can manually call it in your own loops from unit tests, the game itself will probably call it from the Update loop of a MonoBehaviour script. If there is no rootSequence the method will simply abort early. Otherwise it will step throug the “frames” of the flow by calling the “MoveNext” method of the “IEnumerator” sequence. When there is nothing further to perform (detected by “MoveNext” of “false”) then we know we have played the sequence to its completion and can now clear out the fields we had stored and post a completion notification.

public void AddReaction (GameAction action) {
	if (openReactions != null)
		openReactions.Add (action);
}

The “AddReaction” method is what will allow the observers of our action notifications to treat the action as a trigger for a new event. Note that this should only be used directly within an appropriate notification handler such as an actions “prepare” or “perform” keyframe notification, or from a system event notification like the “deathReaperNotification”. Note that the “beginSequenceNotification”, “endSequenceNotification”, and “completeNotification” notifications are NOT appropriate places to add triggered reactions. Attempts to add a reaction at unsupported times could lead to crashes or other unexpected consequences.

IEnumerator Sequence (GameAction action) {
	this.PostNotification (beginSequenceNotification, action);

	var phase = MainPhase (action.prepare);
	while (phase.MoveNext ()) { yield return null; }

	phase = MainPhase (action.perform);
	while (phase.MoveNext ()) { yield return null; }

	if (rootAction == action) {
		phase = EventPhase (deathReaperNotification, action, true);
		while (phase.MoveNext ()) { yield return null; }
	}

	this.PostNotification (endSequenceNotification, action);
}

The Sequence method creates the IEnumerator which serves as the flow for our action. It encapsulates the various phases I have mentioned repeatedly such as the preparation and performance phase. If the action is also the rootAction, then the death event phase can also play out. It also provides notifications for the beginning and ending of a whole sequence, which might be useful for attaching “viewers” to phases or making sure that game state is not “idle” etc.

IEnumerator MainPhase (Phase phase) {
	if (phase.owner.isCanceled)
		yield break;

	var reactions = openReactions = new List<GameAction> ();
	var flow = phase.Flow (container);
	while (flow.MoveNext ()) { yield return null; }

	flow = ReactPhase (reactions);
	while (flow.MoveNext ()) { yield return null; }
}

A “Main” Phase is a sort of sub-flow within the main sequence that only executes if the Game Action has not been cancelled. Valid actions will then use their Phase object to perform its flow, which should also trigger the phase handler, which is an opportunity for other actions to “react”. You can see here that the openReactions was re-created so that responders to the event could attach themselves. The local reference is captured automatically in case there end up being multiple nested flows. When the flow from the phase completes, another phase for the triggered reactions takes over.

IEnumerator ReactPhase (List<GameAction> reactions) {
	reactions.Sort (SortActions);
	foreach (GameAction reaction in reactions) {
		IEnumerator subFlow = Sequence (reaction);
		while (subFlow.MoveNext ()) {
			yield return null;
		}
	}
}

The “React” Phase is where we create a new sequences for each of the actions that occurred as a triggered reaction to an earlier action. Before looping through each, I make sure to sort the list of reactions.

IEnumerator EventPhase (string notification, GameAction action, bool repeats = false) {
	List<GameAction> reactions;
	do {
		reactions = openReactions = new List<GameAction> ();
		this.PostNotification(notification, action);

		var phase = ReactPhase (reactions);
		while (phase.MoveNext ()) { yield return null; }
	} while (repeats == true && reactions.Count > 0);
}

An “Event” phase is something that can occur following a root action. Currently I only have support for a “Death” event which will ultimately clean the battlefield of any mortally wounded minions. In the future I could add additional events for new features such as an “Aura” event which makes sure that card abilities due to auras can be updated appropriately. Event phases may need to loop, as is the case for a Death event, because the death of a minion could lead to actions that cause the deaths of additional minions after the fact.

int SortActions (GameAction x, GameAction y) {
	if (x.priority != y.priority) {
		return y.priority.CompareTo(x.priority);
	} else {
		return x.orderOfPlay.CompareTo(y.orderOfPlay);
	}
}

Finally I have provided a SortActions method which is responsible for the sorting of a list of Game Actions. The final result will order them first by “priority” in descending order (higher priority goes first), and then by “orderOfPlay” in ascending order (lower values mean they were played first and so should respond first).

There is also a “ActionSystemTests” script provided for this as well. Feel free to check out the code there if you like as it also includes a sample system that responds to the various events, adds reaction actions, etc.

Summary

In this lesson we created our first system, the Action System. This system coordinates the flow and resolution of events related to the execution of any sort of Game Action. It is potentially one of the most important systems in the entire project as nearly every other system will need to use it. Because of its recursive nature (methods that end up calling themselves again) it might be a little hard to understand how everything works. Be sure to pay close attention to the code here as it is a valuable pattern to master. If necessary, try adding breakpoints and stepping through the code so you can see how it all works together.

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!

2 thoughts on “Make a CCG – Action System

Leave a Reply

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