[Tutorial] Character Stats (aka Attributes) System

What up fellow devs, hope all is well, I’d like to share with you an implementation for a sats (aka attributes) system.
Below, you’ll find the tutorial in both video and text formats.

DOWNLOAD THE ASSET (it’s free!):
https://assetstore.unity.com/packages/tools/integration/character-stats-106351

Video version:
Chapter 1

Chapter 2

Chapter 3

Chapter 1:

Final Fantasy stat sheet.

Stats like Strength, Intelligence, Dexterity, have become staples in many games. And when we’re getting +5 Strength from an item, -10% from a debuff and +15% from a talent, it’s nice to have a system that keeps track of all those modifiers in an organized fashion.

Before starting, gotta give credit where credit is due, my implementation was heavily inspired by this article. It’s a Flash tutorial with all code written in ActionScript, but in any case, the concept is great.

For the basics of this system we need a class that represents a stat, with a variable for the base stat value, and a list to store all the individual modifiers that have been applied to that stat. We also need a class to represent stat modifiers:

using System.Collections.Generic;

public class CharacterStat
{
    public float BaseValue;
 
    private readonly List<StatModifier> statModifiers;

    public CharacterStat(float baseValue)
    {
        BaseValue = baseValue;
        statModifiers = new List<StatModifier>();
    }
}
public class StatModifier
{
    public readonly float Value;

    public StatModifier(float value)
    {
        Value = value;
    }
}

What’s readonly?

When a variable is declared as readonly, it can only be assigned a value when it’s declared, or inside the constructor of its class. The compiler throws an error otherwise.

This prevents us from unintentionally doing things like

statModifiers = new List<StatModifier>();
statModifiers = null;

at some other point in the code.

If at any moment we need a fresh empty list, we can always just call statModifiers.Clear().

Note how none of these classes derive from MonoBehaviour. We won’t be attaching them to game objects.

Even though stats are usually whole numbers, I’m using floats instead of ints because when applying percentage bonuses, we can easily end up with decimal numbers. This way, the stat exposes the value more accurately, and then we can do whatever rounding we see fit. Besides, if we actually do want a stat that is not a whole number, it’s already covered.

Now we need to be able to add and remove modifiers to/from stats, calculate the final value of the stat (taking into account all those modifiers) and also fetch the final value. Let’s add this to the CharacterStat class:

public float Value { get { return CalculateFinalValue(); } }

public void AddModifier(StatModifier mod)
{
    statModifiers.Add(mod);
}

public bool RemoveModifier(StatModifier mod)
{
    return statModifiers.Remove(mod);
}

private float CalculateFinalValue()
{
    float finalValue = BaseValue;

    for (int i = 0; i < statModifiers.Count; i++)
    {
        finalValue += statModifiers[i].Value;
    }
    // Rounding gets around dumb float calculation errors (like getting 12.0001f, instead of 12f)
    // 4 significant digits is usually precise enough, but feel free to change this to fit your needs
    return (float)Math.Round(finalValue, 4);
}

We’ll also need to add this outside the class curly braces (right at the top of the script):

using System;

If you’re like me, the fact that we’re calling CalculateFinalValue() every single time we need the stat value is probably bugging you. Let’s avoid that by making the following changes:

// Add these variables
private bool isDirty = true;
private float _value;

// Change the Value property to this
public float Value {
    get {
        if(isDirty) {
            _value = CalculateFinalValue();
            isDirty = false;
        }
        return _value;
    }
}

// Change the AddModifier method
public void AddModifier(StatModifier mod)
{
    isDirty = true;
    statModifiers.Add(mod);
}

// And change the RemoveModifier method
public bool RemoveModifier(StatModifier mod)
{
    isDirty = true;
    return statModifiers.Remove(mod);
}

Great, we can add “flat” modifiers to our stats, but what about percentages? Ok, so that means there’s at least 2 types of stat modifiers, let’s create an enum to define those types:

public enum StatModType
{
    Flat,
    Percent,
}

You can either put this in a new script or in the same script as the StatModifier class (outside the class curly braces for easier access).

We need to change our StatModifier class to take these types into account:

public class StatModifier
{
    public readonly float Value;
    public readonly StatModType Type;

    public StatModifier(float value, StatModType type)
    {
        Value = value;
        Type = type;
    }
}

And change our CalculateFinalValue() method in the CharacterStat class to deal with each type differently:

private float CalculateFinalValue()
{
    float finalValue = BaseValue;

    for (int i = 0; i < statModifiers.Count; i++)
    {
        StatModifier mod = statModifiers[i];

        if (mod.Type == StatModType.Flat)
        {
            finalValue += mod.Value;
        }
        else if (mod.Type == StatModType.Percent)
        {
            finalValue *= 1 + mod.Value;
        }
    }
    // Rounding gets around dumb float calculation errors (like getting 12.0001f, instead of 12f)
    // 4 significant digits is usually precise enough, but feel free to change this to fit your needs
    return (float)Math.Round(finalValue, 4);
}

Why is the percentage calculation so weird?

Let’s say we have a value of 20 and we want to add +10% to that.

With this in mind, that weird line of code could be written like this:

finalValue += finalValue * mod.Value;

However, since the original value is always 100%, and we want to add 10% to that, it’s easy to see that would make it 110%.

This works for negative numbers too, if we want to modify by -10%, it means we’ll be left with 90%, so we multiply by 0.9.

Now we can deal with percentages, but our modifiers always apply in the order they are added to the list. If we have a skill or talent that increases our Strength by 15%, if we then equip an item with +20 Strength after gaining that skill, those +15% won’t apply to the item we just equipped. That’s probably not what we want. We need a way to tell the stat the order in which modifiers take effect.
Let’s do that by making the following changes to the StatModifier class:

// Add this variable to the top of the class
public readonly int Order;

// Change the existing constructor to look like this
public StatModifier(float value, StatModType type, int order)
{
    Value = value;
    Type = type;
    Order = order;
}

// Add a new constructor that automatically sets a default Order, in case the user doesn't want to manually define it
public StatModifier(float value, StatModType type) : this(value, type, (int)type) { }

What the hell is up with that constructor?

In C#, to call a constructor from another constructor, you essentially “extend” the constructor you want to call.
In this case we defined a constructor that needs only the value and the type, it then calls the constructor that also needs the order, but passes the int representation of type as the default order.

How does (int)type work?
In C#, every enum element is automatically assigned an index. By default, the first element is 0, the second is 1, etc. You can assign a custom index if you want, but we don’t need to do that…yet. If you hover your mouse over an enum element, you can see the index of that element in the tooltip (at least in Visual Studio).
In order to retrieve the index of an enum element, we just cast it to int.

With these changes, we can set the order for each modifier, but if we don’t, flat modifiers will apply before percentage modifiers. So by default we get the most common behavior, but we can also do other things, like forcing a special modifier to apply a flat value after everything else.

Now we need a way to apply modifiers according to their order when calculating the final stat value. The easiest way to do this is to sort the statModifiers list whenever we add a new modifier. This way we don’t need to change the CalculateFinalValue() method because everything will already be in the correct order.

// Change the AddModifiers method to this
public void AddModifier(StatModifier mod)
{
    isDirty = true;
    statModifiers.Add(mod);
    statModifiers.Sort(CompareModifierOrder);
}

// Add this method to the CharacterStat class
private int CompareModifierOrder(StatModifier a, StatModifier b)
{
    if (a.Order < b.Order)
        return -1;
    else if (a.Order > b.Order)
        return 1;
    return 0; // if (a.Order == b.Order)
}

How do Sort & CompareModifierOrder work?

Sort() is a C# method for all lists that, as the name implies, sorts the list. The criteria it uses to sort the list should be supplied by us, in the form of a comparison function. If we don’t supply a comparison function, it uses the default comparer (whatever that does).

The comparison function will be used by the Sort() method to compare pairs of objects in the list. For each pair of objects there’s 3 possible situations:

  1. The first object (a) should come before the second object (b). The function returns -1.
  2. The first object should come after the second. The function returns 1.
  3. Both objects are equal in “priority”. The function returns 0.

In our case, the comparison function is CompareModifierOrder().

Chapter 1 ends here, we already have a pretty good basis for a stat system. Feel free to take a break and/or pat yourself on the back for reaching this point :slight_smile:

Chapter 2:

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:

public enum StatModType
{
    Flat,
    PercentAdd, // Add this new type.
    PercentMult, // Change our old Percent type 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:

private float CalculateFinalValue()
{
    float finalValue = BaseValue;
    float sumPercentAdd = 0; // This will hold the sum of our "PercentAdd" modifiers

    for (int i = 0; i < statModifiers.Count; i++)
    {
        StatModifier mod = statModifiers[i];

        if (mod.Type == StatModType.Flat)
        {
            finalValue += mod.Value;
        }
        else if (mod.Type == StatModType.PercentAdd) // When we encounter a "PercentAdd" modifier
        {
            sumPercentAdd += mod.Value; // Start adding together all modifiers of this type

            // If we're at the end of the list OR the next modifer isn't of this type
            if (i + 1 >= statModifiers.Count || statModifiers[i + 1].Type != StatModType.PercentAdd)
            {
                finalValue *= 1 + sumPercentAdd; // Multiply the sum with the "finalValue", like we do for "PercentMult" modifiers
                sumPercentAdd = 0; // Reset the sum back to 0
            }
        }
        else if (mod.Type == StatModType.PercentMult) // Percent renamed to PercentMult
        {
            finalValue *= 1 + mod.Value;
        }
    }

    return (float)Math.Round(finalValue, 4);
}

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

public readonly float Value;
public readonly StatModType Type;
public readonly int Order;
public readonly object Source; // Added this variable

// "Main" constructor. Requires all variables.
public StatModifier(float value, StatModType type, int order, object source) // Added "source" input parameter
{
    Value = value;
    Type = type;
    Order = order;
    Source = source; // Assign Source to our new input parameter
}

// Requires Value and Type. Calls the "Main" constructor and sets Order and Source to their default values: (int)type and null, respectively.
public StatModifier(float value, StatModType type) : this(value, type, (int)type, null) { }

// Requires Value, Type and Order. Sets Source to its default value: null
public StatModifier(float value, StatModType type, int order) : this(value, type, order, null) { }

// Requires Value, Type and Source. Sets Order to its default value: (int)Type
public StatModifier(float value, StatModType type, object source) : this(value, type, (int)type, source) { }

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:

public class Item // Hypothetical item class
{
    public void Equip(Character c)
    {
        // We need to store our modifiers in variables before adding them to the stat.
        mod1 = new StatModifier(10, StatModType.Flat);
        mod2 = new StatModifier(0.1, StatModType.Percent);
        c.Strength.AddModifier(mod1);
        c.Strength.AddModifier(mod2);
    }

    public void Unequip(Character c)
    {
        // Here we need to use the stored modifiers in order to remove them.
        // Otherwise they would be "lost" in the stat forever.
        c.Strength.RemoveModifier(mod1);
        c.Strength.RemoveModifier(mod2);
    }
}

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:

public bool RemoveAllModifiersFromSource(object source)
{
    bool didRemove = false;

    for (int i = statModifiers.Count - 1; i >= 0; i--)
    {
        if (statModifiers[i].Source == source)
        {
            isDirty = true;
            didRemove = true;
            statModifiers.RemoveAt(i);
        }
    }
    return didRemove;
}

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

public class Item
{
    public void Equip(Character c)
    {
        // Create the modifiers and set the Source to "this"
        // Note that we don't need to store the modifiers in variables anymore
        c.Strength.AddModifier(new StatModifier(10, StatModType.Flat, this));
        c.Strength.AddModifier(new StatModifier(0.1, StatModType.Percent, this));
    }

    public void Unequip(Character c)
    {
        // Remove all modifiers applied by "this" Item
        c.Strength.RemoveAllModifiersFromSource(this);
    }
}

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.

public bool RemoveModifier(StatModifier mod)
{
    if (statModifiers.Remove(mod))
    {
        isDirty = true;
        return true;
    }
    return false;
}

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:

using System.Collections.ObjectModel; // Add this using statement

public readonly ReadOnlyCollection<StatModifier> StatModifiers; // Add this variable

public CharacterStat(float baseValue)
{
    BaseValue = baseValue;
    statModifiers = new List<StatModifier>();
    StatModifiers = statModifiers.AsReadOnly(); // Add this line to the constructor
}

The ReadOnlyCollection stores a reference to the original List and prohibits changing it. However, if you modify the original statModifiers (lowercase s), then the StatModifiers (uppercase S) will also change.

To finish up chapter 2, I’d like to add just two more things. We left the BaseValue as public, but if we change its value it won’t cause the Value property to recalculate. Let’s fix that.

// Add this variable
private float lastBaseValue = float.MinValue;

// Change Value
public float Value {
    get {
        if(isDirty || lastBaseValue != BaseValue) {
            lastBaseValue = BaseValue;
            _value = CalculateFinalValue();
            isDirty = false;
        }
        return _value;
    }
}

The other thing is in the StatModType enum. Let’s override the default “indexes” like so:

public enum StatModType
{
    Flat = 100,
    PercentAdd = 200,
    PercentMult = 300,
}

The reason for doing this is simple - if someone wants to add a custom Order value for some modifiers to 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.

Chapter 3:

This is gonna be a short one, the only thing left to do is make the classes more easily extendable. To do that, we’ll change all private variables, properties and methods to protected, in addition to making all properties and methods virtual.

Only the CharacterStat script needs changes, and I’m showing only the lines that need to be changed:

protected bool isDirty = true;
protected float lastBaseValue;
protected float _value;
public virtual float Value {}

protected readonly List<StatModifier> statModifiers;

public virtual void AddModifier(StatModifier mod);
public virtual bool RemoveModifier(StatModifier mod);
public virtual bool RemoveAllModifiersFromSource(object source);

protected virtual int CompareModifierOrder(StatModifier a, StatModifier b);
protected virtual float CalculateFinalValue();

Let’s also mark the CharacterStat class as [Serializable], so that we can actually edit it from the Unity inspector.

[Serializable]
public class CharacterStat

And on that note, we also need to implement a parameterless constructor for CharacterStat, otherwise we’re gonna get a null reference exception due to statModifiers not being initialized.
Let’s change our original constructor and add the new one:

public CharacterStat()
{
    statModifiers = new List<StatModifier>();
    StatModifiers = statModifiers.AsReadOnly();
}

public CharacterStat(float baseValue) : this()
{
    BaseValue = baseValue;
}

Other than that, I’ve also put both classes inside a namespace:

namespace Kryz.CharacterStats
{
    [Serializable]
    public class CharacterStat
    {
        //...
    }
}
namespace Kryz.CharacterStats
{
    public enum StatModType
    {
        //...
    }

    public class StatModifier
    {
        //...
    }
}

You don’t need to do this if you don’t want to, but I like it for organization reasons.

I will be reading and replying to every comment in this thread, so 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!

37 Likes

Thanks this is great.

I was just looking at how to solve this.

1 Like

Way more clear and straightforward than the link to the other tutorial.

1 Like

Thank you guys so much for your kind words :slight_smile:

Btw, I just uploaded the video version of this tutorial. Also edited the original post with the link.

Apologies for the slight spam, just wanna let people know that I’ve edited the original post with chapter 2 of the tutorial. Hope you enjoy, and feel free to post your thoughts down here, I’ll be reading and replying to every comment :slight_smile:

Great stuff! Really liked the videos, you explained things really well.

Is this up on the Asset Store? I don’t see any links. Cheers!

Hey there, glad you liked it! I’ve already submitted the package to the asset store, but according to Unity, they’re having a large volume of submissions so it’s taking longer than usual to get approval. As soon as it’s up I’ll update the post with links.

Thank you very much for posting this. You clearly put a lot of effort into it, and it’s really educational. I had actually read the Flash-oriented article you mentioned, but the code translation was a bother and I gave up on it.

Great work!

Edit: I’ve been working to extend this so that it can represent other kinds of stats, like Resources (health, mana) which have a MaxValue. It occurred to me that Skills (swords, swimming, blacksmithing) could function very similarly: a ranged value that you check against to determine something else. In a simple scenario you could just use the same class for a skill or a health stat; it would be modifiable, etc. The only difference is presentation in the GUI.

I also wanted to be able to tell a modifier to apply to a ranged value in more ways.

  • Apply to the current value as a percentage of the MaxValue (heal for 10% of your max health)
  • As a buff/debuff:
  • Apply to the MaxValue (increases max health by 10 points)
  • Apply to the MaxValue as a percent of the MaxValue (increases max health by 10%)

I had to split it into two somewhat redundant classes: Stat (which has no max) and Resource (which does), and then the associated StatModifier and ResourceModifier. Ideally I’d use inheritance for this, but I hear a lot about Unity’s issues with subclass serialization, especially if you want to make it a scriptable object with editor support.

Furthermore

I wanted to have modifications be applied from a ModifierGroup object, which would represent a spell or buff/debuff, status effect, etc. It would host the collection of modifiers which make up the total effect, and probably have some kind of timing model for application (Burst, OverDuration, PerTickForDuration, PerTickUntilCancel, etc.). Like: a spell that initially heals for 10% of your max health, then 1% thereafter each second for 10 seconds. Pretty simple with this system.

Anyway, if I get it fleshed out more, and you’re interested, I could upload the results.

3 Likes

@FuriantMedia Thank you very much for your kind words! I’m really happy you found this useful.
Also, damn, great work extending this. That is exactly the type of thing I wanted to facilitate with this system!

I really wanted to make it simple, powerful and easy to expand upon, and even if the code didn’t turn out like that, at least I hoped the idea would. Mission accomplished, I guess :smile:

Looks like a pretty neat approach! Kudos for making something free for the community =)

Not sure how I missed this (maybe its too recent?), I couldn’t find hardly anything on the topic so I ended up making an asset published the store that does Character Stats - it takes a little different approach and offers min/max and affinities for leveling stats.

@LaneFox Oh that’s cool, I’m assuming that’s the “Statinator” link in your signature. Your approach seems quite good with the ScriptableObjects and referencing stats with enum values. Using enums is actually something I was wanting to do too, but never got around to it. Maybe I’ll get back to that after finishing some other tutorial series :slight_smile:

If you don’t mind me asking, how do you deal with that in your asset? Do you just have a script with an enum that users need to edit? Or do you have some kind of editor window for that? What happens to it when users update the asset to a new version?

Using the Enum has its own caveats, but personally I think it’s worth it. There’s other ways to get the similar results but the enum is pretty straightforward. There’s a separate script I have that stores the enum for the stats, it looks like this:

    public enum StatType { Health, Mana, Level, Experience, ExpReward, Agility, Dexterity, Endurance, Strength, RegenHp, RegenMp }

And thats all it contains. At first it was kinda built into core but I’m separating it in its own file out of the /core/ folder so it’s easier to maintain for users through updates.

The other enums look like this.

    public enum StatProperty { Value, Base, Min, Max, Affinity, MaxAffinity }
    public enum StatModType { Active, Direct }
    public enum StatModEffect { Add, Multiply }

And the stat class does all this wonderful magical stuff to manage everything so you can make calls like this:

        public virtual void LevelUp()
        {
            Stats[(int) StatType.Level].AddToRoot(StatProperty.Value, 1);
        }

Accessing them with the int cast isn’t the prettiest (my biggest issue of doing it this way) but it’s a fast index pointer, doesn’t use strings and works great. Here’s another example.

        public virtual void AddExp(float amount = 1000)
        {
            if (GetStatValue(StatType.Experience) + amount >= GetStatMax(StatType.Experience))
            {
                float excess = amount - (GetStatMax(StatType.Experience) - GetStatValue(StatType.Experience));
                LevelUp();
                Stats[(int) StatType.Experience].SetRoot(StatProperty.Value, excess);
            }
            else Stats[(int) StatType.Experience].AddToRoot(StatProperty.Value, amount);
        }

In a different asset I was making I used code generation and a custom editor to create new Stats and regenerate a new .cs file which also worked super good and had a nice UX value but I decided to go without it for this since adding new Stats (in the enum) is more or less a one time thing done early in the project.

Nice, thanks for that write up! Looks pretty cool and pleasant to use. I definitely appreciate that you avoid strings.
I’ve also thought about code generation somewhat like that, so it’s good to know it works, but I still have a lot to learn about editor scripting before trying that :stuck_out_tongue:

1 Like

Hey guys, just wanna let everyone know that Unity has finally approved the asset version of this system!

Here’s the link to the asset store page: Unity Asset Store - The Best Assets for Game Making

And a “how to use” video!

6 Likes

@Kryzarel ,

This is excellent. Thank you so much for sharing and for free. I really like the source Object idea. I was wondering though whether it might not be useful to have a new name string and a description string in the StatModifier(s)?

Just saw this now. Already answered in the video, so np :stuck_out_tongue:

1 Like

@Kryzarel Great tutorial! I learned a lot, thank you very much!

I’m currently working on a ‘stats system’ too, my idea is basically the same as yours, but I do found some problems:

  1. StatType
    I really don’t think it’s a good idea to use enum for StatTypes, because you never know when you want to add or remove types, it might be before you actually make your game, it might be in the progress of you making your game, and even some other day after you have published your game…
    And also, different games need different collection of stat types.

here is my current solution:

//Make the StatType as a ScriptableObject
public class StatType : ScriptableObject
{
    public string typeName; // only a string of typeName here
    // you can add this ID field if you want to
    // public int typeId; 
}

And in the Stat and StatModifier class, they have a StatType field

public class StatModifier
{
    public StatType targetType;
    // etc...
}
public class Stat : ScriptableObject
{
    public StatType type;
    // etc...

    // Make it return bool so you know whether the StatType is matched
    public bool AddModifier(StatModifier newModifier)
    {
        if (newModifier.targetType != type) return false;
        // do mod value things...
        return true;
    }

    // also in Remove    
    public bool RemoveModifier(StatModifier modifierToRemove)
    {
        if (modifierToRemove.targetType != type) return false;
        // etc...
        return true;
    }
}

And a StatTypeList class, manage all the StatTypes of the game.
Since it’s an asset, you can have different set of stat types in your game

public class StatTypeList : ScriptableObject
{
    public StatType[] data;  // just a collection of StatTypes here
}

And the CharacterStats class, a character may have many stats…

// I used some new language features of C# 6 here
public class CharacterStats : ScriptableObject
{
    public Stat[] stats; // I use array so it's more easy to write editor script

    // use indexer to get matched type stat... 
    // I'm not sure it's good or not
    // you might need HashSet or Dictionary to speed up the search
    public Stat this[StatType type] => Array.Find(stats, s => s.type == type);

    public bool AddModifier(StatModifier modifier) =>
        this[modifier.targetType]?.AddModifier(modifier) ?? false;

    public bool RemoveModifier(StatModifier modifier) =>
        this[modifier.targetType]?.RemoveModifier(modifier) ?? false;
}

With Editor scripts, it’ll be more convenient to manage these assets in Editor,
This is how the StatTypeList asset looks like in the project window:
3400033--267616--upload_2018-2-22_13-4-49.png
and in Inspector:
3400033--267617--upload_2018-2-22_13-7-54.png

And the CharacterStats(collection of stats) asset:
3400033--267618--upload_2018-2-22_13-11-20.png
(each stat here can be modified in inspector)

3400033--267619--upload_2018-2-22_13-12-18.png
(the Name of ‘New Stat’ is just the asset name of it)

  1. You may noticed that I make all of them, except StatModifier, as ScriptableObject, ScriptableObject helps a lot on the data persistent and UI binding things, but it also introduce some problems:

I’m wondering how to deal with the Enemy Stat…
Each type of enemy (say goblin, slime, etc…) need a list of stats (if there are many types, the amount of assets will be huge, although we can make Editor scripts to create them automatically), and we can’t change the modifiers directly from their stats (because all the enemies of a same type uses the same stats asset), so we have to deal with it in the enemy’s MonoBehavoiur class…

And the items and equipments that modify stats…

Maybe just do not use ScriptableObject for CharacterStats?

4 Likes

@Kuuo Your approach is very very interesting, you do bring up some excellent points! If I ever expand on this system I’ll definitely keep your post in mind.
The reason for enums is because it’s the easiest thing to edit by anyone and also avoids string comparisons at run time (and typos), but your implementation solves that elegantly by using asset references instead, awesome :smile:

As for the problem you presented, shouldn’t the stat list for each enemy be something that you define independently in each enemy prefab? Each enemy should have its own instance of the stat list. I might not be getting the entire picture just from your post, but that would seem like the more sane approach to take.

@Kryzarel Thank you! I’m still thinking about how to solve that problem.

So now what in my mind is that

  1. the stat assets only store the default stat of the character (both player and enemy)
  2. the stat modifier should not be stored in the assets, but in the MonoBehavoiur script that each character attached, so that each character can just care about their own stats

As for using assets as enums, this idea is from these 2 awesome talks:

  1. Unite 2016 - Overthrowing the MonoBehaviour Tyranny in a Glorious Scriptable Object Revolution
  2. Unite Austin 2017 - Game Architecture with Scriptable Objects

You should definitely check them out~;)

1 Like

@Kuuo I had already seen those talks, but I kind of forgot a lot about them in the meantime. But now that you mention them I do start to remember. Definitely gonna have to watch them again sometime, they’re really interesting :smile: