Make a CCG – Playing Cards

You can take turns drawing cards from your deck and putting them in your hand… but at the moment all that leads to is a hand full of cards. Sounds like we need a way to start playing cards from our hand toward some other purpose. In this lesson we will provide the ability to play a card, and also enable the local player to control which of the cards they wish to play by interacting with what is presented on screen.

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.

Play Card Action

Let’s begin by defining a new game action to provide the necessary context for a player to play a card from his hand. All we really need is the reference to the card to play, because we can determine the owning player from the fields on the card itself:

public class PlayCardAction : GameAction {
	public Card card;

	public PlayCardAction(Card card) {
		this.card = card;
	}
}

Player System

We will handle the application of our new action in the player system, but before that, I want to make some quick changes to the existing code. Currently we have a few actions that cause the zone of a card to change. For example, when drawing a card, we are moving a card from the “deck” to the “hand”. However, all I have implemented is changing the collection that the card is contained by, but I have not remembered to update the reference on the card itself. Because I don’t want to have to remember this step each time a zone changes, I created a reusable method for that purpose:

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;
}

The “ChangeZone” method requires a “card” which will be moving, and the “zone” that you wish to move it to. As an optional parameter you can specify a new player to become the owner of the card. In most cases you won’t need to change the owning player, so it is left as an optional parameter.

In the body of the method, I determine the original owner of the card based on the card’s “ownerIndex” field used as an index into the match’s list of players. The original owner is the “fromPlayer”. If the “toPlayer” is null, then I use the null-coalescing operator to reassign it as the same player as the player which is already in control of it. Now I can use the card’s “zone” field and the “fromPlayer” reference, to be able to remove the card from any previous collection it had been a part of. I use the “toPlayer” reference plus the “zone” parameter that is passed as the target zone for the method to add the card to the new collection it is supposed to be contained by, and finally I also update the fields of the card itself.

We will use the new method in two places. First we update the end of the “OnPerformDrawCards”. Remove the line of code that says this:

action.player [Zones.Hand].AddRange (action.cards);

and replace it with this:

foreach (Card card in action.cards)
	ChangeZone (card, Zones.Hand);

The second place to change is at the end of the “OnPerformOverDraw” method. Remove the line of code that says this:

action.player [Zones.Graveyard].AddRange (action.cards);

and replace it with this:

foreach (Card card in action.cards)
	ChangeZone (card, Zones.Graveyard);

Now as long as I remember to always use this method when moving a card between zones I shouldn’t need to worry about anything getting out of sync, such as a card appearing in more than one collection, or having the card’s field not match the collection that actually holds it.

With that out of the way, let’s move on to the implementation for our new action. First we will add some code to handle registration of the notification:

// Add to OnEnable
this.AddObserver (OnPerformPlayCard, Global.PerformNotification<PlayCardAction> (), container);

// Add to OnDisable
this.RemoveObserver (OnPerformPlayCard, Global.PerformNotification<PlayCardAction> (), container);

And then we will add a handler method for the action:

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

All we do here is to take the played card and move it to the graveyard, not terribly exciting I know, but there will be other reactions to this action in the future. The graveyard will be the correct resting place for some cards that have been played, though other cards like minions and secrets will need to remain in play. We can add code to handle those sorts of exceptions later.

Hand View

I will use the hand view to serve as the “viewer” for the new play card action, although it is just a temporary implementation as the more specific systems like spawning minions or casting spells will ultimately need to take over the animations for us. Let’s begin by handling the registration of the notification:

void OnEnable () {
	this.AddObserver (OnPreparePlayCard, Global.PrepareNotification<PlayCardAction> ());
}

void OnDisable () {
	this.RemoveObserver (OnPreparePlayCard, Global.PrepareNotification<PlayCardAction> ());
}

As we have done multiple times before, I registered for the “prepare” phase’s notification of the action as an opportunity to easily assign a viewer to the “perform” phase of the same action.

void OnPreparePlayCard (object sender, object args) {
	var action = args as PlayCardAction;
	if (GetComponentInParent<PlayerView>().player.index == action.card.ownerIndex)
		action.perform.viewer = PlayCardViewer;
}

IEnumerator PlayCardViewer (IContainer game, GameAction action) {
	var playAction = action as PlayCardAction;
	CardView cardView = null;
	foreach (Transform t in cards) {
		var cv = t.GetComponent<CardView> ();
		if (cv.card == playAction.card) {
			cardView = cv;
			break;
		}
	}
	cards.Remove (cardView.transform);
	StartCoroutine (LayoutCards (true));
	var discard = OverdrawCard (cardView.transform);
	while (discard.MoveNext ())
		yield return null;
}

Because there are two hand views (one for the ally and one for the enemy) I need to make sure that only the correct one attempts to provide the viewer implementation. In the handler method, I use the “GetComponentInParent” method to get the “PlayerView” component from an object that will exist higher in the Game Object hierarchy. I can then compare the card’s owning player against the player assigned to that view component and see if they match or not. If it is a match, we proceed with assigning the viewer.

The “PlayCardViewer” method loops through all of the card views contained by the hand until it finds the one used to represent the played card from the action. We then remove that view from the hand’s list, call the “LayoutCards” method so that the remaining cards in the hand occupy the appropriate amount of space and positions, and then re-use the “OverdrawCard” method to finish up. That method causes the card to appear to be discarded by shrinking out of sight, and then it resets the card and returns it to the pooler for reuse later.

While we are here, I also decided to make a few of the private methods public so that we could use them from other locations as well. Make the “ShowPreview” and “LayoutCards” methods “public”. With this change, I also decided that the “wait” code in “ShowPreview” should actually be moved to the “AddCard” method since it is more relevant as part of that flow and doesn’t need to be used when making previews from other flows. Remove this code from “ShowPreview”:

tweener = card.Wait (1);
while (tweener != null)
		yield return null;

And add this code to the “AddCard” method, after the “preview” has played out:

var tweener = card.Wait (1);
while (tweener != null)
	yield return null;

Draw Cards View

Now that we are re-using cards from our own hand, the cards that are dequeued for drawing may have been left as “face up”. We need to add a new line to the “DrawCardsViewer” after dequeueing our card to make sure it is “face down” as we would expect a card in our deck to be:

cardView.Flip (false);

Game View System

I also noticed that I never assigned the “ownerIndex” field to our cards when we make our decks. Even though this is just “temp” code, we need to remember to handle it. In the “Temp_SetupSinglePlayer” in the inner nested “for loop”, add this line of code:

card.ownerIndex = p.index;

Player Input State

The first prototype I created was completely automated – the computer essentially played itself. This time I am actually adding code to allow the local player to determine the actions that are taken. In order to accomodate this, I have provided another game state called the “PlayerInputState” to serve as a placeholder for while the player has started interacting with the views but has not triggered a game action yet either.

public class PlayerInputState : BaseState {

}

This state can potentially become active for a variety of user input flows, even ones that are never intended to create a game action. For example, a player might click an opponent’s minion card that had been played to the table, because he wants to read the description text of the card. Previewing the card isn’t considered a “game” action, because it wont change any of the game data, although the flow itself can be an important input flow to help the user understand the full state of the game.

Clickable

A simple means of input is by “clicking” (with a mouse – or “tap” if you are on mobile). There might be lots of things we will want to click on, such as cards (to select them) or the background (to deselect), etc. That means we could benefit by a reusable component:

public class Clickable : MonoBehaviour, IPointerClickHandler {
	public const string ClickedNotification = "Clickable.ClickedNotification";

	public void OnPointerClick(PointerEventData eventData) {
		this.PostNotification (ClickedNotification, eventData);
	}
}

Here I have created a simple script that inherits from “MonoBehaviour” so I can attach it to any GameObject in my scene, and which implements the “IPointerClickHandler” interface so that it can respond to the Unity event systems. This will work on canvas objects as well as normal objects with colliders. In the handler method I post a notification, so that multiple systems can easily respond as necessary.

Modify your “Card View” prefab (the project asset) by adding our new “Clickable” component to it. Make sure to save the project so that the prefab will save correctly.

I also created a new object in the scene hierarchy called “Table” which has the “Clickable” component as well as a “Box Collider” component that is large enough to cover the entire view space of the camera, and which sits behind all of the other game objects. I used a “Size” of (x: 50, y: 0.1, z: 50) on the collider itself. Remember to also save the scene.

Click To Play Card Controller

Now we need a “controller” script that handles “Playing A Card” based on “Clicking” a card. It’s a bit of a mouthfull, but the name “ClickToPlayCardController” was the best name I could come up with at the moment. I felt that the name should be specific becaue in the future it might be nice to for it to be able to live alongside additional input flows of playing a card, such as by drag and drop.

I decided that this controller (and its flow with its own states) lives outside of the container that the other game systems occupy. This is because I consider the “view” side of things to be separate and very replacable and customizeable depending on the target platform and intended visuals etc. It is only once a player has committed to an actual game action that the two will overlap.

The basic flow for this input type is to use a mouse click (or tap on a mobile screen) on a card from your hand to see a preview of it, then click it again to “play” it, or click off of the card to cancel out. There’s a lot of code, so let’s break it down into smaller snippets:

public class ClickToPlayCardController : MonoBehaviour {
	// Add code here...
}

The controller is a MonoBehaviour script. This allows me to attach it to an object in the scene’s hierarchy. Go ahead and attach it to the “Ally Player View” in your scene.

IContainer game;
Container container;
StateMachine stateMachine;
CardView activeCardView;

Next I have added a few private fields. I will want to cache a reference to the “game” which is the main container that the other systems are aspects of. I also created a new private “container” which can hold a state machine. This will be a state machine specific to the flow of clicking to play cards. Finally I have a reference to the “activeCardView” which will be the card that the local player has clicked on when initiating this flow.

void Awake () {
	game = GetComponentInParent<GameViewSystem> ().container;
	container = new Container ();
	stateMachine = container.AddAspect<StateMachine> ();
	container.AddAspect (new WaitingForInputState ()).owner = this;
	container.AddAspect (new ShowPreviewState ()).owner = this;
	container.AddAspect (new ConfirmOrCancelState ()).owner = this;
	container.AddAspect (new CancellingState ()).owner = this;
	container.AddAspect (new ConfirmState ()).owner = this;
	container.AddAspect (new ResetState ()).owner = this;
	stateMachine.ChangeState<WaitingForInputState> ();
}

I use the “Awake” method to create and/or cache references as necessary. The “game” is just a cached reference from the “GameViewSystem”, while the “container” is a newly instantiated object. The “stateMachine” is added to our new “container”, and I also manually create the states that will be used by the state machine, so that I can also assign refernces of itself to each state. Finally, I set the default state of the state machine to be the “WaitingForInputState” which basically means it is inactive.

You might wonder why I would bother assigning any references of the controller to the states since all of the fields are “private”. Normally a state as a separate object can’t operate on something else that has private fields, but there are a few exceptions. In this case, the classes that define these new states are defined inside of the “ClickToPlayCardController” class. Because of “where” they are defined, they have access even to the private fields.

void OnEnable () {
	this.AddObserver (OnClickNotification, Clickable.ClickedNotification);
}

void OnDisable () {
	this.RemoveObserver (OnClickNotification, Clickable.ClickedNotification);
}

void OnClickNotification (object sender, object args) {
	var handler = stateMachine.currentState as IClickableHandler;
	if (handler != null)
		handler.OnClickNotification (sender, args);
}

The trigger that ultimately begins this flow will occur as the result of the observation of a “Clickable” notification. The handler method simply passes along the job of determining what to do with the click, based on what the current state of the flow is. Some states can safely ignore input (such as while animating) and so they wont implement the “IClickableHandler”.

private interface IClickableHandler {
	void OnClickNotification (object sender, object args);
}

Keep in mind that even though I am showing new interface definitions and will show new class definitions in a moment, that these are still placed inside of the “ClickToPlayCardController”. It is a fully self-contained mini system. The “IClickableHandler” will be implemented by the various states of this flow if they want to do something with “clicks” on objects.

private abstract class BaseControllerState : BaseState {
	public ClickToPlayCardController owner;
}

All of the states will inherit from this new base state which holds a reference to an instance of a “ClickToPlayCardController” which I call the state’s “owner”.

private class WaitingForInputState : BaseControllerState, IClickableHandler {
	public void OnClickNotification (object sender, object args) {
		var gameStateMachine = owner.game.GetAspect<StateMachine> ();
		if (!(gameStateMachine.currentState is PlayerIdleState))
			return;
		
		var clickable = sender as Clickable;
		var cardView = clickable.GetComponent<CardView> ();
		if (cardView == null || 
			cardView.card.zone != Zones.Hand || 
			cardView.card.ownerIndex != owner.game.GetMatch ().currentPlayerIndex)
			return;
		
		gameStateMachine.ChangeState<PlayerInputState> ();
		owner.activeCardView = cardView;
		owner.stateMachine.ChangeState<ShowPreviewState> ();
	}
}

Our first actual state, “WaitingForInputState”, is also the default state for our state machine. It implements the “IClickableHandler” so it will receive notification calls as long as it is the “current” state. Whan handling this notification, it will verify that the main game’s state machine is currently in the “PlayerIdleState”, if not it will simply abort early.

Next it casts the sender of the notification as a “Clickable” component and then attempts to get a “CardView” component from it. If the “CardView” component is missing, or if the card it represents is not in the player’s hand, or if the card’s owner is not the current player, then we also need to abort.

Assuming we have made it through all of the requirements, we cause the main game’s state machine to transition to a new state called the “PlayerInputState” so that other game actions won’t cause any conflicts or unexpected outcomes in our code. We keep a reference to the card view that had been clicked. Finally, we change our local state machine’s state to be a “ShowPreviewState”.

private class ShowPreviewState : BaseControllerState {
	public override void Enter () {
		base.Enter ();
		owner.StartCoroutine (ShowProcess ());
	}

	IEnumerator ShowProcess () {
		var handView = owner.activeCardView.GetComponentInParent<HandView> ();
		yield return owner.StartCoroutine (handView.ShowPreview (owner.activeCardView.transform));
		owner.stateMachine.ChangeState<ConfirmOrCancelState> ();
	}
}

The purpose of the “ShowPreviewState” is to cause the card to enlarge and center on screen to make it easy for a player to see its details. It is an intermediate step that helps make sure a player clicked on the correct card and can read all the card details to make an informed play decision. When the state enters, we simply start a coroutine that handles the animation from the cards location in the hand, to the preview location. Once the animation completes, we move onto our next local state “ConfirmOrCancelState”.

private class ConfirmOrCancelState : BaseControllerState, IClickableHandler {
	public void OnClickNotification (object sender, object args) {
		var cardView = (sender as Clickable).GetComponent<CardView> ();
		if (owner.activeCardView == cardView)
			owner.stateMachine.ChangeState<ConfirmState> (); 
		else
			owner.stateMachine.ChangeState<CancellingState> ();
	}
}

The “ConfirmOrCancelState” takes over while the clicked card sits in the preview location. From this point a player can either click the card a second time to “confirm” the play action, or click somewhere off of the card to “cancel” the play action.

private class CancellingState : BaseControllerState {
	public override void Enter () {
		base.Enter ();
		owner.StartCoroutine (HideProcess ());
	}

	IEnumerator HideProcess () {
		var handView = owner.activeCardView.GetComponentInParent<HandView> ();
		yield return owner.StartCoroutine (handView.LayoutCards (true));
		owner.stateMachine.ChangeState<ResetState> ();
	}
}

If the player has clicked something other than the original card, we enter the “CancellingState” which is pretty much the opposite of the “ShowPreviewState” because we are simply animating the card back into the hand. At the completion of the animation, we will need to reset things to the way they were before this whole flow began.

private class ConfirmState : BaseControllerState {
	public override void Enter () {
		base.Enter ();
		var action = new PlayCardAction (owner.activeCardView.card);
		owner.stateMachine.ChangeState<ResetState> ();
		owner.game.Perform (action);
	}
}

If the player has clicked the same card again while it is in the preview position, then we enter the confirm state, where we actually create the “PlayCardAction” and have the game perform it. I reset the flow immediately before having the game perform the play action.

private class ResetState : BaseControllerState {
	public override void Enter () {
		base.Enter ();
		owner.stateMachine.ChangeState<WaitingForInputState> ();
		owner.game.ChangeState<PlayerIdleState> ();
	}
}

Here is the implementation for the “ResetState” which will be called at the end of the flow, regardless of if the path chosen was to confirm or cancel the play card action. We set the local state machine back to its default state, “WaitingForInputState”, and we set the game state back to “PlayerIdleState”, although if the confirm state was chosen as part of the flow it will immediately be changing again anyway.

Demo

If you haven’t already, now might be a good time to go ahead and reset the “Player” constants (for maxHand and maxDeck) back to their original values. This way you can experiment with a greater variety of combinations for drawing and playing cards. For example, you may want to draw a card and immediately play it. You may also want to merely preview a card without playing. Draw multiple times (over multiple turns) so that you have a hand full of cards. Verify that you can click specific cards to play (not just the last one drawn). You might want to also try playing more than one card in a single turn.

As a quick tip, I should point out that the card canvas is clickable even in the transparent areas. Because of the way that I created the prefab with the overhanging stats (mana, health, etc) you might think you are clicking a card behind a card when you are actually clicking the transparent area of a card sitting above it. Clicking the mana crystal of a card you want to play can help make sure you got the right card. You might also want to spread out the cards in the hand a bit more – feel free to adjust the “overlap” value in the HandView’s LayoutCards method, you might try something like ‘0.4’ to make them much easier to select.

Summary

In this lesson, we added one of our simplest actions yet – merely playing a card did nothing more than move a card from your hand to your discard pile. Apart from clicking a button to change turns, most of our actions up to this point have been automated actions or reactions to actions. This time around we needed to also provide a fairly complex flow of user interactions that could allow a user to preview and then cancel or confirm a potential game action.

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 *