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.
[json]
{
“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
}
]
}
[/json]
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.
[json]
{
“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”
]
}
[/json]
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.
[csharp]
public virtual void Load (Dictionary
id = (string)data [“id”];
name = (string)data [“name”];
text = (string)data [“text”];
cost = System.Convert.ToInt32(data[“cost”]);
}
[/csharp]
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!
[csharp]
public override void Load (Dictionary
base.Load (data);
attack = System.Convert.ToInt32 (data[“attack”]);
hitPoints = maxHitPoints = System.Convert.ToInt32 (data[“hit points”]);
allowedAttacks = 1;
}
[/csharp]
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:
[csharp]
public Mark (Dictionary
alliance = (Alliance)Enum.Parse (typeof(Alliance), (string)data [“alliance”]);
zones = (Zones)Enum.Parse (typeof(Zones), (string)data [“zone”]);
}
[/csharp]
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:
[csharp]
void Load(Dictionary
[/csharp]
Now let’s implement the new method in each class:
All Target
[csharp]
public void Load(Dictionary
var markData = (Dictionary
mark = new Mark (markData);
}
[/csharp]
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.
[csharp]
public void Load(Dictionary
}
[/csharp]
Random Target
[csharp]
public void Load(Dictionary
var markData = (Dictionary
mark = new Mark (markData);
count = System.Convert.ToInt32(data [“count”]);
}
[/csharp]
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.
[csharp]
public static class DeckFactory {
// Maps from a Card ID, to the Card’s Data
public static Dictionary
get {
if (_cards == null) {
_cards = LoadDemoCollection ();
}
return _cards;
}
}
private static Dictionary
private static Dictionary
var file = Resources.Load
var dict = MiniJSON.Json.Deserialize (file.text) as Dictionary
Resources.UnloadAsset (file);
var array = (List
public static List
var file = Resources.Load
var contents = MiniJSON.Json.Deserialize (file.text) as Dictionary
Resources.UnloadAsset (file);
var array = (List
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
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
if (data.ContainsKey (“target”) == false)
return;
var targetData = (Dictionary
var target = card.AddAspect
var allowedData = (Dictionary
target.allowed = new Mark (allowedData);
var preferredData = (Dictionary
target.preferred = new Mark (preferredData);
}
private static void AddAbilities (Card card, Dictionary
if (data.ContainsKey (“abilities”) == false)
return;
var abilities = (List
private static Ability AddAbility (Card card, Dictionary
var ability = card.AddAspect
ability.actionName = (string)data[“action”];
ability.userInfo = data[“info”];
return ability;
}
private static void AddSelector (Ability ability, Dictionary
if (data.ContainsKey (“targetSelector”) == false)
return;
var selectorData = (Dictionary
var typeName = (string)selectorData[“type”];
var type = Type.GetType (typeName);
var instance = Activator.CreateInstance (type) as ITargetSelector;
instance.Load (selectorData);
ability.AddAspect
}
private static void AddMechanics (Card card, Dictionary
if (data.ContainsKey (“taunt”)) {
card.AddAspect
}
}
}
[/csharp]
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.
[csharp]
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);
}
}
[/csharp]
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!
I have a doubt, does this aproach its more performant than Scriptable objects? or just experimenting?
It’s not so much an issue of performance as it is one of flexibility. For example, the JSON approach would be compatible with things like Firebase which could serve as a backend database to make sure all users see the same deck of cards when playing the game – even if you want to change things after the game has shipped.
Like a asset bundle.
Yes, in a way, it is like an asset bundle. However, I could also use JSON for dynamic content or user created content. For example, when a user wants to save a particular subset of their cards as a deck, and would want that information available on their phone and computer based on their account. I couldn’t use an asset bundle to work with that kind of information because asset bundles are created at edit-time, not at run-time.
Thanks a lot for the post.Really thank you! Much obliged.
You’re welcome!
Thanks a lot for the article post. Much thanks again. Fantastic.
Hello
is this project complete or are you planning to continue on this ? curious what is in store next if it is!
I would say it is more “on-hold”, because I still want to do some actual online multiplayer stuff at a minimum. The next posts in the series will be delayed though because I recently started on a new project at work which cut into my normal free time, and to top it off I am about to go on vacation for a couple of weeks. I am glad to see interest in the project though!
Just wondering about the 3d vs 2d choice…any tips to convert from 3d to 2d…? For the sakes of camera and different devices on different resolution and screen sizes..etc…curious why did u chose 3d vs 2d?
I think the best tip is just to get in and start playing with the tools and try things out. The cards themselves are using 2D canvas UI elements in 3D space, so there shouldn’t be a big leap between the two. I chose 3d because it matched more closely with the game that inspired this series – Hearthstone.
Hello! Thank you for your tutorials, they are awesome!
I would interest on multiplayer with this approach container-aspect. Can you write some posts about that? I have some doubts about scenes of another players (e.g. the main player is allways on bottom side).. Do you would use Unity Network for multiplayer? Thanks a lot again! And I hope you come back with these tutorials! π
For the UI Cards, did you use World Space Canvases?
Yes, the cards are World space canvases. FYI, the whole project is available for download so you can see exactly how it is configured. see the links in the “Summary” paragraph at the end of this lesson.
Nice tutorials, im also a C# dev, but I still like to read tutorials to get a inspiration how other devs implement these features.
Do you have any plans for new game projects?
I have a few ideas, though I probably wont commit until Unity ECS is production ready.
Awesome tutorials! I have a question about the way card abilities are modeled. I understand the advantages of modeling abilities as data, so that game systems can pick the data and apply the game logic and animations. But how would this data-driven approach handle parameters or conditional variables? Example: imagine a card that says “deal 2 damage. If it kills a minion, draw a card.”, the card data must somehow have stored a condition (whether a minion was killed or not) before the execution of the action? Another example: “discard your hand, draw as many cards”. How would you go about modelling these?
Great questions. The architecture I put together is really just to help you get started, so as you can see you can’t exactly model it based on what I have in place. My main answer would be this: you dont have to (and can’t) model the dynamic parts of an action in its data. You simply need to mark it as a kind of ability which the system(s) will know how to use.
Considering your examples, you have an ability that has multiple effects. You know some things about it, such as that it may target one minion and that it will do 2 damage. So those are the things you would put in JSON – the name of the action (which the systems will know is a dynamic action), the kind of target it can select, the amount of damage to do, etc. Perhaps a special system will be created to listen for when you play this exact action, then will look at the target of the action to check if it has been mortally wounded, and if so, then will repond with a re-action that draws a card.
Likewise, you have an action that is basically “DiscardAndDrawHand” and that might be all you have to specify in the JSON. You create a system to listen for that action to be played, which responds by creating a reaction to discard each card in your hand, and another reaction to draw the same number of cards. The system at that time would know the correct values to use when creating the reaction actions.
Does that make sense?
Thanks for the answers. With a lot of iterations I endeded up with a system in which each action produces a “response JSON object” with some data about the actual response. For example, if a DrawAction happens with number 3, but only 2 cards were drawn (e.g. the deck size was only 2), the response object holds info that only 2 cards were drawn.
So the Discard and Draw effect was modeled as 2 effects, “DiscardAction” with “Selection”: “All”, and then I got the result of this action to fuel the next action (“DrawAction”, “FromResult”: “Num” gets the Num property from the Result object).
Sounds like a great solution π
Great tutorial seines!
As I understood it, you added a list of abilities to a card in Order to give it Multiple abailities i.e. deal 3 damage. Draw a card.
I would expect those to be 2 simple abilities to be added to the card. However because one can only add one aspect of the same Type to a card, its mit possible to add Multiple abilities to one card, except you create more subtypes of abilities or create a new Ability like dealDmgAndDraw.
Did i get this right?
You’ve got the basic idea. I wouldn’t add two of the same ability to a card because I would just modify the ability to support more than one of its own application. For example, the Draw a card ability could just hold a field that shows how many cards to draw. I could also create more subtypes of abilities like you said, but I would only do so if they were unable to function apart from each other. Dealing damage and drawing both can function apart from each other, unless their application is more nuanced, like you only draw a card if the applied damage kills its target, but then I might use another pattern still such as making one a triggered reaction of the other and not a direct ability. Good luck!
I’m not talking about adding two of the same ability to a card, but i.e. draw a card ability and deal damage ability to a card. It doesn’t seem to be possible with this implementation, because you can only add one Aspect of Type Ability, since GetAspect will always return just one ability.
I did not quite understand why you added the Ability as an Aspect instead of just adding a List of Abilities to the card object.
In the current implementation you would need to create new Aspects for each ability type on order to have more than one applied to a card.
I was just wondering, because you defined the Abilities of a card specifically as a list, although it’s not possible to have more than one.
Also I noticed using only one Ability for drawing cards will activate abilities that trigger on card draw only once, instead of on each card draw like (“Whenever you draw a card…”) will only trigger once this way.
Also how would you tackle cards like “violet teacher”, so basically cards that act as actionsystems on their own. Is your implementation supposed to instanciate a new actionsystem and attach it to the gameobject of the minion itself?
Thanks for this series, I really learnt alot.
Good questions, and yeah that is a different issue altogether. I probably could (and should) have made the JSON example not use an array of ability for clarity. Although I wrote this a long time ago, I guess that I was probably beginning to think about some architecture that could support more than one ability per card. It might be as simple as making an ability that has an effect of being a compound ability, and which controls how the sub abilities play out. The factory class could then build the cards differently based on the count of abilities in the array.
There is still a lot of functionality I didnt implement, including things like secrets and triggered abilities, though my hope was that readers who had made it this far would have the skill needed to continue on their own. Dynamic triggers may seem complex, but you are really just responding to an action to react with playing an effect just like you would at any other point in the game. You may approach it by making additional systems and structures, similar to how abilities may have Target Selectors, they may instead have something like an Event Trigger aspect instead.
I’m glad you enjoyed the series and have learned from it. You will learn far more if you start digging in and changing / adding features. Good luck! Don’t forget that if you start trying new things and get stuck that you can post in the forums.
Hi thank you for all your articles about Card Games, it helps me a lot in my current project, I really like your way to organise things.
But my today’s question is about storing all cards data and I first thought to store everything in JSON files like you explain, but I also want my game to have translation for multiple languages. Do you think this solution of using JSON relevant ?
I think that use a JSON for each langage would be a solution (and load the correct file corresponding to the current locale) but in another way, the JSON stores also the logic of the card, like attack, abilities, etc… that are no strings to translate. Maintainability is going to be much harder with multiple JSON files.
Have you an idea of a structure that will be more simple and easy to maintain and that can use Unity’s localize system ?
Thank you in advance !
Sorry if my english is not readable sometimes, it’s not my primary language ^^
I have not worked with Unity’s localization libraries before. One solution that doesn’t rely on them would be to use string keys in your card’s json files, rather than the card text itself. The string key can then be used to look up a translated string in a separate localized JSON file.
I thought about this solution right after sending my first message and I think it’s the one I’m going to use.
Thank you for your fast answer !
And the fact that you propose the same idea as mine make’s me more confident for my development.