Search Unity

  1. Get all the Unite Berlin 2018 news on the blog.
    Dismiss Notice
  2. Unity 2018.2 has arrived! Read about it here.
    Dismiss Notice
  3. Improve your Unity skills with a certified instructor in a private, interactive classroom. Learn more.
    Dismiss Notice
  4. ARCore is out of developer preview! Read about it here.
    Dismiss Notice
  5. Magic Leap’s Lumin SDK Technical Preview for Unity lets you get started creating content for Magic Leap One™. Find more information on our blog!
    Dismiss Notice
  6. Want to see the most recent patch releases? Take a peek at the patch release page.
    Dismiss Notice

[Tutorial] Character Stats (aka Attributes) System

Discussion in 'Community Learning & Teaching' started by Kryzarel, Nov 11, 2017.

  1. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    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://www.assetstore.unity3d.com/#!/content/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:
    Code (csharp):
    1. using System.Collections.Generic;
    2.  
    3. public class CharacterStat
    4. {
    5.     public float BaseValue;
    6.  
    7.     private readonly List<StatModifier> statModifiers;
    8.  
    9.     public CharacterStat(float baseValue)
    10.     {
    11.         BaseValue = baseValue;
    12.         statModifiers = new List<StatModifier>();
    13.     }
    14. }
    Code (csharp):
    1. public class StatModifier
    2. {
    3.     public readonly float Value;
    4.  
    5.     public StatModifier(float value)
    6.     {
    7.         Value = value;
    8.     }
    9. }
    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
    Code (csharp):
    1. statModifiers = new List<StatModifier>();
    2. 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:
    Code (csharp):
    1. public float Value { get { return CalculateFinalValue(); } }
    2.  
    3. public void AddModifier(StatModifier mod)
    4. {
    5.     statModifiers.Add(mod);
    6. }
    7.  
    8. public bool RemoveModifier(StatModifier mod)
    9. {
    10.     return statModifiers.Remove(mod);
    11. }
    12.  
    13. private float CalculateFinalValue()
    14. {
    15.     float finalValue = BaseValue;
    16.  
    17.     for (int i = 0; i < statModifiers.Count; i++)
    18.     {
    19.         finalValue += statModifiers[i].Value;
    20.     }
    21.     // Rounding gets around dumb float calculation errors (like getting 12.0001f, instead of 12f)
    22.     // 4 significant digits is usually precise enough, but feel free to change this to fit your needs
    23.     return (float)Math.Round(finalValue, 4);
    24. }
    We'll also need to add this outside the class curly braces (right at the top of the script):
    Code (csharp):
    1. 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:
    Code (csharp):
    1. // Add these variables
    2. private bool isDirty = true;
    3. private float _value;
    4.  
    5. // Change the Value property to this
    6. public float Value {
    7.     get {
    8.         if(isDirty) {
    9.             _value = CalculateFinalValue();
    10.             isDirty = false;
    11.         }
    12.         return _value;
    13.     }
    14. }
    15.  
    16. // Change the AddModifier method
    17. public void AddModifier(StatModifier mod)
    18. {
    19.     isDirty = true;
    20.     statModifiers.Add(mod);
    21. }
    22.  
    23. // And change the RemoveModifier method
    24. public bool RemoveModifier(StatModifier mod)
    25. {
    26.     isDirty = true;
    27.     return statModifiers.Remove(mod);
    28. }
    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:
    Code (csharp):
    1. public enum StatModType
    2. {
    3.     Flat,
    4.     Percent,
    5. }
    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:
    Code (csharp):
    1. public class StatModifier
    2. {
    3.     public readonly float Value;
    4.     public readonly StatModType Type;
    5.  
    6.     public StatModifier(float value, StatModType type)
    7.     {
    8.         Value = value;
    9.         Type = type;
    10.     }
    11. }
    And change our CalculateFinalValue() method in the CharacterStat class to deal with each type differently:
    Code (csharp):
    1. private float CalculateFinalValue()
    2. {
    3.     float finalValue = BaseValue;
    4.  
    5.     for (int i = 0; i < statModifiers.Count; i++)
    6.     {
    7.         StatModifier mod = statModifiers[i];
    8.  
    9.         if (mod.Type == StatModType.Flat)
    10.         {
    11.             finalValue += mod.Value;
    12.         }
    13.         else if (mod.Type == StatModType.Percent)
    14.         {
    15.             finalValue *= 1 + mod.Value;
    16.         }
    17.     }
    18.     // Rounding gets around dumb float calculation errors (like getting 12.0001f, instead of 12f)
    19.     // 4 significant digits is usually precise enough, but feel free to change this to fit your needs
    20.     return (float)Math.Round(finalValue, 4);
    21. }
    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:
    Code (csharp):
    1. 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:
    Code (csharp):
    1. // Add this variable to the top of the class
    2. public readonly int Order;
    3.  
    4. // Change the existing constructor to look like this
    5. public StatModifier(float value, StatModType type, int order)
    6. {
    7.     Value = value;
    8.     Type = type;
    9.     Order = order;
    10. }
    11.  
    12. // Add a new constructor that automatically sets a default Order, in case the user doesn't want to manually define it
    13. public StatModifier(float value, StatModType type) : this(value, type, (int)type) { }
    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.

    Code (csharp):
    1. // Change the AddModifiers method to this
    2. public void AddModifier(StatModifier mod)
    3. {
    4.     isDirty = true;
    5.     statModifiers.Add(mod);
    6.     statModifiers.Sort(CompareModifierOrder);
    7. }
    8.  
    9. // Add this method to the CharacterStat class
    10. private int CompareModifierOrder(StatModifier a, StatModifier b)
    11. {
    12.     if (a.Order < b.Order)
    13.         return -1;
    14.     else if (a.Order > b.Order)
    15.         return 1;
    16.     return 0; // if (a.Order == b.Order)
    17. }
    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 :)


    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:
    Code (csharp):
    1. public enum StatModType
    2. {
    3.     Flat,
    4.     PercentAdd, // Add this new type.
    5.     PercentMult, // Change our old Percent type to this.
    6. }
    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:
    Code (csharp):
    1. private float CalculateFinalValue()
    2. {
    3.     float finalValue = BaseValue;
    4.     float sumPercentAdd = 0; // This will hold the sum of our "PercentAdd" modifiers
    5.  
    6.     for (int i = 0; i < statModifiers.Count; i++)
    7.     {
    8.         StatModifier mod = statModifiers[i];
    9.  
    10.         if (mod.Type == StatModType.Flat)
    11.         {
    12.             finalValue += mod.Value;
    13.         }
    14.         else if (mod.Type == StatModType.PercentAdd) // When we encounter a "PercentAdd" modifier
    15.         {
    16.             sumPercentAdd += mod.Value; // Start adding together all modifiers of this type
    17.  
    18.             // If we're at the end of the list OR the next modifer isn't of this type
    19.             if (i + 1 >= statModifiers.Count || statModifiers[i + 1].Type != StatModType.PercentAdd)
    20.             {
    21.                 finalValue *= 1 + sumPercentAdd; // Multiply the sum with the "finalValue", like we do for "PercentMult" modifiers
    22.                 sumPercentAdd = 0; // Reset the sum back to 0
    23.             }
    24.         }
    25.         else if (mod.Type == StatModType.PercentMult) // Percent renamed to PercentMult
    26.         {
    27.             finalValue *= 1 + mod.Value;
    28.         }
    29.     }
    30.  
    31.     return (float)Math.Round(finalValue, 4);
    32. }
    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:
    Code (csharp):
    1. public readonly float Value;
    2. public readonly StatModType Type;
    3. public readonly int Order;
    4. public readonly object Source; // Added this variable
    5.  
    6. // "Main" constructor. Requires all variables.
    7. public StatModifier(float value, StatModType type, int order, object source) // Added "source" input parameter
    8. {
    9.     Value = value;
    10.     Type = type;
    11.     Order = order;
    12.     Source = source; // Assign Source to our new input parameter
    13. }
    14.  
    15. // Requires Value and Type. Calls the "Main" constructor and sets Order and Source to their default values: (int)type and null, respectively.
    16. public StatModifier(float value, StatModType type) : this(value, type, (int)type, null) { }
    17.  
    18. // Requires Value, Type and Order. Sets Source to its default value: null
    19. public StatModifier(float value, StatModType type, int order) : this(value, type, order, null) { }
    20.  
    21. // Requires Value, Type and Source. Sets Order to its default value: (int)Type
    22. 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:
    Code (csharp):
    1. public class Item // Hypothetical item class
    2. {
    3.     public void Equip(Character c)
    4.     {
    5.         // We need to store our modifiers in variables before adding them to the stat.
    6.         mod1 = new StatModifier(10, StatModType.Flat);
    7.         mod2 = new StatModifier(0.1, StatModType.Percent);
    8.         c.Strength.AddModifier(mod1);
    9.         c.Strength.AddModifier(mod2);
    10.     }
    11.  
    12.     public void Unequip(Character c)
    13.     {
    14.         // Here we need to use the stored modifiers in order to remove them.
    15.         // Otherwise they would be "lost" in the stat forever.
    16.         c.Strength.RemoveModifier(mod1);
    17.         c.Strength.RemoveModifier(mod2);
    18.     }
    19. }
    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:
    Code (csharp):
    1. public bool RemoveAllModifiersFromSource(object source)
    2. {
    3.     bool didRemove = false;
    4.  
    5.     for (int i = statModifiers.Count - 1; i >= 0; i--)
    6.     {
    7.         if (statModifiers[i].Source == source)
    8.         {
    9.             isDirty = true;
    10.             didRemove = true;
    11.             statModifiers.RemoveAt(i);
    12.         }
    13.     }
    14.     return didRemove;
    15. }
    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:
    Code (csharp):
    1. public class Item
    2. {
    3.     public void Equip(Character c)
    4.     {
    5.         // Create the modifiers and set the Source to "this"
    6.         // Note that we don't need to store the modifiers in variables anymore
    7.         c.Strength.AddModifier(new StatModifier(10, StatModType.Flat, this));
    8.         c.Strength.AddModifier(new StatModifier(0.1, StatModType.Percent, this));
    9.     }
    10.  
    11.     public void Unequip(Character c)
    12.     {
    13.         // Remove all modifiers applied by "this" Item
    14.         c.Strength.RemoveAllModifiersFromSource(this);
    15.     }
    16. }
    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.
    Code (csharp):
    1. public bool RemoveModifier(StatModifier mod)
    2. {
    3.     if (statModifiers.Remove(mod))
    4.     {
    5.         isDirty = true;
    6.         return true;
    7.     }
    8.     return false;
    9. }

    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:
    Code (csharp):
    1. using System.Collections.ObjectModel; // Add this using statement
    2.  
    3. public readonly ReadOnlyCollection<StatModifier> StatModifiers; // Add this variable
    4.  
    5. public CharacterStat(float baseValue)
    6. {
    7.     BaseValue = baseValue;
    8.     statModifiers = new List<StatModifier>();
    9.     StatModifiers = statModifiers.AsReadOnly(); // Add this line to the constructor
    10. }
    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.

    Code (csharp):
    1. // Add this variable
    2. private float lastBaseValue = float.MinValue;
    3.  
    4. // Change Value
    5. public float Value {
    6.     get {
    7.         if(isDirty || lastBaseValue != BaseValue) {
    8.             lastBaseValue = BaseValue;
    9.             _value = CalculateFinalValue();
    10.             isDirty = false;
    11.         }
    12.         return _value;
    13.     }
    14. }
    The other thing is in the StatModType enum. Let's override the default "indexes" like so:
    Code (csharp):
    1. public enum StatModType
    2. {
    3.     Flat = 100,
    4.     PercentAdd = 200,
    5.     PercentMult = 300,
    6. }
    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:
    Code (csharp):
    1. protected bool isDirty = true;
    2. protected float lastBaseValue;
    3. protected float _value;
    4. public virtual float Value {}
    5.  
    6. protected readonly List<StatModifier> statModifiers;
    7.  
    8. public virtual void AddModifier(StatModifier mod);
    9. public virtual bool RemoveModifier(StatModifier mod);
    10. public virtual bool RemoveAllModifiersFromSource(object source);
    11.  
    12. protected virtual int CompareModifierOrder(StatModifier a, StatModifier b);
    13. protected virtual float CalculateFinalValue();


    Let's also mark the CharacterStat class as [Serializable], so that we can actually edit it from the Unity inspector.
    Code (csharp):
    1. [Serializable]
    2. 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:
    Code (csharp):
    1. public CharacterStat()
    2. {
    3.     statModifiers = new List<StatModifier>();
    4.     StatModifiers = statModifiers.AsReadOnly();
    5. }
    6.  
    7. public CharacterStat(float baseValue) : this()
    8. {
    9.     BaseValue = baseValue;
    10. }


    Other than that, I've also put both classes inside a namespace:
    Code (csharp):
    1. namespace Kryz.CharacterStats
    2. {
    3.     [Serializable]
    4.     public class CharacterStat
    5.     {
    6.         //...
    7.     }
    8. }
    Code (csharp):
    1. namespace Kryz.CharacterStats
    2. {
    3.     public enum StatModType
    4.     {
    5.         //...
    6.     }
    7.  
    8.     public class StatModifier
    9.     {
    10.         //...
    11.     }
    12. }
    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!
     
    Last edited: Jan 26, 2018
  2. Zymes

    Zymes

    Joined:
    Feb 8, 2017
    Posts:
    96
    Thanks this is great.

    I was just looking at how to solve this.
     
    Kryzarel likes this.
  3. reiniat

    reiniat

    Joined:
    Apr 3, 2014
    Posts:
    5
    Way more clear and straightforward than the link to the other tutorial.
     
    Kryzarel likes this.
  4. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    Thank you guys so much for your kind words :)

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

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    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 :)
     
  6. Saucyminator

    Saucyminator

    Joined:
    Nov 23, 2015
    Posts:
    11
    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!
     
  7. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    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.
     
  8. furiantmultimedia

    furiantmultimedia

    Joined:
    May 8, 2013
    Posts:
    6
    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.

    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.
     
    Last edited: Jan 12, 2018
    Kryzarel likes this.
  9. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    @furiantmultimedia 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 :D
     
  10. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    5,801
    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.
     
  11. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    @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 :)

    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?
     
  12. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    5,801
    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:

    Code (csharp):
    1.     public enum StatType { Health, Mana, Level, Experience, ExpReward, Agility, Dexterity, Endurance, Strength, RegenHp, RegenMp }
    2.  
    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.

    Code (csharp):
    1.     public enum StatProperty { Value, Base, Min, Max, Affinity, MaxAffinity }
    2.     public enum StatModType { Active, Direct }
    3.     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:

    Code (csharp):
    1.         public virtual void LevelUp()
    2.         {
    3.             Stats[(int) StatType.Level].AddToRoot(StatProperty.Value, 1);
    4.         }
    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.

    Code (csharp):
    1.         public virtual void AddExp(float amount = 1000)
    2.         {
    3.             if (GetStatValue(StatType.Experience) + amount >= GetStatMax(StatType.Experience))
    4.             {
    5.                 float excess = amount - (GetStatMax(StatType.Experience) - GetStatValue(StatType.Experience));
    6.                 LevelUp();
    7.                 Stats[(int) StatType.Experience].SetRoot(StatProperty.Value, excess);
    8.             }
    9.             else Stats[(int) StatType.Experience].AddToRoot(StatProperty.Value, amount);
    10.         }
    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.
     
  13. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    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 :p
     
    LaneFox likes this.
  14. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    LadyAth, nuxvomo and Duffer123 like this.
  15. Duffer123

    Duffer123

    Joined:
    May 24, 2015
    Posts:
    877
    @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)?
     
  16. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    Just saw this now. Already answered in the video, so np :p
     
    Duffer123 likes this.
  17. Kuuo

    Kuuo

    Joined:
    Jan 24, 2017
    Posts:
    5
    @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:
    Code (CSharp):
    1. //Make the StatType as a ScriptableObject
    2. public class StatType : ScriptableObject
    3. {
    4.     public string typeName; // only a string of typeName here
    5.     // you can add this ID field if you want to
    6.     // public int typeId;
    7. }
    And in the Stat and StatModifier class, they have a StatType field
    Code (CSharp):
    1. public class StatModifier
    2. {
    3.     public StatType targetType;
    4.     // etc...
    5. }
    Code (CSharp):
    1. public class Stat : ScriptableObject
    2. {
    3.     public StatType type;
    4.     // etc...
    5.  
    6.     // Make it return bool so you know whether the StatType is matched
    7.     public bool AddModifier(StatModifier newModifier)
    8.     {
    9.         if (newModifier.targetType != type) return false;
    10.         // do mod value things...
    11.         return true;
    12.     }
    13.  
    14.     // also in Remove    
    15.     public bool RemoveModifier(StatModifier modifierToRemove)
    16.     {
    17.         if (modifierToRemove.targetType != type) return false;
    18.         // etc...
    19.         return true;
    20.     }
    21. }
    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
    Code (CSharp):
    1. public class StatTypeList : ScriptableObject
    2. {
    3.     public StatType[] data;  // just a collection of StatTypes here
    4. }
    And the CharacterStats class, a character may have many stats...
    Code (CSharp):
    1. // I used some new language features of C# 6 here
    2. public class CharacterStats : ScriptableObject
    3. {
    4.     public Stat[] stats; // I use array so it's more easy to write editor script
    5.  
    6.     // use indexer to get matched type stat...
    7.     // I'm not sure it's good or not
    8.     // you might need HashSet or Dictionary to speed up the search
    9.     public Stat this[StatType type] => Array.Find(stats, s => s.type == type);
    10.  
    11.     public bool AddModifier(StatModifier modifier) =>
    12.         this[modifier.targetType]?.AddModifier(modifier) ?? false;
    13.  
    14.     public bool RemoveModifier(StatModifier modifier) =>
    15.         this[modifier.targetType]?.RemoveModifier(modifier) ?? false;
    16. }
    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:
    upload_2018-2-22_13-4-49.png
    and in Inspector:
    upload_2018-2-22_13-7-54.png

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

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

    2. 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?​
     
    Kryzarel likes this.
  18. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    @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 :D

    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.
     
  19. Kuuo

    Kuuo

    Joined:
    Jan 24, 2017
    Posts:
    5
    @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~;)
     
    Last edited: Feb 26, 2018
    Kryzarel likes this.
  20. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    @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 :D
     
  21. SpyrosUn

    SpyrosUn

    Joined:
    Nov 20, 2016
    Posts:
    12
    @Kryzarel

    Thanks for the nice asset. I am wondering how I should be handling serialized attributes. For example, my Player class contains CharacterStat(s) and the base values are meant to be stored in a savefile. On load time, if some achievements have been reached, certain bonuses will be added, like (for CharacterStat attrib):

    StatModifier modifier = new StatModifier(0.5f, StatModType.Flat);
    attrib.AddModifier(modifier);

    I would actually want to not persist the StatModifier with the attrib object(it can't be stored anyway, it's not serializable), but rather just use the modifier to calculate the new attrib value. Is there a way to do that ?

    Thanks !
     
  22. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    @SpyrosUn Maybe I should have added the [NonSerialized] attribute to the StatModifers list inside the CharacterStat class. Hope this helps. I'll look into this better when I get home from work later today :)

    You can try this yourself in the meantime, since you have full access to the source code in the asset.
     
  23. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    @SpyrosUn
    Hi again. So, as far as I can tell, adding the [NonSerialized] attribute above List<StatModifier> statModifiers and also ReadOnlyCollection<StatModifier> StatModifiers, should be the right way to go.

    Unfortunately, there might be an unfortunate caveat to serializing a CharacterStat, depending on the serialization method you're using. Some serializations don't call the default constructor (for some odd reason), as stated in this StackOverflow answer: https://stackoverflow.com/questions/1076730/datacontractserializer-doesnt-call-my-constructor

    So if you're using one such method, you might need to implement other ways to initialize the lists, such as using the [OnDeserializing] attribute or other methods suggested in that link.
     
  24. Zymes

    Zymes

    Joined:
    Feb 8, 2017
    Posts:
    96
    How would you handle stat changes when a dependent stat has changed?

    For example if you have a stat called Strength. It affects the Defense of the character. You add a modifier to Strength by increase/decrease. Now the stat Defense should change depending on the value of Strength.

    Use events and update the Defense stat? How do you define subscriptions/relations between stats in a sane way?

    Like a change to Defense should not change Strength, it should only propagate one way. Defense could be a calculation summed up with some kind of formula Strength + Agility * 1.25 = Defense or whatever.
     
  25. Codinablack

    Codinablack

    Joined:
    May 8, 2016
    Posts:
    12
    @Kryzarel

    How can I sync the result from the character stat?

    I mean I can make a variable like _maxHealth as the base for a stat called maxHealthBuff, and use it on the player to add items with these modifiers for your stat system, but the output, the value that comes from the modification from the character stat, how do I keep that sync'd on the network?
     
  26. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    @Codinablack
    I'm gonna have to ask you to clarify your question a bit more, not sure if I understood. You want to sync the stat over the network? Unfortunately I haven't done any networking in Unity, so I don't think I can be of any help regarding this topic :(

    However, I didn't understand the objective of the _maxHealth and maxHealthBuff variabes. The CharacterStat class already stores the BaseValue and the calculated value (simply called "Value").
     
  27. Cumuka

    Cumuka

    Joined:
    Nov 27, 2014
    Posts:
    15
    Great approach for adding stats to an item or I should say any attribute to any entity in your game from wide perspective. I'd like to ask you could you share your inspector script? It looks very clean and very useful!

    Thanks in advance!
     
  28. ItzChris92

    ItzChris92

    Joined:
    Apr 26, 2017
    Posts:
    224
    Great work with this. I thoroughly enjoyed the videos too, and I am currently implementing a stat system for my own game that will be based around this system. It's so neat, and efficient. My first attempt at an inventory ended up very "spaghettied".

    One question though. Do you think stats such as HP, Mana, EXP etc. should be classed as a stat under your system? They require min/maxes etc. so I was hoping for a little insight as to how you would (if you would) implement this using your system. I was thinking maybe having two separate character stats for these, a currentHP and a maxHP for example. Do you think this is the right way to approach this?
     
    Last edited: May 31, 2018
  29. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    27
    @ItzChris92 Hey, sorry for the late reply. For stats such as those, my suggestion is to create a new class that extends from CharacterStat and add one new variable: CurrentValue. This could be a public float property without any special behavior except for the fact that it can't go above Value.

    For example, for HP, you probably don't want to apply any buffs/debuffs to the CurrentValue, only to the maximum value, which in this case would be represented by the Value property. You would just use the CurrentValue variable to add or remove health when your character gets damaged or healed.
     
    ItzChris92 likes this.
  30. ItzChris92

    ItzChris92

    Joined:
    Apr 26, 2017
    Posts:
    224
    That's what I first thought, but seeing as it doesn't need to be modified I maybe thought there was a better way. Tbh a stand alone float variable will do the trick for health in this case, but ill definitely have scripts inheriting from CharacterStat for some of my other stats! Thanks for the input. I really am loving this asset. Saved me a lot of rewriting!
     
    Kryzarel likes this.