In the previous lesson I hard-coded a demo deck of cards. This wasn’t “necessary” because of my architectural choices. It was merely a simple placeholder which didn’t require me to commit to any kind of data store or structure. Still, to help avoid any confusion, I decided I would go ahead and provide an example post that shows how the same deck could have been created with some sort of asset – in this case a JSON file.

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.

JSON

If you aren’t familiar with JSON, you can learn about its structure and purpose here. While writing your own code it can be handy to use validator resources, such as the one here, in order to make sure that your syntax is correct.

Unity has its own JSON parsing via the JsonUtility. There are some nice features, although it isn’t as flexible as I would like due to a lack of support for Dictionaries. There are a number of alternate options available online as well as on Unity’s asset store. I have used a free library called MiniJSON for a long time now – you can find a copy here. Go ahead and download the script and add it to your project.

Demo Cards

Next I used a simple text editor to make a JSON resource that could be used to define a collection of cards, such as the ones found in my Deck Factory from the previous lesson. I saved the file in the “Resources” folder as “DemoCards.txt”. Note that the folder you put the asset in is important, and must match my setup or the code wont work correctly.

{ "cards": [{ "id": "Card1", "type": "Spell", "name": "Shoots A Lot", "text": "3 damage to random enemies.", "cost": 1, "abilities": [{ "action": "DamageAction", "info": 1, "targetSelector": { "type": "RandomTarget", "mark": { "alliance": "Enemy", "zone": "Active" }, "count": 3 } }] }, { "id": "Card2", "type": "Minion", "name": "Grunt 1", "text": "", "cost": 1, "attack": 2, "hit points": 1 }, { "id": "Card3", "type": "Spell", "name": "Wide Boom", "text": "1 damage to all enemy minions.", "cost": 2, "abilities": [{ "action": "DamageAction", "info": 1, "targetSelector": { "type": "AllTarget", "mark": { "alliance": "Enemy", "zone": "Battlefield" } } }] }, { "id": "Card4", "type": "Minion", "name": "Grunt 2", "text": "", "cost": 2, "attack": 3, "hit points": 2 }, { "id": "Card5", "type": "Minion", "name": "Rich Grunt", "text": "Draw a card when summoned.", "cost": 2, "attack": 1, "hit points": 1, "abilities": [{ "action": "DrawCardsAction", "info": 1 }] }, { "id": "Card6", "type": "Minion", "name": "Grunt 3", "text": "", "cost": 2, "attack": 2, "hit points": 3 }, { "id": "Card7", "type": "Spell", "name": "Card Lovin", "text": "Draw 2 cards", "cost": 3, "abilities": [{ "action": "DrawCardsAction", "info": 2 }] }, { "id": "Card8", "type": "Minion", "name": "Grunt 4", "text": "Taunt", "cost": 3, "attack": 2, "hit points": 2, "taunt": {} }, { "id": "Card9", "type": "Minion", "name": "Grunt 5", "text": "Taunt", "cost": 3, "attack": 1, "hit points": 3, "taunt": {} }, { "id": "Card10", "type": "Spell", "name": "Focus Beam", "text": "6 damage", "cost": 4, "target": { "allowed": { "alliance": "Any", "zone": "Active" }, "preferred": { "alliance": "Enemy", "zone": "Active" } }, "abilities": [{ "action": "DamageAction", "info": 6, "targetSelector": { "type": "ManualTarget" } }] }, { "id": "Card11", "type": "Minion", "name": "Grunt 6", "text": "", "cost": 4, "attack": 2, "hit points": 7 }, { "id": "Card12", "type": "Minion", "name": "Grunt 7", "text": "Taunt", "cost": 5, "attack": 2, "hit points": 7, "taunt": {} }, { "id": "Card13", "type": "Minion", "name": "Grunt 8", "text": "Taunt", "cost": 4, "attack": 3, "hit points": 5, "taunt": {} }, { "id": "Card14", "type": "Minion", "name": "Grunt 9", "text": "3 Damage to Opponent", "cost": 5, "attack": 4, "hit points": 4, "abilities": [{ "action": "DamageAction", "info": 3, "targetSelector": { "type": "AllTarget", "mark": { "alliance": "Enemy", "zone": "Hero" } } }] }, { "id": "Card15", "type": "Minion", "name": "Big Grunt", "text": "", "cost": 6, "attack": 6, "hit points": 7 } ] }

Note that each card is its own dictionary in the array and that each will have a different collection of key value pairs. For example, a Spell card will not include keys for “attack” and “hit points”, but a Minion card would need them both. Any card “could” have a “target” aspect, and any ability “could” have a target selector. The dictionary allows this to be a unique structure for each card holding exactly the data needed and nothing else.

Demo Deck

Like with the Demo Cards, I also created a sample showing how a deck resource could be used to put together a specific group of cards from our card collection. All I needed this time was a JSON file holding an array of card id’s. This might be used to define the cards used by a boss, or could be created by a user who wishes to persist a collection of his own themed decks.

{ "deck" : [ "Card1", "Card1", "Card2", "Card2", "Card3", "Card3", "Card4", "Card4", "Card5", "Card5", "Card6", "Card6", "Card7", "Card7", "Card8", "Card8", "Card9", "Card9", "Card10", "Card10", "Card11", "Card11", "Card12", "Card12", "Card13", "Card13", "Card14", "Card14", "Card15", "Card15" ] }

Note that I gave my cards ids based on the functions that created them from the previous lesson. The id’s could be anything, such as a database id, a globablly unique id, a custom convention created for your needs, etc.

Card

Now we need to start implementing code that can read our JSON resource and turn it back into an object instance in our game. We will begin with the Card by adding a virtual method that allows a Card to be loaded based on a dictionary obtained from our JSON.

public virtual void Load (Dictionary<string, object> data) { id = (string)data ["id"]; name = (string)data ["name"]; text = (string)data ["text"]; cost = System.Convert.ToInt32(data["cost"]); }

Minion

Because a Minion inherits from a Card, we can override the Load method to make sure that fields specific to this type of card will also be loaded. Don’t forget to call the base version of the method as well!

public override void Load (Dictionary<string, object> data) { base.Load (data); attack = System.Convert.ToInt32 (data["attack"]); hitPoints = maxHitPoints = System.Convert.ToInt32 (data["hit points"]); allowedAttacks = 1; }

Other Card Subclasses

We haven’t actually implemented the other subclasses of cards, except for a Spell, and that particular card didn’t define any extra fields, so we can ignore these for now.

Mark

Marks are used both by the Target card aspect and the Target Selector classes of ability aspects. We can load it with a dictionary of data like this:

public Mark (Dictionary<string, object> data) { alliance = (Alliance)Enum.Parse (typeof(Alliance), (string)data ["alliance"]); zones = (Zones)Enum.Parse (typeof(Zones), (string)data ["zone"]); }

Note that I also imported the “System” namespace.

Target Selector Interface

Our Target Selector classes do not share a base class. However, they do share an interface. In order to make sure we can “Load” each of them, let’s add another method to the interface:

void Load(Dictionary<string, object> data);

Now let’s implement the new method in each class:

All Target

public void Load(Dictionary<string, object> data) { var markData = (Dictionary<string, object>)data["mark"]; mark = new Mark (markData); }

Manual Target

Note that this implementation is empty, because no fields are needed, but I still must implement the method in order to properly conform to the interface.

public void Load(Dictionary<string, object> data) { }

Random Target

public void Load(Dictionary<string, object> data) { var markData = (Dictionary<string, object>)data["mark"]; mark = new Mark (markData); count = System.Convert.ToInt32(data ["count"]); }

Deck Factory

Go ahead and remove ALL of the code inside the body of the Deck Factory. Yup, all of it – it was placeholder code anyway. Our new version is a little shorter, and has the benefit of being reusable for any configuration of card that we want to put into our JSON resources.

public static class DeckFactory { // Maps from a Card ID, to the Card's Data public static Dictionary<string, Dictionary<string, object>> Cards { get { if (_cards == null) { _cards = LoadDemoCollection (); } return _cards; } } private static Dictionary<string, Dictionary<string, object>> _cards = null; private static Dictionary<string, Dictionary<string, object>> LoadDemoCollection () { var file = Resources.Load<TextAsset> ("DemoCards"); var dict = MiniJSON.Json.Deserialize (file.text) as Dictionary<string, object>; Resources.UnloadAsset (file); var array = (List<object>)dict ["cards"]; var result = new Dictionary<string, Dictionary<string, object>> (); foreach (object entry in array) { var cardData = (Dictionary<string, object>)entry; var id = (string)cardData["id"]; result.Add (id, cardData); } return result; } public static List<Card> CreateDeck(string fileName, int ownerIndex) { var file = Resources.Load<TextAsset> (fileName); var contents = MiniJSON.Json.Deserialize (file.text) as Dictionary<string, object>; Resources.UnloadAsset (file); var array = (List<object>)contents ["deck"]; var result = new List<Card> (); foreach (object item in array) { var id = (string)item; var card = CreateCard (id, ownerIndex); result.Add (card); } return result; } public static Card CreateCard(string id, int ownerIndex) { var cardData = Cards [id]; Card card = CreateCard (cardData, ownerIndex); AddTarget (card, cardData); AddAbilities (card, cardData); AddMechanics (card, cardData); return card; } private static Card CreateCard (Dictionary<string, object> data, int ownerIndex) { var cardType = (string)data["type"]; var type = Type.GetType (cardType); var instance = Activator.CreateInstance (type) as Card; instance.Load (data); instance.ownerIndex = ownerIndex; return instance; } private static void AddTarget (Card card, Dictionary<string, object> data) { if (data.ContainsKey ("target") == false) return; var targetData = (Dictionary<string, object>)data ["target"]; var target = card.AddAspect<Target> (); var allowedData = (Dictionary<string, object>)targetData["allowed"]; target.allowed = new Mark (allowedData); var preferredData = (Dictionary<string, object>)targetData["preferred"]; target.preferred = new Mark (preferredData); } private static void AddAbilities (Card card, Dictionary<string, object> data) { if (data.ContainsKey ("abilities") == false) return; var abilities = (List<object>)data ["abilities"]; foreach (object entry in abilities) { var abilityData = (Dictionary<string, object>)entry; Ability ability = AddAbility (card, abilityData); AddSelector (ability, abilityData); } } private static Ability AddAbility (Card card, Dictionary<string, object> data) { var ability = card.AddAspect<Ability> (); ability.actionName = (string)data["action"]; ability.userInfo = data["info"]; return ability; } private static void AddSelector (Ability ability, Dictionary<string, object> data) { if (data.ContainsKey ("targetSelector") == false) return; var selectorData = (Dictionary<string, object>)data["targetSelector"]; var typeName = (string)selectorData["type"]; var type = Type.GetType (typeName); var instance = Activator.CreateInstance (type) as ITargetSelector; instance.Load (selectorData); ability.AddAspect<ITargetSelector> (instance); } private static void AddMechanics (Card card, Dictionary<string, object> data) { if (data.ContainsKey ("taunt")) { card.AddAspect<Taunt> (); } } }

Hopefully the code is pretty self-documenting. At the top I created a lazy loaded property called Cards which is a dictionary mapping from a card id to the json dictionary representing the same card. Whenever the property is accessed it will automatically load our demo collection if needed. The property is public, although it probably wont ever need to be directly accessed.

In the LoadDemoCollection method, I load the resource file for our demo card collection using the “Resources” library to get our TextAsset. Note that it is important to Unload any resource that you manually Load. Next I use the MiniJSON library I mentioned earlier to parse the text of the file into the initial dictionary of data. Finally I grab the array of cards and iterate over each to populate my card collection dictionary.

In the CreateDeck method I perform a similar flow, where we load the demo deck resource from the Resources folder. I again use MiniJSON to deserialize the text into a dictionary that is easy for me to work with. I can then grab the array of card ids that are needed. I loop over each id, and use another method to create the actual card, from the data related to the card id. We will use this method to create the deck of cards used in our game.

The CreateCard method is also public. It doesn’t need to be public scope right now, but it could be helpful for certain card abilities that summon other cards when played. It could also be handy to “reset” a card to its original state if needed. After creating a card, this method goes through a couple of steps to make sure that each of the various card aspects and abilities will also be able to be loaded.

In this case, all of the loading of a card was kept inside the Factory class. However, if the number of unique aspects grows enough, it might make this class feel a bit too long. We could move the “loading” of a card’s aspects into the same systems that manage the aspect to help distribute the burden.

Game View System

Let’s update the GameViewSystem’s “Temp_SetupSinglePlayer” method – it had called our old DeckFactory create method, but now needs to invoke it with a name of a deck to load, and will also specify the owner player index at the same time.

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.CreateDeck ("DemoDeck", 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); } }

Demo

Go ahead and play the game again. Unfortunately there are no new game features to try out, but it is still important to make sure that you aren’t missing any of the previous features now that we have swapped to a new dynamic factory for our cards.

If you like, you could try extending the collection of cards to make your own, and make a couple of different decks for the different players. Do this by editing and adding to the “DemoCards.txt” resource file, and by editing or adding another deck resource file as well.

Summary

Data persistance isn’t an “exciting” part of game development to me, but it is admittedly an important one. In this lesson we showed a “potential” way to store your data, but keep in mind that this is only one of many options. I just wanted to prove that the architecture created up to this point is able to work with a common resource pattern. In particular I chose JSON for this because it is very commonly used for fetching online data and could be very applicable to a multiplayer implementation.

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!