Search Unity

Resolved Finding a "nice" way to store many different stats for a unit

Discussion in 'Scripting' started by chemicalcrux, Mar 22, 2023.

  1. chemicalcrux

    chemicalcrux

    Joined:
    Mar 16, 2017
    Posts:
    720
    My game needs a variety of stats for each entity. Health, stamina, balance, drunkenness, mana, whatever.

    I originally had it set up like this:

    Code (CSharp):
    1. public struct StatValue
    2. {
    3.   public float value;
    4.   public float maxValue;
    5.   public float Percentage => value / maxValue;
    6. }
    I then attached these to the unit.

    Code (CSharp):
    1. public class Entity : MonoBehaviour
    2. {
    3.   public StatValue health;
    4.   public StatValue stamina;
    5. }
    A problem arose: I needed to be able to refer to a specific stat without hard-coding a reference. The first use-case that came up was making stat-bars: I didn't want to write a separate script for every single stat!

    So, I created a Stat enum:

    Code (CSharp):
    1. public enum Stat
    2. {
    3.   Health,
    4.   Stamina,
    5.   Mana,
    6.   // etc.
    7. }
    And then I added a new function to the Entity class:

    Code (CSharp):
    1. public StatValue GetStat(Stat stat)
    2. {
    3.   return stat switch
    4.   {
    5.     Stat.Health => health,
    6.     Stat.Stamina => stamina,
    7.     // etc.
    8.   }
    9. }
    This worked fine for the moment, since that was read-only.

    However, I now want to be able to modify stats through the same interface. It seems weird to read them via the enum key, but then write them by just directly accessing a field.

    But, since StatValue is a struct, it's a value type. That means I cannot do something like this:

    Code (CSharp):
    1. entity.GetStat(Stat.Health).value = 3;
    I could turn StatValue into a class, making it a reference type. However, this prevents it from appearing in the inspector. I guess I could write a custom editor, but I'd rather not...

    My ideal solution would be used like this:

    Code (CSharp):
    1. entity.stats[Stat.Health].value -= 10;
    2. Debug.Log(entity.stats[Stat.Health].percentage);
    Does anyone have any strong opinions about this one? It's probably bikeshedding (I've spent two hours thinking about this so far...), but I figure I ought to just get this right now.
     
  2. Brathnann

    Brathnann

    Joined:
    Aug 12, 2014
    Posts:
    7,188
    One possible option is to make the stats class inherit from Monobehaviour. Then, create a dictionary of stats using the enum as the key and the class as the value. As you add the stat component to the player, enemy, whatever, create a key/value pair and add it to your dictionary.

    Then you could have the values visible on each component. You can even set an extra field for the name of stat value. So visiblly it might show "Health" as the name along with the values. I feel like that might be the way to go. But obviously, there are a bunch of ways to set this up.
     
  3. Olipool

    Olipool

    Joined:
    Feb 8, 2015
    Posts:
    322
    Dictionary might be a good idea for what you describe. But maybe can you give more details why you want/have to access the stats in a "generic" way using enums? Why can't you just say: entity.health.value=3? (maybe encapsulated in Properties)

    And you can of course add a method:
    Code (CSharp):
    1. public void SetStat(Stat stat, float value)
    2. {
    3. switch(stat)
    4. {
    5. case Stat.Health: health.value=value; break;
    6. case...
    7. }
    8. }
     
  4. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,330
    You can get quite close to your ideal solution by defining an indexer and some implicit conversion operators:

    Code (CSharp):
    1. [Serializable]
    2. public sealed class Stats
    3. {
    4.     [SerializeField] private StatValue health;
    5.     [SerializeField] private StatValue stamina;
    6.  
    7.     public StatValue Health => health;
    8.     public StatValue Stamina => stamina;
    9.  
    10.     public StatValue this[Stat stat]
    11.     {
    12.         get => stat switch
    13.         {
    14.             Stat.Health => health,
    15.             Stat.Stamina => stamina
    16.         };
    17.  
    18.         set
    19.         {
    20.             switch(stat)
    21.             {
    22.                 case Stat.Health:
    23.                     SetHealth(value);
    24.                     return;
    25.                 case Stat.Stamina:
    26.                     SetStamina(value);
    27.                     return;
    28.             }
    29.         }
    30.     }
    31.  
    32.     public void SetHealth(float value) => health = new StatValue(value, health.MaxValue);
    33.     public void SetStamina(float value) => stamina = new StatValue(value, stamina.MaxValue);
    34. }
    Code (CSharp):
    1. [Serializable]
    2. public struct StatValue
    3. {
    4.     [SerializeField] private float value;
    5.     [SerializeField] private float maxValue;
    6.  
    7.     public float Value => value;
    8.     public float MaxValue => maxValue;
    9.     public float Percentage => value / maxValue;
    10.  
    11.     public StatValue(float value, float maxValue)
    12.     {
    13.         this.value = value;
    14.         this.maxValue = maxValue;
    15.     }
    16.  
    17.     public static implicit operator StatValue(float value) => new StatValue(value, 100f);
    18.     public static implicit operator float(StatValue value) => value.value;
    19. }

    Result:
    Code (CSharp):
    1. public void Example()
    2. {
    3.     var stats = new Stats();
    4.      
    5.     float health = stats.Health;
    6.     health = stats[Stat.Health];
    7.      
    8.     stats.SetHealth(75f);
    9.     stats[Stat.Health] = 75f;
    10.  
    11.     float healthPercentage = stats.Health.Percentage;
    12.     healthPercentage = stats[Stat.Health].Percentage;
    13. }
     
    Olipool likes this.
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,925
    You can also take a component approach, similar in Unity's components, using SerializeReference to serialise a collection of different plain classes:

    Code (CSharp):
    1. [System.Serializable]
    2. public abstract class EntityStatBase
    3. {
    4.  
    5. }
    6.  
    7. public sealed class EntityStatHealth : EntityStatBase
    8. {
    9.     public int Heath;
    10. }
    11.  
    12. public sealed class EntityStatMana : EntityStatBase
    13. {
    14.     public int Mana;
    15. }
    16.  
    17. public clas Entity : Monobehaviour
    18. {
    19.     [SerializeReference]
    20.     private List<EntityStatBase> _entityStats = new();
    21.  
    22.  
    23.     public T GetEntityStat<T>() where T : EntityStatBase
    24.     {
    25.         int count = _entityStats.Count;
    26.         for (int i = 0; i < count; i++)
    27.         {
    28.             if (_entityStats[i] is T cast)
    29.             {
    30.                 return cast;
    31.             }
    32.         }
    33.      
    34.         return null;
    35.     }
    36.  
    37.     public bool TryGetEntityStat<T>(out T stat) where T : EntityStatBase
    38.     {
    39.         stat = GetEntityStat<T>();
    40.         return stat != null;
    41.     }
    42. }
    This is my general approach to this kind of stuff.
     
  6. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,913
    Classes can serialize and show in the inspector the same as structs using
    [Serializable]
    .
     
  7. chemicalcrux

    chemicalcrux

    Joined:
    Mar 16, 2017
    Posts:
    720
    Thanks for the suggestions, everyone.

    I did consider this -- it reminds me of the ECS way of doing things. Each stat could register itself with its owner during Awake() to avoid the tedium of dragging all those references around.

    Suppose I have an effect that deals damage. Obviously, you could have one that deals damage to health, but what about stamina, or mana (or maybe one that "heals" a stat that you want to keep low, like fear?).

    I want to be able to tell an entity to gain/lose any stat without lots of duplicate logic in every place that wants to do it.

    Thanks for the example! I was playing around with that, but I hadn't quite fleshed it out all the way.

    Definitely seems reasonable.

    I have to admit that I'm a little fuzzy on SerializeReference...I'll figure it out at some point :p

    Oh, huh! I swear I tried this and had everything vanish from the inspector -- but I just did it again and it looks like it's working.

    Maybe I forgot to mark something as serializable...but surely that'd have broken the struct, too! Who knows o_O

    That should be all I need for the time being.
     
  8. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,925
    It's basically serialising 'by reference', rather than by values which Unity's default serialisation works by. So you can use it for polymorphic serialisation, which, as it turns out, is useful in a lot of situations.

    Requires inspector work/tools to get working, but definitely worth it.
     
  9. Olipool

    Olipool

    Joined:
    Feb 8, 2015
    Posts:
    322
    Hm what tools do you need? I just read about it and it sounds like it should just work?
     
  10. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,925
    Unless something has changed in 2022/23, there's no built in way to select/instance subtypes in fields or collections via the inspector.

    The serialisation works great. Just the inspector/editor support is lacking.

    Thankfully tools like Odin Inspector support it out of the box.
     
    Olipool likes this.