Search Unity

  1. We've closed the job boards. If you're looking for work, or looking to hire check out Unity Connect. You can see more information here.
    Dismiss Notice
  2. Unity 2017.3 has arrived! Read about it here.
    Dismiss Notice
  3. 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:
    13
    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.

    FULL SOURCE CODE DOWNLOAD AT BOTTOM OF POST!

    Video version:
    Chapter 1

    Chapter 2

    Chapter 3


    Chapter 1:
    [​IMG]
    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. Besides, I'm gonna be releasing this on the Unity Asset Store (for free!). It has already been submitted for approval, now it's just going to take a few days.

    I've also uploaded the full source code as an attachement to this post. The asset store version will have an example UI, but other than that it's the exact same thing!


    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!
     

    Attached Files:

    Last edited: Dec 18, 2017
  2. Zymes

    Zymes

    Joined:
    Feb 8, 2017
    Posts:
    95
    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:
    13
    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:
    13
    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:
    10
    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:
    13
    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 at 7:53 PM
    Kryzarel likes this.
  9. Kryzarel

    Kryzarel

    Joined:
    Jul 30, 2013
    Posts:
    13
    @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,101
    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:
    13
    @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,101
    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:
    13
    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.