Post feature RSS Dev-Log #01: Designing a Stats system

A quick overview of the Stats system in our game, how we designed it and what we learned along the way.

Posted by on

When we started designing OrbWars, it quickly became clear that we wanted our characters to be able to develop over the course of the game and we also wanted to implement a few status effects.
We needed a Stats system, a "simple" one.


We didn't want to make the game *about* the stats. In fact, we decided to hide most of the number-crunching that goes on behind the scenes from the player.
All that the player needs to care about are what we decided to call **Meta-Stats** and there are only three of them.
A Meta-Stat is technically a stat in itself, but mostly functions as a collection of associated stats that are influenced by the Meta-Stat's value.
For example, our Orb Warriors have a Meta-Stat called *Power* that contains the stats *Physical Damage*, *Elemental Damage* and *Impact*. When an Orb Warrior increases its Power, this also modifies the three associated stats.

Modifiers, you say?

Every stat in our game consists of a base value and a set of modifiers. Together, they are used to calculate the actual value of the stat.
Modifiers come in 4 arithmetic varieties: Add, Subtract, Percentage Increase and Percentage Decrease.
This way, the modifier effects are always explicitely positive or negative, which minimizes errors on that front.
Percentage Increase / Decrease are multiplicative effects and - using some PropertyDrawer fairy dust - can be entered in an intuitive way.

StatModifier example

For the actual calculation, modifiers are combined into an additive and a multiplicative value and then applied to the base value, starting with the additive effects.
Note that multipliers are added together and not multiplied with each other, which seems to be the norm for treating these types of boosts in video games. This is to prevent exponential growth of these values, which can get out of hand quickly.
To give an example, say a stat has 5 modifiers that each increase the stat by 30%. In our implementation these are added for a total boost of 150%, so we multiply our base value by 2.5.
In a multiplicative implementation, the same multipliers would amount to 3.7.
The related code snippet (simplified) looks like this:

CalculateValue (simplfied)

Cool, how do you work with this?

If you're anything like me, you've seen these Unite conference talks from 2016 and 2017 about ScriptableObjects.
They are quite interesting and worth a watch. Most of all, they make you excited to implement things using ScriptableObjects. It will be neat and tidy and the world will be a better place.

When implementing the first draft of our Stats system, we took that concept and ran with it.
Stat: ScriptableObject, StatModifer: ScriptableObject, StatType: ScriptableObject, StateCollection: you get the idea...
And it worked! But it wasn't nice to work with. It is cumbersome to assign references and set data in dozens of ScriptableObjects and it quickly bloats your project structure.

The most gruelling thing to work with, was using ScriptableObjects as StatTypes. The idea is to have a ScriptableObject type that doesn't hold any special data or logic, but simply functions as a unique identifier.
It is an alternative to enums, that is based on assets rather than code.
This is actually quite nice when working in the editor. You get nice object fields which you can assign and the little popup only shows relevant assets. Adding a new type to this is as simple as creating a new ScriptableObject asset. Neat, right?

The problems arise when trying to work with this system in code.
In our case, we wanted to be able to easily read value from the StatCollection. But if we can identify a stat only through its StatType and if the StatType is a ScritableObject, we first need a reference to said ScriptableObject.
How do you obtain the reference to a ScriptableObject through code? You can create a serialized field and assign it in the editor, you can use Resources.Load, you can write a (static) switch somewhere, you can have all assets in some collection somewhere, maybe in another ScriptableObject.
None of these options are attractive, all of these options create extra work and dependencies and finally, none are as elegant and simple as using an enum.

Glorious serializable class revolution

So mid-development we revised the system. A lot of previous work went out the window, including some neat editor scripts, but the result was worth it.
There are no longer any ScriptableObjects involved in our stat system (except for the option of defining a StatModifier wrapped in a ScriptableObject, so it can be used in multiple places).
Instead, we have a StatCollection MonoBehaviour which we can add to all entities in the game that should have stats. The StatCollection has a List of stats (and Meta-Stats) that can be configured right on the MonoBehaviour.

With this system, organizing stats in the editor and accessing and modifying stats in code has become much more managable.
To make stat access extra neat, we added an array indexer to the StatCollection that uses the StatType:


Going through nested arrays of Meta-Stats, Stats and Modifiers in the inspector can still be a little confusing.
So we added a foldable info element at the top of the StatCollection, that presents all data in a more concise way and makes it easy to access relevant info about the StatCollection from inside the editor.

StatCollection overview

So what's next?

Our game features online multiplayer, so we needed a way of syncing all this data over the network. There really wasn't any neat way to do this with the ScriptableObject approach.
Using serializable classes / structs didn't make this trivial, but that is a topic for another day.

Post a comment
Sign in or join with:

Only registered members can share their thoughts. So come on! Join the community today (totally free - or sign in with your social account on the right) and join in the conversation.