Using component based design for RPG attributes & stats?

What do you guys think?

I was thinking you could create a new prefab and add components like “Current”, “Max”, etc.
For resource stats like Health or Mana, you’d have components “Current”, “Max”, “Repeat” (repeat would just invoke a UnityEvent at a configured interval and the “Current” component could have a method “IncreaseBy” that could take an int.)
And so on and so forth.

The only thing I’m concerned about is: 1) Performance, 2) You would have to create a new prefab for every stat since it would be too many components to add to 1 GameObject. Then when you want to decrease a stat, you’d first have to find the prefab as a child then get it’s name. I’m not sure what the best way to do that would be.

Thoughts?

I’d avoid implementing this. Making modifications later on would be a pain, say, for adding a new one or removing one altogether (as you stated). If you’re looking for something modular you could use a composite design: Using the Composite Design Pattern for an RPG Attributes System | Envato Tuts+ might be a good place to start if you want to go that route.

Having a prefab act as your groupings (character sheet) could work if you still (for whatever reason) wanted to have stats tied to prefabs. So have a MonoBehaviour that contains variables for health, intelligence, etc… which are of the Attribute type.

1 Like

Why would you want to do this? I don’t see ANY advantages to it.

Well I listed the reasons and advantages in the OP.

There aren’t any advantages to that. It’s modular to the point that you don’t want to use it.

You would be better off making something like this:

    [System.Serializable]
    public class Stats
    {
        [SerializeField] public Stat Level;
        [SerializeField] public Stat Experience;
        [SerializeField] public Stat XpReward;

        [SerializeField] public Stat Health;
        [SerializeField] public Stat Armor;
        [SerializeField] public Stat Strength;
        [SerializeField] public Stat Agility;
        [SerializeField] public Stat Dexterity;
        [SerializeField] public Stat Endurance;
    }

    [System.Serializable]
    public class Stat
    {
        public Stat(int sBase, int actual, int min, int max, int perLevel)
        {
            Base = sBase;
            Actual = actual;
            Min = min;
            Max = max;
            IncreasePerLevel = perLevel;
        }
        [SerializeField] public float Base;
        [SerializeField] public float Actual;
        [SerializeField] public float Min;
        [SerializeField] public float Max;
        [SerializeField] public float IncreasePerLevel;
    }

This way its all in one class and much easier to navigate, display and use.

Then it would look like this on your character:

2 Likes

That’s true but what about stats like Health or Mana that regenerate periodically?

I agree with @ 's point about the flexibility of component-based design versus locking all of your stats into a single monolithic data structure. What if you want to define a breakable door that has Health and Armor but not Strength, Agility, etc.?

If you’re defining attributes as components, I’d just as soon add them all to the main GameObject. It gives you the flexibility of composition while displaying them all in one inspector view.

I think of attributes like Health as an object consisting of < Current, Max, Events >, where Events are the events that Health handles. I wouldn’t split Current and Max into separate classes. So, in practice, you’re not going to have a ridiculous number of components. You’ll only have the ones that make sense for each character or entity.

What about them? Just modify the Actual. The stats are instanced in the parent class.

I agree there is some value in the concept but I don’t see why making prefabs/components could be better than just making a different Stat class or structure as needed. Or just ignoring the unnecessary parts altogether?

Different Stat classes with overlapping data (e.g., Health defined separately in both) break the Don’t Repeat Yourself (DRY) rule. You end up having to maintain duplicates of the same code in more than one place.

To try to work around this, you could put Health in a parent class and then create subclasses for BreakableDoor, Character, etc. But then you get into the classic attribute inheritance problem. For example, you might have a specific breakable door that also has an XpReward, so you move XpReward from the Character subclass to the parent class. Eventually everything ends up in the ancestor class this way.

With component-based design, you can add exactly what you need, and nothing’s duplicated.

If you want to show all the stats in a nice table like the screenshot in your first post, you can add a custom editor that compiles all the stat components.

This is just a different way of looking at design; everyone’s free to take it or leave it of course.

1 Like

The above is my [basically]current implementation, as is the inspector which is just a generic drawer for the Stat component. If I were to need more stats they would be put in the Stats class. Anything with a Stats class will get the appropriate Stats and it is predictable.

With all of the stats there, unused stats can just be zero (depending on formulas) instead of splintering Stat classes. Ideally, I think you need all of the stats there anyway to properly accommodate generic formulas for damage and such. I would not want to find the stat I need in a dynamic list every time I had to run a calculation against it.

Overall if you’re free to add any stats, I think that would introduce some complications in application… Now we need iterate over the list of stats to find which stat we want and there is a possibility that it won’t be there. There could be some crafty ways to do this but it just doesn’t seem to be advantageous to me.

In a traditional RPG Health / Mana aren’t really stats, they are a representation of your Vitality / Intelligence stat values, like a base value + value from stat (in Wow for example, 1 Stamina = 10 HP).

Health / Mana regeneration should be handled by ability scripts in my opinion, or a controller script that takes all bonuses from other ability/stats scripts and updates HP value in a single tick.

But I agree with you, I would also go with component approach with stats, but I would let items / abilities modify stats script directly. It simplifies modifiers because when you equip your sword, just GetComponent(), if your object has one increase +2 STR. When you unequip it, you just remove -2 STR back.

Or if you know your stats ahead and won’t be having many (something like Monster Hunter skill stats if you played perchance), you can just add them to your CharacterSheet script and modify them from there.

1 Like

There are clearly multiple ways to skin a cat, such as LaneFox’s solution or the composition solution I suggested, each with their pro’s and con’s. With composition, you could add a Health component that appears as a stat, but under the hood uses interfaces to Vitality to compute itself dynamically – whereas a different GameObject might use a different component to represent health as an actual value rather than computed dynamically. Depending on your project’s needs, this may be overkill, and putting them all in a CharacterSheet might be the most direct answer.

I thought about it more and I think I was having a hard time because I was taking the wrong approach; my stat had base, flat bonus, factor bonus, and total. Then I had a separate class “resource” with ‘current’. Having simply current and max seems much simpler. I’m not sure how the calculations will be done though since I would like flat bonuses to be applied before multipliers.

What about using UnityEvents to hook up calculations in the inspector? For each stat, you could hook up exactly which calculation methods you want to run.

1 Like

Thanks Tony. You always have great insight. :slight_smile:

Always a pleasure bouncing around ideas like this!

I support the composite design approach. Get you some attributes, modifiers, with some change events.

var health = new ModifiableValue<float> { baseValue = 100f };
health.PropertyChanged += (_, _) => Console.WriteLine($"Health is {health.value}.");
health.modifiers.Add(Modifier.Times(1.10f));
// Prints: Health is 110.
health.modifiers.Add(Modifier.Plus(5f, "+5 health"));
// Prints: Health is 115.

This is a library I made to tackle these kinds of issues. It’s MIT licensed.

1 Like