Character Stats/Attributes in Unity (pt. 2) — Additive Modifiers & Optimizations
This is part 2 of a tutorial on character stats. Check out part 1 here:
There’s also a video version of this tutorial if you prefer that:
Right now our percentage modifiers stack multiplicatively with each other, i.e., if we add two 100% modifiers to a stat, we won’t get 200%, we’ll get 400%. Because the first will double our original value (going from 100% to 200%), and the second will double it again (going from 200% to 400%).
But what if we would like to have certain modifiers stack additively? Meaning that the previous example would result in a 200% bonus instead of 400%.
Let’s add a third type of modifier by changing our StatModType
enum
to this:
Don’t forget to change Percent
to PercentMult
in the CalculateFinalValue()
method (inside the CharacterStat
class). Or just use Visual Studio’s renaming features to do it for you ^^.
Inside the CalculateFinalValue()
method, we need to add a couple of things to deal with the new type of modifier. It should now look like this:
This time the calculation gets pretty weird. Basically, every time we encounter a PercentAdd
modifier, we start adding it together with all modifiers of the same type, until we encounter a modifier of a different type or we reach the end of the list. At that point, we grab the sum of all the PercentAdd
modifiers and multiply it with finalValue
, just like we do with PercentMult
modifiers.
For the next bit, let’s add a Source
variable to our StatModifier
class. This way, later on when we actually have stuff in our game that adds modifiers (like items and spells), we’ll be able to tell where each modifiers came from.
This could be useful both for debugging and also to provide more information to the players, allowing them to see exactly what is providing each modifier.
The StatModifier
class should look like this:
Let’s say we equipped an item that grants both a flat +10 and also a +10% bonus to Strength. The way the system works currently, we have to do something like this:
But now that our modifiers have a Source
, we can do something useful in CharacterStat
— we can remove all modifiers that have been applied by a certain Source
at once. Let’s add a method for that:
Why is the
for
loop in reverse?
To explain this let’s look at what happens when we remove the first object from a list vs when we remove the last object:Let’s imagine we have a list with 10 objects, when we remove the first one, the remaining 9 objects will be shifted up. That happens because index 0 is now empty, so the object at index 1 will move to index 0, the object at index 2 will move to index 1, and so on. As you can imagine, this is quite inefficient.
However, if we remove the last object, then nothing has to be shifted. We just remove the object at index 9 and everything else stays the same.
This is why we do the removal in reverse. Even if the objects that we need to remove are in the middle of the list (where shifts are inevitable), it’s still a good idea to traverse the list in reverse (unless your specific use case demands otherwise). Any time more than one object is removed, doing it from last to first always results in less shifts.
And now we can add and remove our (hypothetical) item’s modifiers like so:
Since we’re talking about removing modifiers, let’s also “fix” our original RemoveModifier()
method. We really don’t need to set isDirty
every single time, just when something is actually removed.
We also talked about letting players see the modifiers, but the statModifiers
list is private. We don’t want to change that because the only way to safely modify it is definitely through the CharacterStat
class. Fortunately, C# has a very useful data type for these situations: ReadOnlyCollection
.
Make the following changes to CharacterStat
:
The ReadOnlyCollection
stores a reference to the original List
and prohibits changing it. However, if you modify the original statModifers
(lowercase “s”), then the StatModifiers
(uppercase “S”) will also change.
To finish up part 2, I’d like to add just two more things. We left the BaseValue
as public, but if we change it, it won’t cause the Value
property to recalculate. Let’s fix that.
The other thing is in the StatModType
enum
. Let’s override the default “indexes” of the enum
values, like so:
The reason for doing this is simple — if someone wants to add a custom Order
value to make some modifiers sit in the middle of the default ones, this allows a lot more flexibility.
If we want to add a Flat
modifier that applies between PercentAdd
and PercentMult
, we can just assign an Order
anywhere between 201 and 299.
Before we made this change, we’d have to assign custom Order
values to all PercentAdd
and PercentMult
modifiers too.
And that’s it for part 2. Don’t hesitate to drop a comment if you have any questions, suggestions or feedback.
I hope this has been helpful, and goodbye for now!