One of the most powerful features of the Action system is the way it can chain together actions with reactions into a long sequence of events. At the moment we have only implemented a single action – changing turns, but in a game like Hearthstone, this action will always have at least one reaction. When a player starts their turn, they are supposed to draw a card. Let’s go ahead and implement the draw a card feature as a reaction to the action for changing turns.
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.
Draw Cards Action
All reactions are actually just actions that are played as a triggered event instead of being played manually by a user interaction. This means that I will create it as a subclass of GameAction just like we did before with the ChangeTurnAction. The purpose of the action is to serve as a context for the system that will apply it. What information is necessary? In this case, we will need to know which player is performing the action and how many cards should be drawn. After the action is performed, it would also be nice if the action could indicate which cards were drawn, so that a view somewhere else could display the drawn cards correctly.
[csharp]
public class DrawCardsAction : GameAction {
public int amount;
public List
public DrawCardsAction(Player player, int amount) {
this.player = player;
this.amount = amount;
}
}
[/csharp]
In the code above, I used a simple “amount” field to hold the target amount of cards that a player is “supposed” to draw. I then have a List of “cards” to hold the result – whatever was successfully drawn. Note that the count of drawn “cards” may not match the target “amount” desired, for example if you tried to draw a card, but your deck was already empty.
Player System
Next we will create a system to handle the application of the Game Action onto the Game model itself. You could choose to organize this in a variety of ways. I decided to put it in a system for the Player, because when I describe the action itself, I would describe it as a “Player draws a card”, and so if the Player is the one performing the action, then the Player system makes the most sense as the location it occurs. Should any one system become too long (such as more than a few 100 lines of code) I would of course reconsider the location of each bit of code and potentially create additional systems with smaller focus.
[csharp]
public class PlayerSystem : Aspect, IObserve {
// … Add Code Here
}
[/csharp]
The Player System class inherits from Aspect. You should know by now that this superclass allows the system to be attached to the same container object as all of our other systems so that they can work together if needed. We also implement the IObserve interface, which will allow it to register and unregister for notifications at appropriate times.
[csharp]
public void Awake () {
this.AddObserver (OnPerformChangeTurn, Global.PerformNotification
this.AddObserver (OnPerformDrawCards, Global.PerformNotification
}
public void Destroy () {
this.RemoveObserver (OnPerformChangeTurn, Global.PerformNotification
this.RemoveObserver (OnPerformDrawCards, Global.PerformNotification
}
[/csharp]
In this case, the notifications will be used initially to listen for the ChangeTurnAction which it will use as the trigger to initiate its own DrawCardsAction. It will also listen for the performance of its own action in order to actually apply the logic at the correct time.
[csharp]
void OnPerformChangeTurn (object sender, object args) {
var action = args as ChangeTurnAction;
var match = container.GetAspect
var player = match.players [action.targetPlayerIndex];
DrawCards (player, 1);
}
[/csharp]
In the notification handler for the performance of changing turns, we figure out which player is becoming the active player and pass it along to another method that handles creating the actual action context, using the correct player, and a fixed number of cards to draw based on changing turns.
[csharp]
void DrawCards (Player player, int amount) {
var action = new DrawCardsAction (player, amount);
container.AddReaction (action);
}
[/csharp]
The “DrawCards” method was separated on its own because there are likely to be many “triggers” for actually drawing a card(s) and this will allow me to keep my code a little more DRY. The parameters we will need to create the DrawCardsAction are passed directly to the method. Once the action is created, it is also automatically added as a reaction to the action system via the extension method on the container.
[csharp]
void OnPerformDrawCards (object sender, object args) {
var action = args as DrawCardsAction;
action.cards = action.player [Zones.Deck].Draw (action.amount);
action.player [Zones.Hand].AddRange (action.cards);
}
[/csharp]
Finally we have the notification handler for actually applying the logic of drawing a card(s). I determine the number of cards to draw based on the action’s context. Then I use another extension method on a List to handle randomly taking elements from a collection (I will show the code for this next). Note that I assign them to the action itself so that views and/or reactions can know “what” cards were taken. Next, we add the drawn cards to the player’s “hand”.
There are at least two additional points that will need to be considered here in the future. First, what happens when you successfully draw a card but your hand is full? It could be that the card is “destroyed” – moved to the discard pile. The other issue is to consider what happens when you try to draw a card(s) and did not have enough left in your deck to draw? It could be that the player takes some sort of penalty, such as fatigue damage. In both cases, these should be considered addional reactions to the intended action of drawing a card.
List Extensions
In the Player System, you may have wondered how I was using a “Draw” method on the List class. I did this by adding a new extension in my pre-existing “Common/Extensions/ListExtensions” class. The methods follow:
[csharp]
public static T Draw
if (list.Count == 0)
return default(T);
int index = UnityEngine.Random.Range (0, list.Count);
var result = list [index];
list.RemoveAt (index);
return result;
}
public static List There are two overloaded implementations of the Draw method. The first does not accept a “count” parameter, assuming that you only want to draw one card. It can be convenient because the result does not need to be wrapped by another object (a List). The second version does take a “count” of cards to try to take. The final results are returned in a List – which could be empty if there were no cards to draw. This allows the call to be safe in that you don’t need to worry about out of bounds errors on the collection you are drawing from. One interesting note about these methods: because the items are drawn at random from the entire collection, the collection itself never needs to be shuffled. This is an on-going truth. For example, you might have imagined needing to shuffle a deck both at the beginning of a game as well as if a game action caused cards to be added to the deck during gameplay. In either case, by using the “Draw” method, each card has the same chance of being picked. Later on for demonstration purposes, I will name the cards, in order, so that it is more evident that random cards can be drawn while no shuffling is necessary. Don’t forget – because we added a new system, we need to add it to the factory in order for it to be included as part of the container. [csharp] In the scene hierarchy, I have added a component marking where the concept of a board would appear. In this case I decided to add a reference at this level to one of my reusable scripts called a SetPooler. A pooler is something I created to aid in the reuse of expensive objects (GameObjects) rather than needing to constantly destroy and re-create them. Without using a pooler, a battle could easily instantiate many cards, but if you use a pooler, you can limit that number because cards that have been discarded can be reused to display newer cards in the future. All I need to add to this script is a new field: [csharp] I then created a new child GameObject in the scene (in edit mode) called the “Card Pooler” that had a SetPooler component attached. I used the “Card View” prefab as the reference assigned to this pooler. All of the other settings can be left at default, although you may wish to pre-populate it with a few instances – I set mine at 10. Finally I manually connect the BoardView’s “cardPooler” reference to the component instance just created. Because I have a visual reference to the concept of a player’s deck, I also want to be able to visually approximate how many cards remain in the deck. In other words, as a player draws cards, the width of the deck should slowly shrink until no cards remain. To handle this, I created a method called “ShowDeckSize” that expects a normalized value (0-1) indicating how much of the deck should be visible: [csharp] I also added a bunch of functionality to the view for displaying the cards themselves. For example, I added a reference to the Card model that needs to be displayed. When drawing cards, I want to support the ability to see both the back and front of the card. While a card is on the deck, it should be face-down, and I should only see it as face-up, if it is a card I am drawing. When my opponent draws a card, I should not be able to see it until he plays it. [csharp] void Awake () { public void Flip (bool shouldShow) { void Toggle (GameObject[] elements, bool isActive) { Next, we need to add the code that observes the DrawCardsAction notification and presents the results to our users. Note that this could have been placed just about anywhere, such as in the BoardView, or PlayerView component scripts. Adding it to the PlayerView might be the most intuitive since it is the PlayerSystem that performs the logic. However, since there are two PlayerView instances in a scene then we would need multiple listeners and would need to add extra code to “ignore” the action where the player didn’t match. The BoardView might have been another good choice, because it could listen to the notification one time for all players, and then just trigger the matching player to take over. I sort of liked that idea as well, but imagined that the BoardView may end up responsible for far too many tasks. In the end I decided to simply add a new component specific to this action. I created a new script in the Components folder called “DrawCardsView”. I also attached this new component to the same GameObject that the BoardView is attached to, so that I could easily get a reference to the board and its children player views. [csharp] void OnDisable () { void OnPrepareDrawCards (object sender, object args) { IEnumerator DrawCardsViewer (IContainer game, GameAction action) { for (int i = 0; i < drawAction.cards.Count; ++i) {
int deckSize = action.player[Zones.Deck].Count + drawAction.cards.Count - (i + 1);
playerView.deck.ShowDeckSize ((float)deckSize / (float)Player.maxDeck);
var cardView = boardView.cardPooler.Dequeue ().GetComponent var showPreview = action.player.mode == ControlModes.Local; Because this is a MonoBehaviour, I can simply use the “OnEnable” and “OnDisable” methods to add and remove notification listeners. In this case I am observing the “prepare” phase of the DrawCardsAction as an opportunity to attach a “viewer” to the “perform” phase of the same action. In the viewer method itself I make the very first statement a return statement with a value of “true” – which causes the “perform” key frame to trigger. This means that the Player System would apply the logic, and the DrawCardsAction should have its “cards” field updated so we know which cards have successfully been drawn. Next, I can cache some references such as getting the correct PlayerView which matches the player who is actually drawing cards. I then loop over the number of cards that need to be drawn. Within each loop I determine how many cards are left in the deck and scale the deck view appropriately. Then I use my SetPooler to “Dequeue” a new card view instance (automatically creating new objects if necessary). I parent the view instance to the GameObject in the scene that represents the location for the player’s hand, but I set its world position and rotation to match the top of the player’s deck. You can think of this as the first keyframe in a tween so that we can animate the card from the deck to our hand. However, the animation needs to be different depending on whether or not it is the local player or the opponent that is drawing the card. Furthermore, there are several actions that can cause a card to need to be put in a players hand besides drawing a card from a deck. Therefore, I implemented the rest of this viewer’s animation in another script called the HandView. In the Hand View, I created a public method “AddCard” which accepts a transform reference of a GameObject (that should also have a CardView component attached). It also takes a parameter called “Show Preview” that indicates whether or not the card should animate straight into the player’s hand, or if it should take a small detour so that the player can see what was drawn. [csharp] cards.Add (card); I created a “ShowPreview” method to handle the display of a drawn card to a user before sliding a card into place among the other cards in the hand. It Tweens the card view from “wherever” it currently is (in our case it will be located at the deck), and animates it moving to the same position as another GameObject that appears in the scene hierarchy – we have cached a reference to it called the “activeHandle”. While a card is between the deck and the display location, we will be checking its rotation. Whenever we determine that the card is physically face-up based on the rotation angle, we tell the CardView component to update itself appropriatly. After reaching the activeHandle position, the card remains still at that location for a second to give a user plenty of time to see it. [csharp] Finally, I have added a “Layoutcards” method which adjusts the position of all of the cards in the hand so that they can make room for the newly drawn card. [csharp] Tweener tweener = null; var position = inactiveHandle.position + new Vector3 (xPos, 0, 0); while (tweener != null) There is one final step to do before trying everything out. At the moment, we haven’t actually created a deck of cards for either player. We need some sort of placeholder data for now, and I want it to have random values for all of the card fields like mana cost, attack and health, etc so that we can see whether or not the views update properly. In addition, I want to name each card in order so that we can see that cards are drawn as if the deck was shuffled, even though we didn’t need to shuffle it. Open up the GameViewSystem script and update the Temp_SetupSinglePlayer method as follows: [csharp] foreach (Player p in match.players) { At this point we have successfully added everything necessary to implement drawing a card, both for the model behind the scenes and for the view to display it to the user. Save your scene and project and then press “Play” for the “Game” scene. When you press the “End Turn” button you should see the opponent draw a card before it is your turn again. When your turn begins you will also draw a card, but the animation should be different – you should see a preview of your card before it lands face up in your own hand. You wont be able to change turns again until the entire action sequence has played out at which point the game state will be idle again. You can keep drawing cards as long as you like, and should notice the size of the deck shrink over time. Eventually the deck will be depleted and the view for the deck will disappear from the screen. You can continue changing turns, but no additional cards will be drawn. In this lesson, we created our first action-sequence, by causing a new action to occur as a result of another action. Now, whenever a turn is changed, a player will draw a card. We implemented everything necessary on the models, systems and views to make a complete and playable demo. 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!
int resultCount = Mathf.Min (count, list.Count);
List
for (int i = 0; i < resultCount; ++i) {
T item = list.Draw ();
result.Add (item);
}
return result;
}
[/csharp]
Game Factory
game.AddAspect
[/csharp]Board View
public SetPooler cardPooler;
[/csharp]Deck View
public void ShowDeckSize (float size) {
squisher.localScale = Mathf.Approximately (size, 0) ? Vector3.zero : new Vector3 (1, size, 1);
}
[/csharp]Card View
public bool isFaceUp { get; private set; }
public Card card;
private GameObject[] faceUpElements;
private GameObject[] faceDownElements;
faceUpElements = new GameObject[] {
cardFront.gameObject,
healthText.gameObject,
attackText.gameObject,
manaText.gameObject,
titleText.gameObject,
cardText.gameObject
};
faceDownElements = new GameObject[] {
cardBack.gameObject
};
Flip (isFaceUp);
}
isFaceUp = shouldShow;
var show = shouldShow ? faceUpElements : faceDownElements;
var hide = shouldShow ? faceDownElements : faceUpElements;
Toggle (show, true);
Toggle (hide, false);
Refresh ();
}
for (int i = 0; i < elements.Length; ++i) {
elements [i].SetActive (isActive);
}
}
void Refresh () {
if (isFaceUp == false)
return;
manaText.text = card.cost.ToString ();
titleText.text = card.name;
cardText.text = card.text;
var minion = card as Minion;
if (minion != null) {
attackText.text = minion.attack.ToString ();
healthText.text = minion.maxHitPoints.ToString ();
} else {
attackText.text = string.Empty;
healthText.text = string.Empty;
}
}
[/csharp]
Draw Cards View
public class DrawCardsView : MonoBehaviour {
void OnEnable () {
this.AddObserver (OnPrepareDrawCards, Global.PrepareNotification
}
this.RemoveObserver (OnPrepareDrawCards, Global.PrepareNotification
}
var action = args as DrawCardsAction;
action.perform.viewer = DrawCardsViewer;
}
yield return true; // perform the action logic so that we know what cards have been drawn
var drawAction = action as DrawCardsAction;
var boardView = GetComponent
var playerView = boardView.playerViews [drawAction.player.index];
cardView.card = drawAction.cards [i];
cardView.transform.ResetParent (playerView.hand.transform);
cardView.transform.position = playerView.deck.topCard.position;
cardView.transform.rotation = playerView.deck.topCard.rotation;
cardView.gameObject.SetActive (true);
var addCard = playerView.hand.AddCard (cardView.transform, showPreview);
while (addCard.MoveNext ())
yield return null;
}
}
}
[/csharp]Hand View
public IEnumerator AddCard (Transform card, bool showPreview) {
if (showPreview) {
var preview = ShowPreview (card);
while (preview.MoveNext ())
yield return null;
}
var layout = LayoutCards ();
while (layout.MoveNext ())
yield return null;
}
[/csharp]
IEnumerator ShowPreview (Transform card) {
Tweener tweener = null;
card.RotateTo (activeHandle.rotation);
tweener = card.MoveTo (activeHandle.position, Tweener.DefaultDuration, EasingEquations.EaseOutBack);
var cardView = card.GetComponent
while (tweener != null) {
if (!cardView.isFaceUp) {
var toCard = (Camera.main.transform.position – card.position).normalized;
if (Vector3.Dot (card.up, toCard) > 0)
cardView.Flip (true);
}
yield return null;
}
tweener = card.Wait (1);
while (tweener != null)
yield return null;
}
[/csharp]
IEnumerator LayoutCards (bool animated = true) {
var overlap = 0.2f;
var width = cards.Count * overlap;
var xPos = -(width / 2f);
var duration = animated ? 0.25f : 0;
for (int i = 0; i < cards.Count; ++i) {
var canvas = cards [i].GetComponentInChildren
cards [i].RotateTo (inactiveHandle.rotation, duration);
tweener = cards [i].MoveTo (position, duration);
xPos += overlap;
}
yield return null;
}
[/csharp]Game View System
void Temp_SetupSinglePlayer() {
var match = container.GetMatch ();
match.players [0].mode = ControlModes.Local;
match.players [1].mode = ControlModes.Computer;
for (int i = 0; i < Player.maxDeck; ++i) {
var card = new Minion ();
card.name = "Card " + i.ToString();
card.cost = Random.Range (1, 10);
card.maxHitPoints = card.hitPoints = Random.Range (1, card.cost);
card.attack = card.cost - card.hitPoints;
p [Zones.Deck].Add (card);
}
}
}
[/csharp]
Demo
Summary
Hi John, amazing tutorial so far. I’m struggling to understand the loop of this archtecture, do you have a link or maybe another post on the blog that explains the workflow for the Game Systems, Actions and notifications? I kinda understand the notification system, but after rereading the code it’s getting harder to follow up with so many systems working simultaneously. Cheers from Brazil!
Much of the game systems in this tutorial are unique to this game, due to the complexity of its design. If there is something particular you are struggling with, feel free to ask here or on my forums and I will try to elaborate. I have talked about actions and notifications in other blog posts such as this mini 3 part series where I originally created the notification center:
http://theliquidfire.com/2014/12/10/social-scripting-part-3/
I have also used similar architecture patterns in other tutorial project series. If something doesn’t click in one, maybe it would help to try another such as the Tactics RPG. Good luck!