Characters, Jobs, and Abilities

The basic architecture looked like this:

Each Character has a Species, a set of shared characteristics. They also have a current Job, and a record of the Jobs they have unlocked.

Each Job defines a set of Abilities that Characters can learn.

Finally, in order to differentiate between, say, a Character who just unlocked the Soldier Job, versus a level 10 Soldier who’s mastered all the Soldier Abilities, we need one more data class: JobProgress. It tracks a Character’s level in a Job, and which of that Job’s Abilities it has unlocked.

The big design choice of note here is the decision NOT to write a specific class for each Job and Ability. I was looking at ~25 Jobs with 4–8 Abilities each, for a total of ~100–200 Abilities.

At the time, writing 125–225 unique classes sounded time-consuming, error-prone, and poorly designed. But wait, those Abilities are all going to share common building-blocks, right? They’re all going to have some combination of similar Effects like damaging enemies, restoring health, moving Characters, bestowing boons/ailments, and so on. There are also only so many ways that Abilities can be targeted (self, melee, ranged, beam, cone, blast, etc).

The Restrictions to unlock each Job or Ability could be broken down to a set of building blocks as well: is the Character a specific Species? Has it reached X level in Job Y? Has it learned Z Ability?

The engineering principle I’m describing is reusability; I was determined to define Jobs and Abilities broadly as ScriptableObjects, and not implement each one individually. Instead, I would code the definitions of tiny Restrictions and Effects themselves, and then combine and reuse them as needed. The goal was to reduce code duplication, and the more Jobs and Abilities I created, the more those gains would compound.

Let’s see how that worked out for me…

Restrictions

In the context of Jobs and Abilities, a “Restriction” defines a check that a Character must pass in order to unlock it. Let’s look at some hypothetical requirements for the Job of Recruit and its descendant, Soldier.

Recruit: Biological only (Human or Cyborg, no Robots)

Soldier: Recruit level 3 or higher

But without classes for Human, Cyborg, Robot, Recruit, Soldier, and so on, I needed the blocks from which those checks would be built.

So my code definition of Restriction was: a class which performs a check on a Character, called IsMet(Character) , and returns true if the Character meets its requirements.

I define two subclasses of Restriction: SpeciesRestriction and JobRestriction, which return true from IsMet(Character) if the Character is of the valid Species or has reached the required level of the specified Job, respectively.

When the game needs to know if a Character has unlocked a Job, it runs all the Job’s Restrictions’ IsMet function against the Character. If they all pass, the Job is unlocked.

Abilities have Restrictions too, but I’ll spare you more diagrams (I have them though, DM me to see them, they’re great). The main point here is that all Job/Ability Restrictions have the same input (a Character) and conceptual output (whether a thing is unlocked). They don’t require any other context.

This system isn’t terrible, though perhaps bulkier than necessary depending on the scale of the game. It’s when I got into Effects, and how that intersected with Restrictions, that my trouble truly started.

Effects

An Ability that does nothing would be pretty useless, so each Ability has 1 or more Effects such as damaging, healing, or moving Characters.

But what if the individual Effects of an Ability must be limited to different Characters? Take an example seen in many games, Life Steal. This Ability has two Effects: Damage Target(s), and Heal Self. Each Effect needs to select the Characters to which it applies, depending on who was the source and who was the target.

“Wait a minute,” I thought. “Selecting is just a different way of saying Restricting, isn’t it?” And so the concept of a Restriction in my code base grew to cover this too.

If you’re wondering why Soldiers can Steal Life, it was to keep this example “simple.” You want to get into psions and psychics on top of all this? Didn’t think so.

Looks grea- oh… wait… the Restriction bool Met(Character) method only takes Character as a single parameter, and doesn’t provide (or until this point, require) any other information. How would an Effect tell by itself if the Character in question was the target of the Ability, or the source? Clearly, this type of Restriction needs some situational context.

So I stopped, took this as a sign that my previous definition of “Restriction” didn’t fit this use case, and implemented this part in some other way.

Just kidding, that would be too easy. “NO!” I curse the voice in my head muttering something about cans and worms, “I’m smarter than this problem! I’ll use TEMPLATING!”

So the simple Restriction class became Restriction<T>, and bool IsMet(Character) became bool IsMet(T) . Everything that was a Restriction until this point became a Restriction<Character>, and now Ability-related Restrictions became Restriction<ActionContext>.

“And you know what else?” I railed against my better judgement, “I noticed another place I can use Restrictions too — in the way Abilities choose targets! Some Abilities can only affect biological Characters, others only mechanical, and others don’t pick a single targetatallbutareablastorawaveor…”

And through the floodgates came multi-tiered inheritance of Restriction subclasses based on the amount and type of information they wanted, conversion functions, overloaded IsMet(...) methods, properties that were just typecasts of other properties, and worst of all: custom Unity Editors. The abstract concept of a “Restriction” (which if you think about it, is about as broad as the concept of an if statement) came into use any time I wanted to apply modular filtering on anything in the game.

Note: I have nothing against custom Unity Editors when used for good. Using them to enable bad design is not good.

Here’s what I end up with to implement just two Jobs and one Ability:

Remember that blue line across the middle.

If you’re keeping score, that’s seven classes, three subclasses, and twelve Editor items. For three in-game concepts.

And yes, many of those classes will be reused, some of them dozens of times, and that’s great (though the Editor item list will continue to balloon dramatically when most Abilities have multiple Effects, and most Effects have their own Restrictions). But really, most of those reused pieces were only a few lines long… couldn’t they just have been functions of their base classes? That’s still in-line with the reusability principle, it just doesn’t needlessly split code off into other entities.

Now, this type of design by itself isn’t inherently bad (though it sure as hell isn’t good, in my opinion); indeed, for some developers whose minds work this way, with proper planning, it could in theory save time or lines of code.

But for me, this meant implementing entire subclasses of Restriction that were used only once. On top of that, as it turns out when your entities are split into a multilayered web with dozens of connections and inscrutable structure, it negates a benefit that ScriptableObjects provide in the Unity Editor too, because it takes extra time to unwrap it in your brain whenever trying to see how pieces fit together.

Best of all, I STILL hadn’t learned my lesson when I started my current game, SCUM (#SCUMgame), because I still thought splitting things into tiny modular chunks is always more robust. Here’s a screenshot of my Unity Editor after implementing just eight crew Commands (SCUM’s equivalent to Abilities) with, you guessed it, Effects and Restrictions: