Notifications
Article
A Tale of ScriptableObjects
Published 4 months ago
37
0
(Not) Hitting the Reusability Sweet Spot
ScriptableObjects are one of the greatest gifts of Unity development. They occupy a uniquely useful intersection between scripting and visual manipulation of in-game assets. In a system where you need the ability to define a type of thing, and then create global instances of those things that differ only in their field values, the ScriptableObject is a godsend; it allows you to do that directly in the Unity Editor instead of through code.
But games are complex, and sometimes changing just the fields between instances isn’t enough. Maybe you want one to behave differently than another. What’s the right approach then? Should you just gate the behavior behind fields, like a state machine? Should you use Inheritance? Or is this question inherently flawed; does the notion of differing behavior between instances of the same thing betray the entire purpose of ScriptableObjects?
I’ve been exploring indie game development in my personal time for a few years now, while working at some of tech’s biggest companies by day. I am still in the middle of my journey to publishing my first game, but along the way I have gotten myself into some glorious messes trying to strike the right balance between reusability and simplicity in my codebase.
This is one of those messes: the story of how I took the ScriptableObject, a tool of convenience that is supposed to be a time-saver, and twisted it into a horrible, incomprehensible time-sink.
My motivations were pure. I held aloft a core design principle as an unimpeachable rule, and in doing so, I was crushed under its weight. Maybe you already know better, in which case you might still enjoy this through sheer schadenfreude. But maybe you’re just getting started too. And if this recap can prevent just one of you from losing weeks of time going down a similar rabbit hole, only to have to claw your way back out like I did, well, dolor hic tibi proderit olim.

The Game

I set out to make a game like one of my all-time favorites: Final Fantasy: Tactics. FF:T was a tactical RPG in which you start with a crew of Characters, lowly Squires and Chemists with nothing but daggers and the shirts on their backs. But by the end of your adventure, they’ve become Ninjas, Knights, Summoners, Mystics, and more, all based on how you choose to specialize them as you play.
For my game (at various times called Laser Tactics, Rogue Tactics, and Kill Order) I chose Sci-Fi as a genre instead of fantasy to differentiate myself from the main inspiration, and because I’ve yet to find a Sci-Fi TRPG with the same fundamentals as FF:T. I worked up all my design documents and spreadsheets that enumerated a hierarchical tree of 25 Jobs, with a few hundred Abilities that Characters could learn. It was just a matter of putting those ideas into code. But hey, I’m a seasoned programmer. That should be the easy part, right?
I could honestly do a whole series on the mistakes I made just on this game, with topics such as the perpetual narrowing of scope, and my first forays into procedural generation. But for now we’re talkin’ ScriptableObjects, and the design of these entities in my game. In this example, that means the Characters (player-controlled and enemy), Jobs, and Abilities.

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.
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:
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:

What Could Have Been

In the grand scheme of things, going through this exercise not once, but twice has opened my eyes to the proper usage and restri-… limitations of ScriptableObjects. I now have a much better appreciation for how they are best utilized, at least for me:
To define a type of entity in the game, whether it be tangible like “Gun” or “Room,” or conceptual like “Job” or “Riddle.”
  • As a container for a set of related fields, like global enemy stats, or prefabs, such as different color versions of the same visual effect, or a list of possible audio effects to cycle through.
  • So what should I have done here? Well, it’s what I’ve finally gotten around to doing in SCUM, and the main advice I would give to either of the two of you that are still with me:
By default, leave the details of how a thing should behave in the definition of that thing.
Code for your current and known future needs. Do not complicate your design to account for unknown or possible future situations. You aren’t gonna need it.
Remember the blue line in that last diagram? Following this guidance, everything below that line disappears.
Does this mean subclassed SO’s are bad? Of course not. If I returned to Rogue Tactics, Job would still be a ScriptableObject. I would just subclass Job directly, and those subclasses would be responsible for their differences in behavior. I’d end up with this:
Job and Ability become abstract ScriptableObjects, and gain the required method IsUnlocked(Character), which does all the checks that were once in modular Restrictions on a per-Job/per-Ability basis. Ability also gains Execute(ActionContext), which performs everything once handled by combining Effects.
However, this doesn’t mean that every Job needs its own class. One recurring case is second-tier Jobs that are unlocked once you achieve level three in a starting Job. Recruits become Soldiers. Adepts become Psions. Technicians become Mechanics. The only differences are the starting Job required and their Abilities. Do I need separate classes for Soldier, Psion, and Mechanic? Of course not.
[CreateAssetMenu(menuName = "Job/SecondTier")] public class SecondTierJob : Job { public Job RequiredJob; public override bool IsUnlocked(Character character) { var jobProgress = character.UnlockedJobs.Find( prog => prog.BaseJob == RequiredJob); return jobProgress != null && jobProgress.Level >= 3; } }
And this is without extracting that Job/level requirement further into the Job base class, since more Jobs will require that in combination with other things down the line. This gives me this final diagram (excluding Character & JobProgress, which haven’t changed):
You can imagine similar situations with Ability. There might be a dozen Abilities in the game that damage an Enemy Character and inflict some kind of stat penalty or ailment; they can all be instances of one DamageAndAfflict subclass of Ability. And so on.
Was all that work above really worth not having to do this a few dozen times? Hell-to-the-no. THIS is how ScriptableObjects should be used. They don’t need to be Frankensteined together from dozens of tiny parts. Each one just needs to know what it’s supposed to do.
As for my initial question — “what if you want instances of a ScriptableObject to behave differently?”, this is my answer following those tenets above: subclassing a ScriptableObject with different behavior is fine. Just don’t get too fine-grained in splitting it up just to make sure you can maybe reuse some parts later, or you’ll make more of a mess than you save yourself.
It really is that simple, and I’m frankly embarrassed it took me as long as it did to figure these things out in practice, even if I’ve known them in theory since college.
If at any point in reading this you’ve drawn a parallel between my design and yours, and you’ve been running into trouble getting the pieces to fit, well… Reusability is great, but it’s just a principle. Some published games make good use of it, others might not. But you know what all those published games have in common? Their developers (hopefully) know how they freaking work.
Until next time, DZ

About the Author

Dane Zeke Liergaard graduated with a B.S. in Computer Science from the University of Wisconsin, Madison in August 2011. That same month he moved across the country to Seattle with everything he owned, including his cat, packed into a 2004 Chevy Aveo. Dane worked for Amazon, Inc. from 2011 to 2016, transitioning between Customer Service/Telephony infrastructure into Amazon Smile in 2014.
In May 2016, he took a job at Google’s office in Venice Beach, CA, working on YouTube’s subscriptions architecture. In September 2017, after ~1.25 years in LA (more than enough) he transferred to Google Seattle. During this transition, he also changed title from SWE (Software Engineer) to DPE (Developer Programs Engineer). For a great explanation of what a DPE is and does, check out this article by Dane’s colleague @fhinkel.
Developer Programs Engineer — Say What!?
dzlier@google.com 5ive Bullets Unity Publisher Page @DZLiergaard @5iveBullets Work github profile Personal github profile

Other Projects
5ive Bullet Games
5ive Bullet Games - Designer
2
Comments