For my second developer diary, I’d like to jump into some of the implementation details of my game, Tragic. I’m not going to assume the reader has any coding background, but I will be popping some snippets from the implementation in here that will be more or less readable to you based on your background.
Creating a Card Game
I mentioned before that I work in the games industry as an engineer. Part of my job involves interviewing engineering candidates for positions at my company. During these interviews one of the things I frequently ask is for the candidate to design and architect a card game and then sell me on this architecture. While I had my own considered answer, I had never before written a card game more complicated than Blackjack. Part of the motivation of writing Tragic was to put my theories to the test.
My main thesis was that the proper way to work with cards that have flexible effects was to use a component based architecture (I’ll define that a little later in this post). During last year’s competition I was forming the ideas for the game and I talked them over with a friend of mine. He also had some clever ideas about how I could pull some of the logic out of the game code and put it into configuration files. With these ideas in mind, I got to work.
Game State
Let’s first take a step back. A computer is nothing more than a state machine. All this means is that it stores some data that is completely static at any one point in time (the state) and at some defined frequency it looks at that data and potentially changes it based on a set of rules (the machine). Computer programs, and thus computer games are state that effects how the machine works.
Game state then is all of the data associated with a snapshot in time of the game. In a baseball game, for instance, the state might include who the runner is currently at first base and who is in each defensive position, including information about them, like how fast they can run. The pitcher pitches the ball. The play is made. The game updates the game state then waits for the next state changing event to arrive.
Continuing with the baseball analogy we would call the players on the field Game Objects. Some game engines also refer to them as Actors. In a game like Tragic, the game objects include things like the player and non-player characters, the items you can equip, and the cards themselves. For the player character, I kept everything in code. In my case these are C# files that run inside the Unity game engine. For nearly every other object type in the game I tried to push as much as possible out of the code and into configuration.
Configuring a Card
Let’s take a look at the configuration of a fairly simple card:
{
"ID": "C_CONFUSION",
"Name": "Confusion",
"Cost": 0,
"Rarity": "Normal",
"Targets": "NoTarget",
"Classification": "Curse",
"Group": "Restricted",
"NarrativeDescription": "A warrior swings his arms in a mighty arc, but no weapon lies within his grip.",
"Components": [
{
"Type": "Inert"
}
]
}
This little snippet of text contains everything the game needs to know about how this card will function in the game without the game code needing to know anything about the existence of a ‘Confusion’ card. The game code actually has no idea about the particulars of any of the cards in the game. There are over 100 cards in the game and it has to learn about all of them by reading JSON files filled with configuration like you see here.
There are a lot of benefits in moving data from code to configuration:
- It’s highly readable. Designers can create new gameplay mechanics without needing to first learn to code.
- It is easy to modify or swap in and out the game objects available to the game, even while the game is running.
- There is no need to recompile the game when you need to make a change.
- You can allow for community modifications to the game without creating large security risks.
- And best of all, it dramatically cuts down on the development time of the game.
Component Objects
The most interesting and flexible portion of the card configuration is it’s list of Components. There is a single class object that all cards use and it contains the following:
public class Card : IComponentGroup
{
public string ID { get; set; }
…
public List<IComponent> Components { get { return ComponentGroup.Components; } }
public Card()
{
ComponentGroup = new BasicComponentGroup();
IsTemplate = true;
UniqueID = Guid.NewGuid();
}
}
public interface IComponent
{
IComponent MakeCopy();
}
I show this code simply to point out that Components can be just about anything. A card component has to be a class specially marked as an IComponent, but the only thing it is required to do is be able to make a copy of itself. The Card class itself simply keeps track of a list of Components.
The Confusion card shown above has a single component, “Inert”. Inert is an instance of what I call a Keyword component:
interface IKeywordComponent : IComponent
{
string Keyword { get; }
string NarrativeDescription { get; }
}
The Keyword in this case is “Inert” and the NarrativeDescription is defined in another configuration file. These two pieces of text show up in the tooltip for the card. The game code has a method in it that checks to see if a card in your hand can be played at any given time. One of the first things I do in that method is to check to see if card has an Inert component on it, and if so return false.
The great benefit to this approach is that I did not have to write into the Card class some data field that keeps track of whether a card is playable or not. I could have done that, but if I were to add a such a field for every single possible configuration for cards, the code would have quickly become bloated and unmanageable.
Compounding Components
Let’s look at a more complicated example:
{
"ID": "R_DEALT_OR_DEFENSE",
"Name": "Enter Stance",
"Cost": 1,
"Rarity": "Epic",
"Targets": "Self",
"Classification": "Effect",
"Group": "Standard",
"ClassRestrictions": [ "Rogue" ],
"NarrativeDescription": "Two fighters demonstrate their stances, one offensive and one defensive.",
"Components": [
{
"Type": "EvenOdd",
"Even": [
{
"Type": "ApplyEffect",
"Amount": 2,
"EffectID": "INCREASE_PHYSICAL_DEALT",
"AppliesTo": "Self"
}
],
"Odd": [
{
"Type": "Defense",
"Amount": 7,
"DamageType": "Both"
}
]
}
]
}
Enter Stance is a card available only to the Rogue class. The Rogue has a unique mechanic in her cards in that they sometimes change their ability depending on whether you have an even or odd number of cards in your hand. This makes the Rogue a bit more of a tactical class than the other two.
The interesting thing about this configuration is that it contains Components that in turn contain more Components. The EvenOdd Component has two separate lists of child Components, one associated with Odd hands and one with Even hands. The EvenOdd Component is also marked internally as a playable component, whereas all keyword components are not.
Again the card class has no idea that this sort of thing is possible. When you play the card, it simply looks through all of its components to check if they are playable and then tells the ones that are to play themselves. The EvenOdd Component also has no idea what its children Components are capable of, but it does know how to check on the current Hand size of the player. Once it does that it can tell the appropriate children to go play themselves.
There are several CompoundComponents in the game, including ones I call ConditionalComponents. ConditionalComponents are very similar to EvenOdd in that they check if some case is true or false and change their actions based on that evaluation. In this case I had to write a little parser for an evaluation language specific to Tragic (making this the third time in a row I had to write my own parser for my IFComp entry).
To get an idea of what a ConditionalComponent looks like:
{
"ID": "R_WEAPON_ENFEEBLED_DRAW",
"Name": "Press The Advantage",
"Cost": 1,
"Rarity": "Epic",
"Targets": "SingleOpponent",
"Classification": "Attack",
"Group": "Standard",
"ClassRestrictions": [ "Rogue" ],
"EffectDescription": "Use your weapon. If the target has Enfeebled, gain 1 Energy and Draw 1.",
"NarrativeDescription": "Always willing to turn victory into victory, the fighter chases a fleeing foe.",
"Components": [
{
"Type": "Weapon"
},
{
"Type": "Conditional",
"Condition": "HasEffect Target REDUCE_PHYSICAL_DEALT",
"Conditional": [
{
"Type": "AddEnergy",
"Amount": 1
},
{
"Type": "DrawCard",
"Amount": 1
}
]
}
]
}
This example also shows that I had to manually create the EffectDescription for this card. For most cards, the description of the effect of the card that you see below the card’s name is automatically generated based on the components in the card. I found that a bit too difficult for some cards, especially those with Conditionals and so I gave myself a configurable way out.
Triggers
Congratulations on making it this far in a post on game implementation. The final topic on configuration here concerns Triggers. The standard way to use a card is of course to play that card through the game’s user interface. Doing so triggers the play of the card’s components that are marked as playable. Some cards however have effects that occur outside of normal card play.
{
"ID": "R_DISCARD_TO_DRAW",
"Name": "Bait",
"Cost": 0,
"Rarity": "Epic",
"Targets": "Self",
"Classification": "Effect",
"Group": "Standard",
"ClassRestrictions": [ "Rogue" ],
"EffectDescription": "When discarded, draw 1",
"NarrativeDescription": "A mousetrap that appears to be far too large to be intended for catching a mouse.",
"Components": [
{
"Type": "DrawCard",
"Amount": 1,
"Trigger": "Discard"
},
{
"Type": "Inert"
}
]
}
Each ActionableComponent has a trigger (PlayableComponents are ActionableComponents whose Trigger defaults to OnCardPlay). In this case the components trigger when the card is discarded through some means outside of normal play. Bait is also Inert, so it has no function without being discarded. This means that Bait has been designed as a combo card where it can blunt the effect of mandatory discard actions.
Other triggers include start and end of a turn, start and end of a match, when damage is taken or dealt, when healing occurs, when a card is drawn, when a buff or debuff is applied, etc.
The absolute magic of this is all, is that with Triggers added to the arsenal of Components, I now have a flexible system that is no longer limited to Cards. There are scores of items that you can equip in the game that needed to have all the same flexibility as cards. Using this system I can simply add the same components to those items. Similarly the enemies that you fight perform all of their actions through Components.
So, What Did I Learn?
I am pretty pleased with outcome of a component based architecture expressed through configuration files. It makes sense that it would work - most game engines adopt a similar approach for their game objects. It’s flexible, repeatable, describable in game text, and it’s testable. If anything, in the future I would give myself fewer outs (a few of the cards use a CustomComponent that basically says, 'this is too complicated, go run some code instead).
I hope that the flexibility of the design manifests itself in an enjoyable game experience for the player. I know I had fun crafting it all.