Stat System - Being stuck somewhere between Interfaces and Generics, could need an advice

Hello,
for my game I need units to have attributes, stats, commonly known ones like Health, Health Regeneration, …
Therefore, I created the following interfaces to describe stats and their behavior:

  • IStat<T> - This MonoBehavior is considered a Stat, with a value, a name and a description
  • IDynamicStat<T> - This MonoBehavior is an IStat<T> with an additional dynamic value ( as in X of MAX Health)
  • IRegenerationStat<T> - This MonoBehavior is an IStat<T> which periodically modifies another IStat<T>

Additionally, I created the following interfaces to allow modification:

  • IGenericBaseModifying<T> - This object modifies objects of Type T

  • IGenericBaseModifiable<T> - This object allows IGenericBaseModifying<T> to subscribe to it.

  • IGenericDynamicModifying<T> - This object modifies objects of Type T

  • IGenericDynamicModifiable<T> - This object allows IGenericDynamicModifying<T> to subscribe to it.

Here is an example for an implementation ( in reality there is a base class to reduce redundancy):

public class Health : MonoBehavior, IStat<Health>, IDynamicStat<Health>, IGenericBaseModifiable<Health>, IGenericDynamicModifiable<Health>
{
    #region IStat<Health>
    public float BaseValue
    {
        get
        {
            throw new NotImplementedException();
        }
    }
    public float Value()
    {
        throw new NotImplementedException();
    }
    #endregion

    #region IGenericBaseModifiable<Health>
    public void Add(IGenericBaseModifying<Health> modifier)
    {
        throw new NotImplementedException();
    }

    public bool Remove(IGenericBaseModifying<Health> modifier)
    {
        throw new NotImplementedException();
    }
    #endregion

    #region IGenericDynamicModifiable<Health>
    public void Add(IGenericDynamicModifying<Health> modifier)
    {
        throw new NotImplementedException();
    }

    public bool Remove(IGenericDynamicModifying<Health> modifier)
    {
        throw new NotImplementedException();
    }
    #endregion

    #region IDynamicStat<Health>
    public float DynamicValue
    {
        get
        {
            throw new NotImplementedException();
        }
    }
    #endregion
}

I hope this is not too confusing. Health is a Stat with a base value and a max value, it is also a DynamicStat with a [runtime] current value and as well its base value and its dynamic value can be modified from the outside by subscribers who implement IGenericDynamicModifying<Health> and/or IGenericBaseModifying<Health>.

As you can see, this is quite clean in the inspector:

So far so good.

However, I now wanted to implement Items into my game. An Item has several ItemModifier which should be able subscribe to the corresponding stats.

As the interfaces for Modifying and Modifiable are generic, ItemModifier has to be generic as well. This means, I have to create a HealthItemModifier, an EnergyItemModifier, basically for every Stat I have in my game. Additionally, one also has to consider how an ItemModifier works, multiplicatively, additive, …, which means I would easily have to create four ItemModifier per Stat.

If you now think about it, this will be the same story for BuffModifier, DebuffModifier, TalentModifier, SkillModifier and whatever I will implement into my game. An obvious solution would be to make one Modifier class, however, Java teached me that you then do not know what kind of modifier you have, which is absolutly required when trying to interact with a specific kind, like all BuffModifiers.

Another solution I came up with, I could get rid of ItemModifier entirely and implement the interfaces into the objects directly, exactly like the Health example above. An item would then imlpement all required interfaces. As you can imagine, this would be awful to do as well, since one then has to write one class for every possible combination of items ( one item which modifies only damage, one item which only modifies energy and damage, …).

Last solution I can think of, making the IModifiable and IModifying not generic, but how would I then know which type I should aim for? How would an item know it modifies Health and not Energy? I would then have to serialize types, which I kinda hesitate to do.

I could need some input and thoughts with my situation, I greatly appreciate your comments, feedback and suggestions.

3448470--273115--stats.PNG

Sure this can get out of hand with too many interfaces for everything and can become hard to manage on the long run. Just throwing what I do in my game in this case.

Mind that I’m working in a normal fps game, so I don’t need this kind of deep system, but since I create everythign dynamically at runtime had to find a way around stats even for simpel things like health.

So on my character I don’t have hardcoded types of stats I have a base class simpl called Status, this class has an id, min max value in floats, a current value, if reaching 0 will cause death and some references for UI.

My character is made of 3 status classes with id health,armor and hoxygen. Now to be able to modify the stats I put the stats in a dictionary ordered by id as key.
When I pick an item that has the ability to modify stats, I just send a StatusModifier class to an overload in the character script, that overload check the StatusModifier target id for the Status to modify and if it finds any in the dictionary proceed to apply values and do whatever has to be done which in my case is update min value and run the UI info to show on hud what has been picked up.

So this way I only need two classes, a Status with all the info and a Modifier that modify the Status. This avoids me to have too many type of different classes and also avoid having monobehaviours for things that should just hold data.

But yeah my game structure is simplier than your but maybe I was of some help.

Thank you for your reply, @

Your approach is one that I considered as well. A great advantage is its simplicity. One only needs a StatController which has a dictionary with string+Stat combination and a base class for stats, that’s all.

I myself hesitate to use this design, though. My mentors in Java used to disapprove of this design, they said “one don’t know what stat you have there” and it would be very dificult to reach for something specific like Health. I do know about the dictionary and I like that this approach also allows to follow the MVC-controller pattern a lot more easily, yet I cannot force myself to use it. I am not sure what exactly is preventing it, it is only a vague feeling.

Besides the growing complexity, isn’t the major disadvantage of my current design that I have to manually create a lot of classes? I could use an opinion on that. In short, creating a literal class for everything, starting with Stats, like Health, Energy, … up to all related stuff, like HealthItemModifier, EnergyItemModifier, EnergyBuffModifier, …

Well probably for your case my approach is sure a bit too simple, I don’t have to keep track of many things, if a stat health for example isn’t found it just doesn’t apply damage, giving me the ability to make enemies immune to certain damage modifier, but againI don’t use many stats per character.

I don’t think there is anything against having too many classes or interfaces, I myself try to minimize the use of too many only for a reason that it becomes complicated to manage it, at least for me.

Thats why I went the other route especially for thigns that basicly have the same data in it, like armor or health, same behaviour in the end. But yeah my method is prone to fuck ups, I’m fulyl aware of it and try to minimize problems with fallbacks if somethign goes wrong.

Having modded id software games in the past a lot kinda went that route emulating a bit what id software does, I take inspiration on my code structure from quake 3 or doom 3 source code, you can take a look here:

Maybe if you got the game, take a look at something like Skyrim or Oblivion and check their tool sets, codewise it tells nothing, but the hierarchy on how items and modifiers are structured may help you on how write down your classes.

I don’t know the code specifics but for example a consumable item in Skyrim is simply an ALCH type, it doesn’t do anything on itself but it contains the data for what it does, more precisely a reference to an ENCH which is an effect that modify character specific attributes, like health. An ENCH can be filled with all sort of hardcoded effects, so yeah they utilize base stats in there.

So you may slim down the use of interfaces for each item type, and restrict to categories, like you have the item potion for example that has a reference to an effect, then you do the stats hardcoding in the effect types but in the end you would have one item category and can reuse effects for other things, also could manage those effects on their own instead of each item type. Not sure I was clear on explanation, but in the end won’t cut too much the use of hardtyped classes but categorizing what items does separatly could give you a bit more order.

Also for timed things like health regeneration I would personally do one manager on the character that take care of all the timed effects on stats, instead of a monobehaviour for each modifier, but thats just me, nothign wrong with having multiple behaviours.

In general, I say this is not a problem. Lots of small, focused classes is great.
That said … I think there is too much thinking going on here. The problem will not be the number of classes but the explosion in complexity when combining all your actions.

Why do you need an IGenericBaseModifying? Why not just reach in and modify the stat you want to change? I dislike what I see in your inspector snapshot because it looks like the Vitality stat needs to know about all the types which might reference it. That will create a bidirectional dependency, which is a variant of the “Inappropriate Intimacy” code smell.

A simple approach I’ve seen is to create a component which can be added to a game object for each stat. For Health, it might look like:

public class Health : IHealthSystem
{
  [SerializeField]
  private int maxHealth;
  public int MaxHealth
  {
    get { return maxHealth; }
    private set { maxHealth = value; }
  }
  [SerializeField]
  private int currentHealth;
  public int CurrentHealth
  {
    get { return currentHealth; }
    private set { currentHealth = value; }
  }

  public event Action<IHealthSystem> OnDeath { add; remove; }

  public Health()
  { }

  public IHealthSystem.Damage(int value)
  {
    CurrentHealth -= value;
    if (CurrentHealth <= 0)
      OnDeath?.Invoke(this);
  }

  public IHealthSystem.Heal(int value)
  {
    CurrentHealth += value;
    if (CurrentHealth > MaxHealth)
      CurrentHealth = MaxHealth;
  }
}

A regeneration component might look like this

public class HealthRegen : IRegen<IHealthSystem>
{
  private IHealthSystem HealthSystem { get; }
  [SerializeField]
  private int timeToHeal;
  public int TimeToHeal
  {
    get { return timeToHeal; }
    private set { timeToHeal = value; }
  }
  [SerializeField]
  private int amountToHeal;
  public int AmountToHeal
  {
    get { return amountToHeal; }
    private set { amountToHeal = value; }
  }
  private float NextHealTime { get; set; }

  public Awake()
  {
    IHealthSystem healthSystem = GetComponent<IHealthSystem>();
    if (healthSystem == null)
      throw new ArgumentNullException(nameof(healthSystem));
    HealthSystem = healthSystem;
  }

  public Update()
  {
    if (Time.CurrentTime > NextHealTime)
    {
      HealthSystem.Heal(AmountToHeal);
      NextHealTime = Time.time + TimeToHeal;
    }   
  }
}

@ @eisenpony Thank you for your reply!

@

I assume your setup looks something like this:

using System;
using System.Collections.Generic;
using UnityEngine;

public class StatController : BaseBehavior
{
    [SerializeField]
    private Dictionary<string, Stat> stats;

    public bool Add(IModifier modifier, string identifier)
    {
        if (stats.ContainsKey(identifier))
        {
            stats[identifier].modifiers.Add(modifier);
            stats[identifier].isDirty = true;
            return true;
        }
        return false;
    }
    public bool Remove(IModifier modifier, string identifier)
    {
        if (!stats.ContainsKey(identifier))
        {
            return false;
        }
        return stats[identifier].modifiers.Remove(modifier);
    }

    [Serializable]
    public class Stat
    {
        [SerializeField]
        private string identifier;

        [SerializeField]
        private float baseValue;

        [SerializeField]
        [InspectorDisabled]
        internal readonly List<IModifier> modifiers;

        [SerializeField]
        [InspectorDisabled]
        internal bool isDirty;

        [SerializeField]
        [InspectorDisabled]
        private float value;

        public float BaseValue
        {
            get
            {
                return baseValue;
            }
        }

        public float Value
        {
            get
            {
                if (!isDirty) { return value; }
                if (modifiers == null || modifiers.Count == 0) { return baseValue; }

                value = baseValue;
                foreach (var modifier in modifiers)
                {
                    if (modifier != null)
                    {
                        modifier.Modify(ref value);
                    }
                }
                isDirty = false;
                return value;
            }
        }
    }
}

Not sure how this would prove useful in a long run, with several mechanics at once, but it surely handy and simple enough.

About the approach you posted, @eisenpony wouldn’t I end up with somethign similar to my design, once I start to generalize? Considerung there would be one stat system per stat per unit, wouldn’t I try to generalize them into three main systems, as there are:

  • a stat with a base value and a max value
  • a stat with a base value, a max value and a dynamic current value
  • a stat with a base value, a max value and modifying another stat

Or do you mean something different with IHealthSystem, e.g., which would be the first category? As in hardcoding every stat, avoiding generalization/specification and not caring about redundancy for the sake of easy managment and overlooking one’s work?

There is something I never quite got with Unity and that’s accessability and protection level of objects. Obviously, both your approaches @eisenpony and @ would allow an easy implementation of a stats system, yet wouldn’t it be severely vulnerable? If I can directly modify a stat, I wouldn’t even need modifiers, I think, I could reach out and modify it, however, everyone could do so, every object. Isn’t this something one should avoid?

Additionally, I don’t know how to reference stat types, as @ says:

How exactly do I properly reference to my stats here? Do I use a string-based approach? I also have access to a TypeSpecifier which searches a type and all its derived types, but both options feel strange to me, somehow.

Or would I definitely hardcode the targeted stats? I mean, if I think of some items of genre like RPG or something in that direction, the combinations are quite colorful and having a hardcoded class for every combination? Hm, I don’t know, is this how one should do it?

No, you shouldn’t avoid that. Who is the “everyone” that you’re worried about? You and your team are writing the code, so simply agree that if you don’t need to mess around with player stats, you wont do it. Anyways, adding an interface which must be “implemented” just to be allowed to modify a stat doesn’t solve the problem. The evil component you are imagining will simply implement the interface and twiddle the stat.

Maybe you could generalize it to that level, but my question is why? How many stats are we talking about here? hundreds? Thousands? Most games I’m familiar with have 10 to 20 stats maximum. I think 10 to 20 stats is very reasonable to manage so don’t over generalize your code.

That said, some stats might behave differently than health. Example, a “dexterity” stat is probably just used as an input to other formulas so it doesn’t need an entire “system” wrapping it up. Perhaps you could generalize these kinds of stats, though a simple integer, integer (current, max) pair might also be good enough.

Ohhh, I see, thank you, that explains a lot, as example why public fields are always serialized. I feel quite enlightened now, I did not consider my game to be a nutshell and me being able to do in there what I want, that was my mistake.

About the interface requirement, yeah, you are right, I don’t know why I justified it like this, I thought I know that, since any object could implement the required interface and do whatever it wants.

Even if there are only a couple of stats, I believe as soon as one has to copypaste something more than once, one is doing something wrong and one should try to generalize. After all, all these stats are likely to behave the same way, Energy Regeneration like Health Regeneration like Stamina Regeneration and so on. I am going to go one step further having a look on the MCV-controll pattern and will make my stats pure data containers, writing the modifying and regenerating in sepeperate controllers.

. Thank you for this advice, that’s how I am going to do!

Thank you guys, @eisenpony @ you helped me alot!