Make a CCG – Summon Minions

Now that we can play cards, let’s add a reason to do so. Our player’s deck is currently filled with nothing but minions, so you might be able to guess what this lesson is going to be all about. After playing a card that happens to be a minion, we will “summon” the minion. Instead of going straight to the graveyard, it will go onto the player’s battlefield, ready for combat.

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.

Summon Minion Action

The action for this lesson is the action of summoning a minion. The context we need to provide is just as simple for playing a card – we must provide the minion to summon. Other details such as the owning player can be determined from the fields on the minion itself.

public class SummonMinionAction : GameAction {
	public Minion minion;

	public SummonMinionAction(Minion minion) {
		this.minion = minion;
	}
}

Minion System

I imagine that there will be several actions to take regarding minions, so rather than continue to clutter up a different system, we will go ahead and make something new. To begin with, the system will have two purposes: determining “when” to summon a minion, and “how” to implement the action when the time comes.

public class MinionSystem : Aspect, IObserve {

	public void Awake () {
		this.AddObserver (OnPreparePlayCard, Global.PrepareNotification<PlayCardAction> (), container);
		this.AddObserver (OnPerformSummonMinion, Global.PerformNotification<SummonMinionAction> (), container);
	}

	public void Destroy () {
		this.RemoveObserver (OnPreparePlayCard, Global.PrepareNotification<PlayCardAction> (), container);
		this.RemoveObserver (OnPerformSummonMinion, Global.PerformNotification<SummonMinionAction> (), container);
	}

	void OnPreparePlayCard (object sender, object args) {
		var action = args as PlayCardAction;
		var minion = action.card as Minion;
		if (minion != null) {
			var summon = new SummonMinionAction (minion);
			container.AddReaction (summon);
		}
	}

	void OnPerformSummonMinion (object sender, object args) {
		var cardSystem = container.GetAspect<CardSystem> ();
		var summon = args as SummonMinionAction;
		cardSystem.ChangeZone (summon.minion, Zones.Battlefield);
	}
}

Note that the system is an “Aspect” so it is going to be attached to the same “Container” as our other game systems. It also implements “IObserve” so that I can easily manage notifications. The first two methods, “Awake”, and “Destroy” are required by the interface.

We observe the prepare phase of the “Play Card Action” as an opportunity to react with a “Summon Minion Action”. This means that the actual “Summon” will be performed before the “Play” completes. I think of it like this – part of “playing” a minion includes “summoning” it, so until you have summoned it, you have not completed playing it. Furthermore, if for some reason you couldn’t summon the minion, then you shouldn’t be able to play the card either.

We observe the perform phase of the “Summon Minion Action” as an opportunity to implement the action itself. In this case, the only requirement is that we change the zone of the card from the player’s hand, to the player’s battlefield. The “CardSystem” and its method to “ChangeZone” are shown next.

Card System

I already had a method to change the zone of a card in the “Player System”, though the method itself was private. I could have simply made the method public, but I also decided it might be a good idea to split it into its own system, so that all of my systems can be more organized and relevant to a single entity.

public class CardSystem : Aspect {
	public void ChangeZone (Card card, Zones zone, Player toPlayer = null) {
		var fromPlayer = container.GetMatch ().players [card.ownerIndex];
		toPlayer = toPlayer ?? fromPlayer;
		fromPlayer [card.zone].Remove (card);
		toPlayer [zone].Add (card);
		card.zone = zone;
		card.ownerIndex = toPlayer.index;
	}
}

This system has been implemented as an “Aspect”, so it will also be included on the same container as our other game systems. The “ChangeZone” method was copied from the “PlayerSystem” but was made public scope.

Player System

Now that we have added a new “Card System” with a duplicated “Change Zone” method, we should refactor the “Player System” to use it instead of its own private version. You don’t want to have copies of the same logic in different places, otherwise, you might find that the two implementations diverge over time which can lead to hard to find bugs.

void ChangeZone (Card card, Zones zone, Player toPlayer = null) {
	var cardSystem = container.GetAspect<CardSystem> ();
	cardSystem.ChangeZone (card, zone, toPlayer);
}

This updated version of the “Change Zone” method is now just a wrapper that forwards the request to the “Card System” instead of handling the logic itself.

While we are here, we will need one extra update. Currently, the flow of playing a card looks like this:

  1. Prepare Play Card Action
  2. Prepare Summon Minion Action
  3. Perform Summon Minion Action
  4. Perform Play Card Action

But if you follow that sequence, the performed summon action will change a card’s zone to the battlefield, and a performed play action will reassign the zone to the graveyard, effectively un-doing the only real goal of the summon action. Therefore we need to add a check to the play action’s implementation to see if the card’s zone has been changed or not. We will only move a card to the “Graveyard” if it is still held in the “Hand” like so:

void OnPerformPlayCard (object sender, object args) {
	var action = args as PlayCardAction;
	if (action.card.zone == Zones.Hand)
		ChangeZone (action.card, Zones.Graveyard);
}

Game Factory

We have added two new systems this lesson, so we need to remember to add them to the container. We of course do this in the Game Factory’s “Create” method. Add the following where the other systems are added:

game.AddAspect<CardSystem> ();
game.AddAspect<MinionSystem> ();

Board View

We have completed all of the back-end side of things, so now let’s look at how to display all of this to a user. One of the first things we need is a way to show a summoned minion. We already have a prefab in the project called “Minion View” which we will use for this purpose. However, I don’t actually want to just continually instantiate and destroy them as needed. I would much rather use an object pooler just like I do with the normal cards. In order to provide easy access to this new pooler, let’s expose it as a field on the Board View component:

public SetPooler minionPooler;

In the scene, I created a Game Object called “Minion Pooler” which I set as a child of the “Board” Game Object. I attached a “Set Pooler” component, and configured it so that the “Prefab” field was using the “Minion View” prefab project asset. The remaining fields can be left at their default values. Make sure to save the scene.

Minion View

Now that we are using the Minion View, we should start providing a basic implementation. We were already able to see the attack and health stat of a normal card, so at a minimum, we will want to make sure the new minion view shows the same stats on the summoned card.

public Minion minion { get; private set; }

public void Display (Minion minion) {
	this.minion = minion;
	Refresh ();
}

void Refresh () {
	if (minion == null)
		return;
	avatar.sprite = inactive;
	attack.text = minion.attack.ToString();
	health.text = minion.hitPoints.ToString();
}

We provided a public property called “minion” so we can know which model is driving the view. The property has a private setter so that you must use the “Display” method for assignment. The method can then trigger a “Refresh” while tracking the new model.

The “Refresh” method makes sure we have actually passed a model, and if so, will update the sprite and labels based on the state of the model. Don’t worry about the different sprites such as active vs inactive (and the taunt variation of each), we will implement those in a future lesson when we have systems in place to support them.

Hand View

Remember that the Hand View currently implements a viewer for the perform phase of playing a card. However, the perform phase of the play a card action wont come until after a minion has been summoned. We don’t want to leave the card right in front of the screen or the animation of summoning a minion will be obscured, so we will need to make a few changes.

First, let’s provide a convenient method so that we can ask the hand to give us the card view that is used to represent any given card:

public CardView GetView (Card card) {
	foreach (Transform t in cards) {
		var cardView = t.GetComponent<CardView> ();
		if (cardView.card == card) {
			return cardView;
		}
	}
	return null;
}

Hopefully the code here will look pretty familiar, because it was mostly copied from inside of the “PlayCardViewer” method. Remember that we don’t want to repeat code, so in that method, you should delete the following lines:

CardView cardView = null;
foreach (Transform t in cards) {
	var cv = t.GetComponent<CardView> ();
	if (cv.card == playAction.card) {
		cardView = cv;
		break;
	}
}

… and replace them with this:

CardView cardView = GetView (playAction.card);
if (cardView == null)
	yield break;

Note that we now cover a scenario where no matching card is found. This will allow other actions to have already removed the card before this method runs, while making sure that it doesn’t crash the program due to a null reference.

Since we have provided a means of getting a card’s view for a model, now we can handle removing that view from the hand. This includes resetting the card and making sure to handle pooling.

public void Dismiss (CardView card) {
	cards.Remove (card.transform);

	card.gameObject.SetActive (false);
	card.transform.localScale = Vector3.one;

	var poolable = card.GetComponent<Poolable> ();
	var pooler = GetComponentInParent<BoardView> ().cardPooler;
	pooler.Enqueue (poolable);
}

Again, this code should look familiar, because most of it was copied from the “OverdrawCard” method. We wont want duplicate code there either, so replace it with the following version:

IEnumerator OverdrawCard (Transform card) {
	Tweener tweener = card.ScaleTo (Vector3.zero, 0.5f, EasingEquations.EaseInBack);
	while (tweener != null)
		yield return null;
	Dismiss (card.GetComponent<CardView> ());
}

Table View

Our final piece to implement is adding code to animate the summon minion action so that a user can see it happen. We already had a “TableView” component, and in this project the idea of the player’s “table” and the “battlefield” are synonymous. Since summoning a minion places a minion in the battlefield, I felt like this component would be the ideal location for this code.

The existing code already had a field with a reference to a “minionViewPrefab”, but that was a shortcut I used in my early prototype and should be removed. We should be using the minion pooler instead. We will cache a reference to the pooler in the “Awake” method for convenience like so:

SetPooler minionPooler;

void Awake () {
	var board = GetComponentInParent<BoardView> ();
	minionPooler = board.minionPooler;
}

To trigger the desired animation, we will need to observe some notifications. Use the prepare phase notification so that we can attach a “viewer” to the perform phase. Because this is a MonoBehaviour, we will use the OnEnable and OnDisable to register and unregister the notification observers.

void OnEnable () {
	this.AddObserver (OnPrepareSummon, Global.PrepareNotification<SummonMinionAction> ());
}

void OnDisable () {
	this.RemoveObserver (OnPrepareSummon, Global.PrepareNotification<SummonMinionAction> ());
}

void OnPrepareSummon (object sender, object args) {
	var action = args as SummonMinionAction;
	if (GetComponentInParent<PlayerView>().player.index == action.minion.ownerIndex)
		action.perform.viewer = SummonMinion;
}

The viewer method itself is slightly long:

public IEnumerator SummonMinion (IContainer game, GameAction action) {
	var summon = action as SummonMinionAction;
	var playerView = GetComponentInParent<PlayerView> ();
	var cardView = playerView.hand.GetView (summon.minion);
	playerView.hand.Dismiss (cardView);
	StartCoroutine(playerView.hand.LayoutCards (true));

	var minionView = minionPooler.Dequeue ().GetComponent<MinionView> ();
	minionView.transform.ResetParent (transform);
	minions.Add (minionView);
	minionView.gameObject.SetActive (true);

	minionView.Display (summon.minion);
	var pos = GetComponentInParent<PlayerView> ().hand.activeHandle.position;
	minionView.transform.position = pos;

	var tweener = LayoutMinions();
	tweener.duration = 0.5f;
	tweener.equation = EasingEquations.EaseOutBounce;
	while (tweener != null)
		yield return null;
}

The general method signature is something you have seen multiple times by now. Inside the method body, we cast the action to the subclass “SummonMinionAction” to allow us to get the more specific context we need. Based on the object hierarchy in the scene, we can go up to find a player view (GetComponentInParent), then use that to get an easy reference to a sibling object – the hand view. We will use the new methods we added there to “Dismiss” the card that is being played to summon a new minion. This will make sure we can actually see the animation for summoning. In addition, we need to make the hand perform a “LayoutCards” so that the remaining cards in the hand use the correct positions and spacing, with no gaps, etc.

Next we need to use the minion pooler to get access to a new view that can display our summoned minion. The “Dequeue” method will reuse one if it is available, or create one as needed. In order to keep the scene hiearchy organized, I then parent the new object to the table. I also add a reference of the component to our list of “minions” – which is a List of the views we use to display the minions on the battlefield. Because the pooler returns objects in an inactive state, we also need to activate it.

In the third “paragraph” of code statements (little groupings of code separated by new lines), we tell the new “MinionView” component to “Display” the minion that is being summoned so that its stats will show up correctly. Then we move it to the Hand’s “activeHandle” position in world space as sort of a set-up before we tween it to a new position on the battlefield. The active handle position is the place where the card appears for a preview.

In the last group of statements we create a tweener to represent the “Layout” of our minions. Normally they are centered, so they all adjust to make room for the new minion. I actually use several tweeners, but the method merely returns the last one created – which will also happen to be the one on the minion we are actually summoning. For this minion, we modify the duration to be slightly longer than the minor adjustment time needed for the other minions. We also specified a nice little bounce curve so it looks like we drop the minion onto the field.

Tweener LayoutMinions () {
	var xPos = (minions.Count / -2f) + 0.5f;
	Tweener tweener = null;
	for (int i = 0; i < minions.Count; ++i) {
		tweener = minions [i].transform.MoveToLocal (new Vector3 (xPos, 0, 0), 0.25f);
		xPos += 1;
	}
	return tweener;
}

This code shows how we move each minion view to a space that evenly distributes them across the table. Note that we only return the tweener for the last minion in the list.

Demo

Go ahead and save your scene and then run it. Once you play a card, you should see it get summoned to the table. The stats that had been on the card before will be the same stats that appear on the minion. You can summon more than one minion on a single turn if you wish.

Note that we haven’t added any restrictions to playing cards yet, such as mana cost, or zone capacity – those will be dealt with in future posts. If you try to summon more than seven cards, you will quickly see that the board is not designed to hold them all as they will be overlapped by other screen elements.

Summary

We’re getting closer and closer to something “fun” all the time. Now playing a card actually serves a purpose and we are able to summon a minion to the battlefield. There is still a long way to go, such as handling minion abilities, battle-cries, and managing combat, not to mention that we also need to make sure the computer opponent can play cards as well. Even still, it is exciting to see the whole project come together one bite-sized piece at a time.

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 *