Let’s start with simple imaginary example, because it might be unclear what this thread is about.
There is boat or tract and it has 4 armament (turret) slots. Player is controlling the boat and can shoot from mounted guns to enemies. Boat parts have different components like Boat, Hull, Turret or Gun, and all of them can have different attributes like boat health, turret rotation speed, gun damage and all these attributes can have modifiers like “+20% of something”.
As you can see player tract/boat is not just simple human character with stats like intelligence, strength or resistance to fire, but it’s compound object and many of them can be present multiple times or not at all (turret).
In short there is no single source of stats, but the boat itself might have status effects or modules that produces modifiers for other components, eg. “-10% reload speed if boat HP is below 50%”. This is where problem starts.
It is hard to design system/script that assigns modifiers where it wants, I mean how does the status effect knows where the hull or gun is? What if gun is destroyed or replaced? What if there is other special component with attributes - enemy boat might have different object structure or even different set of stats. This is even worse if there are conditions like “Water guns shoot 10% faster” - attribute must be aware from where it comes to decide if it can be applied.
At the beggining I was thinking about serializable objects with base value and list of modifiers ( something similar to this ), but with all these problems I am really stuck and I don’t know where to go now. I will appreciate any tips or design patters that could help me to find the solution.
Have a base class called “ShipComponent” or something that you can inherit from, each ShipComponent can have a list of StatusEffects each with their own multiplier.
If your Hull (which inherits from ShipComponent) is damaged, then reduce X StatusEffect to -1% or whatever.
Your ShipComponents could be stored in a List or some other multiple storage mechanism, whatever best fits your purpose I guess, this way you can have a Hull stored as ShipComponent.
Thank you for the input!
You mean to create class (ShipComponent) that is responsible for status effects and other stuff, so instead of single source of status effects and bonuses to have them scattered too? Interesting idea, but can this fix the problem of “finding” stats?
For example there is hull module with bonus “+10% damage to water guns”, it could search the list of all components, but to make it strong typed all stats must be contained in this (gun) component. If there is gun with special ammo or different setup like gun charge or anything that is not common to all guns it will not be possible to set/find.
I mean I have separate MonoBehaviours to do things and some other events can modify what happens, like n’th shot has different stats driven from other component and this component must be aware of this too. It would mean that every single class must inherit from ship component to be considered in all this and modules or status effects must also consider this - I would need huge cast swtich (checking type) for each possible component.
Let me know if this makes sense, maybe I overcomplicated my explanation.
ShipComponent is just a base class for your ships components, e.g. Hull.
Hull will inherit from ShipComponent.
ShipComponent will contain a Dictionary<StatusEffect, float> (StatusEffect could be some form of enum? float is the percentage effect that this StatusEffect has, e.g. StatusEffect.EXTRA_DAMAGE will be stored against 1.5, so this ship will deal 150% of it’s normal damage.)
Store the ShipComponents in a List in another class, say, Ship? So:
Ship contains many ShipComponents, ShipComponent contains many StatusEffects
Ok, but Hull has maxHP and it’s not interested in EXTRA_DAMAGE. I mean obviously it could just ignore it, but what if there is condition: “-1% reload of water guns for each missing 1% of HP”, in this case only some (water) guns should be affected, plus the effect is based on the other component (hull durablity).
Yeah, but that’s my point - script that produces the modifier must actually know this, so it must search for all guns and for health, cast them and do it again and again, but the thing is gun is not the only script that has damage attribute, there will be something else that has it, so it would mean that there must be huge check for any potential scripts interested in that stat.
In fact, in my case gun has no damage attribute, it’s separate component that is responsible for that and to know it’s “Water gun” it would need reference to that gun and know everything about other components too - this system sounds very rigid and complicated.
If many different things have health, but not everything, and those things are not necessarily related to each other, then you could use Interfaces to implement them. Suppose you have an interface, IHealth or maybe IDamageable, then each component that can be damaged will implement IDamageable. You can then search the list of components for all IDamageables when you want to deal damage or calculate health-related effects. Weapons with special effects/damage types can implement interfaces like IWaterDamager. I wouldn’t over modularize the system until you know you need more modularity, though, because that can make it more difficult to understand and work with.
What you said is true, but it does not solve the problem of stats. What I wanted is almost exactly what you can find in link in the first post, but I think it’s not possible to do with modularity of components. This is why I try to find some design pattern that can help me to deal with it or even completely different way to approach the problem.
Maybe this is just not doable?
It’s definitely doable, consider rewriting your existing approach to better suit this application, it sounds like you’ve built the rest of the application without keeping your end goal in mind. If you find that something is ‘not possible’ then it’s because you’ve done something wrong previously.
See if you can create a class diagram first and adjust your implementations as such.
Hm, in my case this is not needed as bonuses would be recreated from modules (scripts) on load and status effects are more like buffs, so they shouldn’t persist, however this is good point. I think there would be predefined bonus anyway, like scriptable object or monobehaviour, and there is reference to that object/prefab.
Ok, so I will throw my current thoughts on the table:
Possibly my case does not need super modularity and it should be expected that some component exists always in some place - there is expected some structure or rules. Furthermore, I think context of attrbutes is very important, at least in my case. It would be hard to create conditional bonus without context - if there is no way to get current HP it’s impossible to create “-10% reload speed if boat HP is below 50%”.
I think that not every component can be source or target of modifiers, there are special components for that, for example it would be expected that character is on the top and have reference to health component. It means there must be some key components managing that.
Also it’s not always possible to have single object representing attribute, I mean if I do “+5% damage”, then it’s possible to accumulate such bonus, but base damage might be unknown at this point, so there is no way to calculate final value without it.
Putting it together there would be some Entity class (aka Boat) as root for everything and it has reference to key modules with stats, in case of my example it would be health, turret or guns. If something applies status effect to the boat it could search for target components (as they are expected there) and modify stats, but possibly to make it less error prone it can be good idea to create modifiers or status effects dedicated for these modules (example guns). It requires more code, but it will be easier to manage modifiers and it would be harder to forget about some class. Whole thing is more component than attribute oriented, so there can be stat based on some special class with modifiers or just simple float field.
This also make it easier to react to events, for example “OnFireGun” can be handled with context and this allows to read some properties or even calculate custom stat.
I realized just now how to generalize the problem - it is composition vs inheritance. I am used to inheritance where the main goal is extending features with subclasses and objects have some structure defined by types and references, but Unity is based on components and my problem is I am not sure how to properly develop modular composition with the same capabilities. I understand the concept and I know how to separate features (like movement from health), but I have hard time when designing something that must communicate with other things, and this stats system is good example of such a case.