Developer Diary #2 - Code vs Configuration

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.

9 Likes

wow, this is awesome!

i’ll take a look at all this and point out what i think the coolest parts are later, assuming that’s welcome!

(also, it’s interesting how much of this is really just program design fundamentals – the way this is architected is mostly identical to the way it’s done in react/jsx engines, as well. JSX is just a fancy, mostly XML way to generate plain JS objects that fit into JSON. all just components, configuration, composition, etc., all the way down.)

1 Like

Comment away. I’m happy to hear about the flaws too.

1 Like

those i think i’d be less equipped to weigh in on – i simply don’t know enough about how well this suits the unity toolchain (i don’t use it) to say what you should’ve done differently. i will say that we differed slightly in that we have configuration-as-code. producing config files in typescript allowed full access to intellisense throughout the codebase. some clever behind-the-scenes work by the engine to JIT well-typed manifest files allows in-editor and linter errors for erroneous asset names, e.g. chapter-negative-one instead of chapter-negative-1.

2 Likes

What to code and which to configure?

It depends where you want the system boundary to be. But everything inside that boundary is code (even if you’ve disguised it as config). And you want a low defect rate for your system, hence you write unit tests, which invoke your code only, and should be invariant to user config.

Object oriented languages give us a way to define behaviour (classes) and capture state (objects). But combining behaviours is tricky in languages that don’t support multiple inheritance. Likewise providing special cases for particular combinations of things (An elvish thief gets +1 Charisma, etc).

So, you end up writing little unit tests which capture your intentions (I want to stack buffs on this Hammer a particular way to maximise area damage, etc).

I’ve found the Strategy architectural pattern very useful. You basically select behaviour at run-time, not compile time. And because it’s code you can write unit tests which are not data (config) dependent.

1 Like

you’ll probably be horrified to know there’s basically no unit tests involved in our game! (i certainly am; it’s one of many items on the todo list.) in practice, most of that was done through real-time automation. nearly all the difficulty came from things which didn’t concern any game code, and couldn’t be tested through any gamewise unit tests. (more devops, HTML audio, front-end perf, webpack, etc.)

i think broadly this is a pretty big difficulty for novice game programmers who don’t have a background or education in software engineering, though. there are a billion cool ideas, patterns, and modalities in programming that we can choose from to produce safe and modularized game codebases. i think, if one doesn’t have any experience with any of those patterns, it can be very hard to find which one fits your design better. we made a lot of sacrifices that would’ve been insane for basically any “normal” single-page app, progressive web app, or web game, but we did so with the knowledge that we already knew how to fix the problems if it all went pear-shaped in the last week.

in particular it seems to be extremely hard to find intro-level documentation for any game engine which isn’t twine or inform. the work of programming is very different from technical writing, both are very different from the work of game design, and no one’s ever made a FOSS text game engine into the sole basis of their business income. i’ve found the most helpful thing is flexibility – when it is time to listen more to the designers and editors than to the programmer? one can construe that as separate hats for one person to wear, or scale it up, but deadlines nearly always force us to decide whether we want to focus on velocity or our ideal best practices.

(and none of this is at all to disagree with you or the utility of strategy patterns, mostly just that my “best practice” depends entirely on the game.)

1 Like

Well that’s why I practice writing IF as a hobby, and fall back on Software Engineering for a living :laughing:

Seriously, I think the confluence of art and engineering is where it’s at the same time the most attractive, and the most challenging.

I wouldn’t be comfortable supporting a product if I didn’t have good test coverage. And yet, we know that correctness is not the secret to an entertaining game.

As an example, I was surprised this year exactly how long it took me to create (well-proven, industry-backed) UML diagramming in a notation which I actually found useful in designing character interactions for my long-running IF project. I’ve finally worked out how to apply Activity Diagrams to an evolving narrative. But it seems that traditional SW Engineering does not anticipate many of the challenges game development presents.

2 Likes

I guess now I’ll have to do diary #3 next week on how I did testing for the game.

3 Likes

Haha, well I’d love to hear that but please don’t feel any pressure :grinning:.
Thank you for sharing your thoughts on design. I think there are all too few people who care deeply about this kind of thing, and it’s great to meet and chat here.

1 Like

(@crisostimo, please do, we badly need some recent prior art for the test work to come :slight_smile: )

but, @tundish, likewise on SWE work, not least of which because the opportunities involved in making free IF don’t really tend to be in IF. you can get a game industry job, or you can work for something like choices/episodes, but neither of those is really “IF” as most would ideally describe it, and there’s certainly no big-budget contract IF programming jobs i know of right now.

and i think you’re spot on about that confluence being both fascinating and difficult. on the other side, having spent some time in the ink community, when i was learning about how ink worked, it seemed like the single most common source of problems was overengineering by programmers who thought ink seemed too good to be true. they assumed it would somehow help them co-work or optimize to have multiple stories for different display panes, or NPCs, or etc. actually, inkle made both ink and inky (and inklecate, and you’re starting to see one issue already :laughing:) intending everyone to use a single story, and every single one of their games, heaven’s vault included, was written using inky to produce a single story. too much thinking is a huge peril here.

some of the reasons i’ve come up with why that’s the case, and why a lot of SWE lessons only fit halfway:

  • IF/text games are fairly unique in that the vast majority of them were made for a single release, and so the codebase will never need to change, nor receive any more responsibilities than it already has. if the player can’t tell it’s broken, it’s not! that couldn’t be more different from a software or service offering for money, where users expect steady updates and non-brokenness in a wild permutation of cases, some always outside the user stories. (it’s also why we’ll definitely add tests before then; a lot of the difficulty will be sliced off by switching permanently to an electron executable rather than a CDN-based web delivery.)
  • the IF audience has largely orthogonal feelings about what makes a game “polished” as compared to AAA gamers and SaaS users, and these tend to have less relation in practical terms to standard coder practice than IF historical practice plus narrative ingenuity in that context. this is hardly to suggest mark sample did less work, but i’m sure for many players he accomplished as much in “babyface” with a twine, a flat HTML, and some clever CSS as we did with a comp-server-breakingly large entry. most would consider that a misapplication of resources; the editor-manager agrees with that, but that’s her job to agree with.
  • the largest zones for visibility don’t really expect finished work so much as self-contained work. “a sense of harmony” is explicitly billed as a prelude and that probably won’t hurt it much. from that context, for commercial entries, the ifcomp is somewhere between cannes-style film festival, GDC/IGF-style nearly-done booth promo, an early access release, and a classic pc gamer disc demo of a game you’ll probably see on shelves next year. in this, as in most things online, the material consequences of going unseen are hugely worse in revenue terms than releasing a glitteringly complex game which may well not work at all for 1-10% of users, and has about 50 different line items in the billing.

so i think it requires a gestalt of nearly all the skills we normally expect to be distributed to different workers and departments, plus the manager-brain needed to tell when you’re just plain going too far with one aspect. (i predicated a lot of the manager-brain on the idea we wouldn’t have enough text or audio to fill out the game, and now there’s too much audio to even preload in large quantities.) there’s quite simply few games that are perfect in one way, and average in all the others, who are going to place much better than the endearingly ragged ones. even then, placement’s not even half the story; reviews drive the traffic. we’ve even got an award for perplexing people, gaining the highest standard deviation, and doing the opposite of selling to the whole crowd!

2 Likes

I’m just starting to get a glimpse at the challenges you are facing, and the judgements you have to make.

My own objective is to create (for the Python community in this case) some FOSS tooling which is reliable and flexible. It should not be a source of friction to the creator, nor impose an unreasonable cost of ownership when in production.

I’ve really enjoyed this thread. Will have to stop now. It’s bed-time in my part of the world. :smiley:

2 Likes