Search Unity

  1. If you have experience with import & exporting custom (.unitypackage) packages, please help complete a survey (open until May 15, 2024).
    Dismiss Notice
  2. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice

To use explicit typed classes or not to use explicit typed classes?

Discussion in 'Scripting' started by Ardenian, Feb 18, 2020.

  1. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Background: When I started getting into programming and computer science, it was Unity that led me to it. I loved how easy it is to create variations of prefabs and new instances of ScriptableObjects.

    However, while learning Java in college, I struggled because my mentors reinforced the idea of explicit typed classes, which we also do here, in Unity, when we use generic containers and such while also preserving inspector functionality. Such an example can be found here: Finally, a serializable dictionary for Unity!

    Their reasoning was and is: "If it is a type, we know what it is". This makes sense. It allows to add special behavior for certain derived classes, it makes it easy to differ and use specific types at certain locations. However, with it comes a huge workload.

    Example: Here is a little example to help understand my problem. Right now, I tinker around in Unity, for fun, writing an ARPG framework. There, each character has character stats which have a base value. Each character stat can be modfied, altering its maximum value. If no modifier is present, the base value of the stat is the maximum value.

    I solved this by introducing a ScriptableObject for the character stats. I then added a MonoBehaviour component to wrap a character stat and add it to characters. Each of these wrappers also know a collection that has all modifiers of that character stat.

    So far so good. Imagine these non-generic so far. However, I now struggled to link stat data (ScriptableObject) with the characters themselves, into the wrapper (MonoBehaviour). How do I know which wrapper holds which stat data? How do I index stat data based on the wrapper, to allow modifiers to access them per character?

    Eventually, I settled for generics. I made my character stat generic, I made the wrapper generic. Then I explicit typed both of them. Looks kinda like this:

    Code (CSharp):
    1.  
    2. public class CharacterStat : ScriptableObject {}
    3. public class CharacterStat<T> : CharacterStat { /*...*/ }
    4. public class VitalityStatData : CharacterStat<VitalityStatData> {}
    5.  
    Code (CSharp):
    1.  
    2. public class CharacterStatWrapper<T> : MonoBehaviour where T : CharacterStat
    3. {
    4.     [SerializeField]
    5.     private T instance;
    6. }
    7. public class VitalityCharacterComponent : CharacterStatWrapper<VitalityStatData> {}
    8.  
    The joke here is that I can use
    gameObject.GetComponent<CharacterStat<VitalityStatData>>()
    to get the vitality component of a unit without indexing the wrappers somewhere or checking which one is the one I need. This allows me to select which stat a modifier should reference. However, if you paid attention, a modifier would get an instance of a ScriptableObject. Thus, I have to make the modifier generic as well:

    Code (CSharp):
    1.  
    2. public class Modifier<T> : ScriptableObject where T : CharacterStatWrapper {} // there are no wildcards in C#, are there? So I wouldn't have to have CharacterStat as base class
    3. public class VitalityModifier : Modifier<VitalityStatData> {}
    4.  
    Now in Modifier<T> I can write myself some method:

    Code (CSharp):
    1.  
    2. public void Apply(GameObject target, T component) where T : CharacterStatWrapper
    3. {
    4.     target.GetComponent<CharacterStat<T>>().Add(this);
    5. }
    6.  
    Nonetheless, this feels kinda odd, though it is hard to pinpoint why.

    Conclusion: Now as you can imagine, this is a lot of work. A game like an ARPG easily has like, dunno, 30-50 stats. I would need to write a stat definition for each of them, a wrapper and a modifier, as well as who knows what else comes up. Then, when I create my unit instances, I would have to create a new stat data instance for every unit type, though assuming that unit variants share the same data.

    So is this is whole "generic, but explicit typed" approach a good idea? It does appear to be working a little bit against what Unity is offering when it comes to workflow, however, besides that I see more advantages than drawbacks. In particular when it comes to indexing and knowing which object needs what, because the types already solve that and you already know what you have in front of you instead of having to guess by its name (ScriptableObject instances). What is your take on this subject?
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,554
    This sounds like overkill. Why do you need these generic classes? What is the point of all this? Can you show us the implementation of some of these so we can get a better understanding of your design?
     
    Bunny83 likes this.
  3. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    I try to describe it with a more background now. It is quite a big package and it is hard to break it into little pieces so that others are able to comprehend my problem.

    Currently, I try to build a character stat/attribute/value framework.
    • A character is an entity controlled either by the player or an AI (or someting in that direction). It interacts with other entities, by walking, spellcasting, attacking, talking and other actions.
    • A character stat is a value associated with such an entity, used to describe the state of said entity. A "Vitality" stat could provide information about how healthy an entity is, "Energy" how much power is left for the entity to use weapons or other gadgets and so on.
    As for implementation, a character is a GameObject with components, that's in stone, but character stats are open to design. While tinkering around, one of the central questions that kept appearing is the ability to modify such stats while preserving the original value. Such an object is a modifier. Examples for such a modifier could be:
    • "The Vitality [stat] points of this character is increased by 100 bonus points".
    • "The total Vitality [stat] points of this character are increased 10%".
    This creates the need for a wrapper component. We already know that a character has an associated value with this stat Vitality, but now this value is altered, while we want to preserve the original value. As a result, a character has a bunch of components, with each component wrapping a particuar stat.

    So far, so good. However, now one of my main problems, which leads to my overkill solution with explicit typed objects, is how do I reference a stat? Let's say a stat is a ScriptableObject, the wrapper knows exactly one instance of such a stat, let's call it "DefaultPlayerVitality". My player character has therefore a component with this stat instance referenced.

    Now my next goal is creating a modifier for this stat. I would like to be able to add a modifier to this wrapper, this stat component. How do I tell the modifier, a ScriptableObject, that it is supposed to reference the Vitality component of my character? I can't use "DefaultPlayerVitality" because this is an instance and only my player character uses it. Another character might use "DefaultFoeVitality". You might see where I am getting here. I basically need to tell my modifier "Go modify Vitality!", but such a reference cannot be serialized in vanilla editor, can it? It is important to mention that the modifier does not know which target GameObject and thus which stat instance it is dealing with. I can't just reference them in the Inspector ( think of buffs from games, a character is slowed, modifiying its MovementSpeed stat value. The buff gets added to the target and later on, at some point, removed again).

    An easy solution would be introducing an enumeration that includes every stat and then a dispatcher uses the enumeration value from my modifier to grab the right stat component from the target character. However, this is something that I don't want to do because I don't want this structure to rely on the enumeration for various reasons, including but not limited to making changes to it and breaking serialization in the process.

    However, now I am at loss if I don't use the overkill solution. Somehow, I need to tell my modifier that it should target a specific stat component type. It does boil down to something like a
    TypeSpecifier
    property allowing me to choose a type deriving from the stat component base class, but that is too loose for my taste. Therefore, I am kinda stuck now.
     
    Last edited: Feb 18, 2020
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,554
    Question...

    What are you doing with these ScriptableObject's object identity?

    Like... if you drag a SO onto the wrapper behaviour. What makes the SO unique to that entity? Because SO instances are shared unless duplicated. If you have a single SO in your asset and you drop it onto multiple entities, they all have the same SO/stat.
     
  5. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    SO instances act as data references.

    Think of an enemy type "Arachnid". For some reason, there are dozens of arachnid variants (different prefabs) in the game which all share the same stat values, they all have the same Vitality base value, for instance.

    Using a shared SO instance here, it allows me not only to edit the stat value for all of these unit types, it also saves a lot of memory because not every prefab instance has its own clone of the data, as it is with prefabs, but referencing the SO instance with the value. I do know that Prefab Variants are able to offer the edit functionality, however, I expect these stats to become somewhat large in memory consumption for various reasons, so I do not want every instantiated prefab to have its own clone of the data.

    Inspired by 3 cool ways to architect your game with Scriptable Objects.
     
  6. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,648
    If I were making an RPG, I wouldn't bother wrapping any of the stats in any sort of explicit type. I would just have one generic stat type and use a dictionary-like collection to store them. To be honest, I don't really understand your example as to why having explicit types would be an advantage.
     
  7. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,554
    I think I understand what you're going for... and if so, what about this? :

    Code (csharp):
    1.  
    2. public abstract class EntityStat : ScriptableObject
    3. {
    4.  
    5.  
    6.  
    7. }
    8.  
    9. public abstract class EntityStatBehaviour<TStat, TBehaviour> : MonoBehaviour where TStat : EntityStat where TBehaviour : EntityStatBehaviour<TStat, TBehaviour>
    10. {
    11.  
    12.     [SerializeField]
    13.     private TStat _statData;
    14.  
    15.     [System.NonSerialized]
    16.     private HashSet<StatModifier<TStat, TBehaviour>> _modifiers = new HashSet<StatModifier<TStat, TBehaviour>>();
    17.  
    18.     //translator property to resolve the generic type
    19.     protected abstract TBehaviour StatBehaviour { get; }
    20.  
    21.     public TStat StatData
    22.     {
    23.         get { return _statData; }
    24.         set { _statData = value; }
    25.     }
    26.  
    27.     public void AddModifier(StatModifier<TStat, TBehaviour> modifier)
    28.     {
    29.         if (_modifiers.Contains(modifier)) return;
    30.  
    31.         _modifiers.Add(modifier);
    32.         modifier.Apply(this.StatBehaviour);
    33.     }
    34.  
    35.     public void RemoveModifier(StatModifier<TStat, TBehaviour> modifier)
    36.     {
    37.         if(_modifiers.Remove(modifier))
    38.         {
    39.             modifier.Remove(this.StatBehaviour);
    40.         }
    41.     }
    42.  
    43.     public IEnumerable<StatModifier<TStat, TBehaviour>> GetModifiers()
    44.     {
    45.         return _modifiers;
    46.     }
    47.  
    48. }
    49.  
    50. public abstract class StatModifier<TStat, TBehaviour> : ScriptableObject where TStat : EntityStat where TBehaviour : EntityStatBehaviour<TStat, TBehaviour>
    51. {
    52.  
    53.     public abstract void Apply(TBehaviour behaviour);
    54.  
    55.     public abstract void Remove(TBehaviour behaviour);
    56.  
    57. }
    58.  
    59. public static class StatExtensionMethods
    60. {
    61.  
    62.     public static void AddModifier<TStat, TBehaviour>(this GameObject go, StatModifier<TStat, TBehaviour> modifier) where TStat : EntityStat where TBehaviour : EntityStatBehaviour<TStat, TBehaviour>
    63.     {
    64.         var stat = go.GetComponent<EntityStatBehaviour<TStat, TBehaviour>>();
    65.         if (stat != null)
    66.         {
    67.             stat.AddModifier(modifier);
    68.         }
    69.     }
    70.  
    71.     public static void RemoveModifier<TStat, TBehaviour>(this GameObject go, StatModifier<TStat, TBehaviour> modifier) where TStat : EntityStat where TBehaviour : EntityStatBehaviour<TStat, TBehaviour>
    72.     {
    73.         var stat = go.GetComponent<EntityStatBehaviour<TStat, TBehaviour>>();
    74.         if (stat != null)
    75.         {
    76.             stat.RemoveModifier(modifier);
    77.         }
    78.     }
    79.  
    80. }
    81.  
    And an implementation:
    Code (csharp):
    1.  
    2. [CreateAssetMenu(fileName = "VitalityStat", menuName = "Stat Helper/VitalityStat")]
    3. public class VitalityStat : EntityStat
    4. {
    5.  
    6.     public float SomeData; //this is some random data that is unique to VitalityStat
    7.  
    8.  
    9. }
    10.  
    11. public class VitalityStatBehaviour : EntityStatBehaviour<VitalityStat, VitalityStatBehaviour>
    12. {
    13.  
    14.     public float CurrentStat; //state that can be modified by modifier
    15.  
    16.     protected override VitalityStatBehaviour StatBehaviour { get { return this; } }
    17.  
    18. }
    19.  
    20. [CreateAssetMenu(fileName = "VitalityModifier", menuName = "Stat Helper/VitalityModifier")]
    21. public class VitalityModifier : StatModifier<VitalityStat, VitalityStatBehaviour>
    22. {
    23.  
    24.     public override void Apply(VitalityStatBehaviour behaviour)
    25.     {
    26.         //modify the behaviour
    27.         behaviour.CurrentStat = behaviour.StatData.SomeData + 10f;
    28.     }
    29.  
    30.     public override void Remove(VitalityStatBehaviour behaviour)
    31.     {
    32.         //remove the modification from the behaviour
    33.         behaviour.CurrentStat = behaviour.StatData.SomeData;
    34.     }
    35.  
    36.  
    37. }
    38.  
    And here's me adding a modifier to a gameobject and looping them:
    Code (csharp):
    1.  
    2. public class zTest01 : MonoBehaviour
    3. {
    4.  
    5.     public VitalityModifier modifier;
    6.  
    7.     void Start()
    8.     {
    9.         this.gameObject.AddModifier(modifier);
    10.         foreach(var modifier in this.GetComponent<VitalityStatBehaviour>().GetModifiers())
    11.         {
    12.             Debug.Log(modifier.name);
    13.         }
    14.     }
    15.  
    16. }
    17.  
     
    Ardenian likes this.
  8. kdgalla

    kdgalla

    Joined:
    Mar 15, 2013
    Posts:
    4,648
    What I'm wondering, though, is why there needs to be VitalityStat type, rather then Vitality simply being an instance of EntityStat.
     
    lordofduct likes this.
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,554
    I'm wondering a lot of things too. I really do wish they'd just show us their current implementation to better wrap my head around what it is they're attempting to accomplish and what freedoms they want.

    Like for instance... maybe VitalityStat might have members that aren't in say AttackStat. For instance Vitality might just have a 'health' value, but Attack might have Strength and Speed?
     
  10. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    At first, this confused me a lot, too. Why explicit typing when generic types are so much easier to create and maintain? The answer is, there are a lot of If-Then and Maybe-Or-Not involved. I am trying to find an example to give some insight.

    Your solution works fine, but requires hashing. Hashing is expensive. With explicit types, you can reference and don't have to hash to get access to the stat itself. keep in mind that stats are going to be accessed a lot, possibly multiple times per frame. Having to hash every time because completely different objects try to access it is very slow.

    Trying to find an example, explicit typing allows you to freely extend any type, for any reason. If you used generic definitions, you have to have objects that look exactly the same. Well, you could introduce generic members that enable more modularity, but this would quickly get out of hands. One of your stats need a particular additional member of a type that no other one needs? Now all your other generic stats are stuck with references to null, except the one that actually needs it. If I come across an actual example, I will make sure to post it here.

    This is my idea, yes. It allows to freely build your individual stats and their structure, if needed.

    This looks very good, thank you! Some parts of the structure are almost identical to my own code, others differ. I like a lot how you solved handling adding and removing modifiers to behaviors.

    One question regarding the property
    StatBehaviour
    . You commented that it is needed to handle the generic nature of the component, but is it? I don't see you using it anywhere in the code, at first I thought one need it to get the connection between stat and modifier, however, the extension method already does that, with
    var stat = go.GetComponent<EntityStatBehaviour<TStat, TBehaviour>>();
    , not requiring the translator. Would you mind going into detail with the translator, please, and its benefits?

    There is only one little detail missing in my implementation, then I can share a little example project in Unity that works and everything. It should be ready before the end of the week. I appreciate a lot that you guys take the time to comment here and help me with my problem!
     
  11. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,554
    Note that it's protected, so it's really only used in the EntityStatBehaviour itself. Specifically here:
    Code (csharp):
    1.  
    2.     public void AddModifier(StatModifier<TStat, TBehaviour> modifier)
    3.     {
    4.         if (_modifiers.Contains(modifier)) return;
    5.  
    6.         _modifiers.Add(modifier);
    7.         modifier.Apply(this.StatBehaviour);
    8.     }
    9.  
    10.     public void RemoveModifier(StatModifier<TStat, TBehaviour> modifier)
    11.     {
    12.         if(_modifiers.Remove(modifier))
    13.         {
    14.             modifier.Remove(this.StatBehaviour);
    15.         }
    16.     }
    17.  
    It allows us to have 'this' typed appropriately.

    Since StatModifier<TStat, TBehaviour> expects 'this' typed as TBehaviour, but in the scope of EntityStateBehaviour we don't know that this IS a TBehaviour (technically speaking our constraints to enforce that 'this' is TBehaviour, only that TBehaviour inherits from EntityStateBehaviour). So this is our cast for it.

    We could just say "this as TBehaviour" when call Apply or Remove. BUT technically speaking since the constraint doesn't enforce that this is TBehaviour, we're not actually certain. Say someone inherits from EntityStatBehaviour and composites another TBehaviour. Sure, you wouldn't do this as it's kind of dumb... but the compiler doesn't know that.
     
  12. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,554
    Also. I modified it further so that you can have a non-generic base type. Which is useful with the Unity serializer (especially in older version) where it doesn't like dealing with generic types.

    Code (csharp):
    1.  
    2. public abstract class EntityStat : ScriptableObject
    3. {
    4.  
    5.  
    6.  
    7. }
    8.  
    9. public abstract class EntityStatBehaviour : MonoBehaviour
    10. {
    11.  
    12.     public bool AddModifier(StatModifier modifier)
    13.     {
    14.         return AddModifierGenerically(modifier);
    15.     }
    16.  
    17.     public bool RemoveModifier(StatModifier modifier)
    18.     {
    19.         return RemoveModifierGenerically(modifier);
    20.     }
    21.  
    22.     public IEnumerable<StatModifier> GetModifiers()
    23.     {
    24.         return GetModifiersGenerically();
    25.     }
    26.  
    27.     protected abstract bool AddModifierGenerically(StatModifier modifier);
    28.     protected abstract bool RemoveModifierGenerically(StatModifier modifier);
    29.     protected abstract IEnumerable<StatModifier> GetModifiersGenerically();
    30.  
    31. }
    32.  
    33. public abstract class EntityStatBehaviour<TStat, TBehaviour> : EntityStatBehaviour where TStat : EntityStat where TBehaviour : EntityStatBehaviour<TStat, TBehaviour>
    34. {
    35.  
    36.     [SerializeField]
    37.     private TStat _statData;
    38.  
    39.     [System.NonSerialized]
    40.     private HashSet<StatModifier<TStat, TBehaviour>> _modifiers = new HashSet<StatModifier<TStat, TBehaviour>>();
    41.  
    42.     //translator property to resolve the generic type
    43.     protected abstract TBehaviour StatBehaviour { get; }
    44.  
    45.     public TStat StatData
    46.     {
    47.         get { return _statData; }
    48.         set { _statData = value; }
    49.     }
    50.  
    51.     public bool AddModifier(StatModifier<TStat, TBehaviour> modifier)
    52.     {
    53.         if (_modifiers.Contains(modifier)) return false;
    54.  
    55.         _modifiers.Add(modifier);
    56.         modifier.Apply(this.StatBehaviour);
    57.         return true;
    58.     }
    59.  
    60.     public bool RemoveModifier(StatModifier<TStat, TBehaviour> modifier)
    61.     {
    62.         if(_modifiers.Remove(modifier))
    63.         {
    64.             modifier.Remove(this.StatBehaviour);
    65.             return true;
    66.         }
    67.         else
    68.         {
    69.             return false;
    70.         }
    71.     }
    72.  
    73.     public new IEnumerable<StatModifier<TStat, TBehaviour>> GetModifiers()
    74.     {
    75.         return _modifiers;
    76.     }
    77.  
    78.     protected override bool AddModifierGenerically(StatModifier modifier)
    79.     {
    80.         var concrete = modifier as StatModifier<TStat, TBehaviour>;
    81.         if (concrete != null) return this.AddModifier(concrete);
    82.         else return false;
    83.     }
    84.  
    85.     protected override bool RemoveModifierGenerically(StatModifier modifier)
    86.     {
    87.         var concrete = modifier as StatModifier<TStat, TBehaviour>;
    88.         if (concrete != null) return this.RemoveModifier(concrete);
    89.         else return false;
    90.     }
    91.  
    92.     protected override IEnumerable<StatModifier> GetModifiersGenerically()
    93.     {
    94.         return _modifiers;
    95.     }
    96.  
    97.  
    98. }
    99.  
    100. public abstract class StatModifier : ScriptableObject
    101. {
    102.  
    103.     public abstract System.Type GetBehaviourType();
    104.  
    105. }
    106.  
    107. public abstract class StatModifier<TStat, TBehaviour> : StatModifier where TStat : EntityStat where TBehaviour : EntityStatBehaviour<TStat, TBehaviour>
    108. {
    109.  
    110.     public override Type GetBehaviourType()
    111.     {
    112.         return typeof(TBehaviour);
    113.     }
    114.  
    115.     public abstract void Apply(TBehaviour behaviour);
    116.  
    117.     public abstract void Remove(TBehaviour behaviour);
    118.  
    119. }
    120.  
    121. public static class StatExtensionMethods
    122. {
    123.  
    124.     public static bool AddModifier<TStat, TBehaviour>(this GameObject go, StatModifier<TStat, TBehaviour> modifier) where TStat : EntityStat where TBehaviour : EntityStatBehaviour<TStat, TBehaviour>
    125.     {
    126.         var stat = go.GetComponent<EntityStatBehaviour<TStat, TBehaviour>>();
    127.         if (stat != null)
    128.         {
    129.             return stat.AddModifier(modifier);
    130.         }
    131.         else
    132.         {
    133.             return false;
    134.         }
    135.     }
    136.  
    137.     public static bool AddModifier(this GameObject go, StatModifier modifier)
    138.     {
    139.         var stat = go.GetComponent(modifier.GetBehaviourType()) as EntityStatBehaviour;
    140.         if (stat != null)
    141.         {
    142.             return stat.AddModifier(modifier);
    143.         }
    144.         else
    145.         {
    146.             return false;
    147.         }
    148.     }
    149.  
    150.     public static bool RemoveModifier<TStat, TBehaviour>(this GameObject go, StatModifier<TStat, TBehaviour> modifier) where TStat : EntityStat where TBehaviour : EntityStatBehaviour<TStat, TBehaviour>
    151.     {
    152.         var stat = go.GetComponent<EntityStatBehaviour<TStat, TBehaviour>>();
    153.         if (stat != null)
    154.         {
    155.             return stat.RemoveModifier(modifier);
    156.         }
    157.         else
    158.         {
    159.             return false;
    160.         }
    161.     }
    162.  
    163.     public static bool RemoveModifier(this GameObject go, StatModifier modifier)
    164.     {
    165.         var stat = go.GetComponent(modifier.GetBehaviourType()) as EntityStatBehaviour;
    166.         if (stat != null)
    167.         {
    168.             return stat.RemoveModifier(modifier);
    169.         }
    170.         else
    171.         {
    172.             return false;
    173.         }
    174.     }
    175.  
    176.     public static IEnumerable<StatModifier<TStat, TBehaviour>> GetStatModifiers<TStat, TBehaviour>(this GameObject go) where TStat : EntityStat where TBehaviour : EntityStatBehaviour<TStat, TBehaviour>
    177.     {
    178.         var stat = go.GetComponent<EntityStatBehaviour<TStat, TBehaviour>>();
    179.         return stat != null ? stat.GetModifiers() : System.Linq.Enumerable.Empty<StatModifier<TStat, TBehaviour>>();
    180.     }
    181.  
    182.     public static IEnumerable<StatModifier> GetAllStatModifiers(this GameObject go)
    183.     {
    184.         foreach(var c in go.GetComponents<EntityStatBehaviour>())
    185.         {
    186.             foreach(var m in c.GetModifiers())
    187.             {
    188.                 yield return m;
    189.             }
    190.         }
    191.     }
    192.  
    193. }
    194.  
     
  13. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    I see, thanks a lot for explaining!

    I got set back on my solution, because I tried to implement something that I need later and it messed up my implementation, trying to implement my modifier container logic, which you use a hashset for. It appears that I ran into this issue: Serializing nested generic types in concrete classes

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public interface IValue
    6. {
    7.     float Value { get; }
    8. }
    9.  
    10. public abstract class StatComponent<TStat, TModifier, TLayers> : MonoBehaviour
    11.     where TStat : CharacterStat<TStat>
    12.     where TModifier : CharacterStatModifier<TStat>, IValue
    13.     where TLayers : Enum
    14. {
    15.     [SerializeField]
    16.     private TStat stat;
    17.  
    18.     [SerializeField]
    19.     private LayerContainer modifiers;
    20.  
    21.     [Serializable]
    22.     public class LayerContainer : IValue
    23.     {
    24.         [SerializeField]
    25.         private CustomCollection[] collections;
    26.  
    27.         public LayerContainer()
    28.         {
    29.             int size = Enum.GetValues(typeof(TLayers)).Length;
    30.             this.collections = new CustomCollection[size];
    31.  
    32.             for (int index = 0; index < size; index++)
    33.             {
    34.                 collections[index] = new CustomCollection();
    35.             }
    36.         }
    37.  
    38.         public void Add(TModifier item, TLayers layer)
    39.         {
    40.             int index = Convert.ToInt32(layer);
    41.             collections[index].items.Add(item);
    42.             collections[index].isDirty = true;
    43.         }
    44.  
    45.         public bool Remove(TModifier item, TLayers layer)
    46.         {
    47.             bool removed = false;
    48.  
    49.             int index = Convert.ToInt32(layer);
    50.             removed = collections[index].items.Remove(item);
    51.             if (removed) collections[index].isDirty = true;
    52.  
    53.             return removed;
    54.         }
    55.  
    56.         public float GetValue(TLayers layer)
    57.         {
    58.             return GetValue(Convert.ToInt32(layer));
    59.         }
    60.  
    61.         private float GetValue(int layerIndex)
    62.         {
    63.             float sum = 0.0f;
    64.  
    65.             if (collections[layerIndex].isDirty)
    66.             {
    67.                 for (int index = 0; index < collections[layerIndex].items.Count; index++)
    68.                 {
    69.                     sum += collections[index].items[index].Value;
    70.                 }
    71.  
    72.                 collections[layerIndex].isDirty = false;
    73.             }
    74.  
    75.             return sum;
    76.         }
    77.  
    78.         public float Value
    79.         {
    80.             get
    81.             {
    82.                 float sum = 0.0f;
    83.  
    84.                 for (int layerIndex = 0; layerIndex < collections.Length; layerIndex++)
    85.                 {
    86.                     sum += GetValue(layerIndex);
    87.                 }
    88.  
    89.                 return sum;
    90.             }
    91.         }
    92.     }
    93.  
    94.     [Serializable]
    95.     public class CustomCollection
    96.     {
    97.         [SerializeField]
    98.         public List<TModifier> items;
    99.  
    100.         [SerializeField]
    101.         public float value;
    102.  
    103.         [SerializeField]
    104.         internal bool isDirty;
    105.  
    106.         public CustomCollection()
    107.         {
    108.             this.items = new List<TModifier>();
    109.             this.value = 0.0f;
    110.             this.isDirty = true;
    111.         }
    112.     }
    113. }
    My property
    modifiers
    refuses to be serialized. The thoughts behind this container is that modifiers are added to a specific layer of modification. Let's say there are layers Base, Bonus and Total, then there are modifiers which are altering the Base value in the Bonus layer, others might alter the value in the Total layer, reflecting:
    • "Increases the base vitality of your character by 10 points."
    • "Your character receives 50 bonus vitality."
    • "Increases your total vitality by 10%."
    However, I could not quite nail down why Unity refuses to serialize this collection containing the layers of modifiers, not showing it in the inspector. It does not make sense from my point of knowledge. At first I thought it might be because of using
    TModifier
    in
    CustomCollection
    , but even using non-generic types there did not enable serialization.

    As it appears to be always the case with such problems, I did not commit my code to source control before applying thse changes, so there are a lot of scorched files now. I might need to do a hard reset and use your solution as a base. After all, our code lines only differ in how we add and remove modifiers from our stats.

    Looks great, thank you! I have had a fair share of problems with that issue, too.
     
    Last edited: Feb 19, 2020
  14. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,998
    I think part of your idea is a respectable and old one. "Hungarian Notation" might be the best explanation. The real version says that if you have pounds and ounces, both floats, you should have some way to distinguish them: pnd_n1+ozc_n3 would look wrong.

    Then, for example, C++ (I think) can easily make it an error. You can define
    pound_t
    and
    ounce_t
    as ints, declare
    pound_t n;
    and
    ounce_t k;
    , and have n+k give you a perfect "can't add pounds and ounces error", even though you're adding a pair of ints. I believe other old approaches involve classes and operator overloading, which seems like your approach. I feel like if you tried this in Python, say, it would be easier. Not every language is good at everything.

    My other thought is that everyone else is right. The people doing this were researchers. For a bank, a big framework with lots of safety might pay off. But in a game, anything simple that works is fine. I mean, think of the most complicated RPG you ever played -- even the 5th update wouldn't have all that many different stats and mechanics. And plenty of popular games handled, say, poison "wrong" for 6 months (then players complained when it was fixed).
     
  15. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    I finally got around importing your code and playing around with it. I learned a lot from it, thank you, @lordofduct! One essential thing I learned is about using abstract base classes. I already use them a lot, but I usually end up implementing things in the base class. Your code showed me how useful it can be to leave methods abstract and leave the implementation to derived classes. I already know one or another place where I can put this information to good use, previously struggling.

    I got to say, I miss having a coding mentor. When I started coding, I had a friend that would teach me things occasionally and that one could bug now and then, with questions and what-ifs. He moved away eventually and I didn't find such a relation again, with something being there to ask. I heard Discord has a Unity place as well, I might try that one out.

    I agree. It is easy to lose oneself in generalization and design optimization when in the end, it is merely about glueing few components together, not needing the scalability that another design might offer, making trying to get there pointless. Nonetheless I do enjoy doing this now and then, even if it is only for learning new things.
     
  16. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,998
    That's funny since it's usually the other way. It's often nice to implement the default in a base class, and it's never a hardship doing it. You never _need_ abstract virtual functions -- they're more of an error-checking thing. For example:
    Code (CSharp):
    1. class MapItem { // base class for anything that can be on the map
    2.  
    3.   virtual public bool isDestroyed() { return false; } // most common answer
    4.   // most sub-classes can't be destroyed, and will use this
    5.  
    6.   virtual public void takeDamage(int d) {} // a do-nothing body is a cheap, fake-abstract
    7.   // most items don't take damage, most will skip implementing it
    8.  
    9.   abstract public void setTarget();
    10.   // everyone will want to implement this. An error not to is what we want
    Everyone is free to override isDestroyed and takeDamage. Most won't -- the bodies save us lots of time -- but those bodies don't force us to do or not do anything. Down in setTarget we don't think anyone, or hardly anyone, would want an empty body. It's a tiny bit safer to make it abstract and get a reminder if we forget.

    The most common complaint is that Java/C# Interfaces don't allow the convenience of a default.
     
    Ardenian likes this.
  17. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Does overriding virtual methods work in Unity? I remember using them in the context of custom editors and context menues and they would always call the virtual function in the non-abstract base class, never the overriden function in the derived class. I have to admit, this is still on my list of testing and trying to see what works and what does not, so thanks for bringing it up, I am going to do some testing with the code that I tinkered out with @lordofduct.

    In C#, default implementations for interfaces are a thing in C# 8 I think. It is funny, I recall reading a huge discussion on a Microsoft Github repository about whether this should be a thing, with hundreds of replies. The argument against it basically was that in C#, interfaces are merely promises that an object behaves in a certain way, which is different to C++ headers. Seems whoever is developing C# decided for it now, though, adding default implementations in the newer C# versions. Multiple inheritance, inheritance from multiple base classes, is also going to be a thing. We are not going to see that in Unity for the next decade, though, probably.
     
  18. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,998
    I think everyone who knew inheritance first tries
    public void override Update()
    , since how else would a class inheriting from Monohavior work. That leaves a funny feeling that Unity mucks up virtual functions. But they work fine if you're not dealing with one of the magic words. I just double-checked one of my working editor scripts --
    public override void OnInspectorGUI() 
    and it's working fine.

    Yes, those people on github are correct; an interface is checked by the compiler and then it's nothing. 0 byes of executable. "Only a promise" is a common way to express that. But unless someone can write "and that's important because...". I just looked -- Java 8 lets you write bodies in interfaces now. The microsoft C# page also mentions Swift doing it (which is Apple's language, microsoft's greatest frenemy). So it seems the actual reason is "Gah! Now we have to do it since everyone else is".
     
  19. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Yep, that is a classic one! In my particular case, it was something like this:

    Code (CSharp):
    1. [ContextMenu("Generate Level")]
    2. protected virtual void GenerateInEditor()
    3. {
    4.     /*...*/
    5. }
    6.  
    When I created a derived class and override this method, it would still call the original function, which, on a second thought, makes sense, I guess, because it is that function having the attribute. Nonetheless, I recall adding the attribute to the overriden function doing nothing, not adding a context menu, so I was stuck with the base class implementation. However, last time I tried this was in Unity V2018, so who knows what is the current state of art.

    Can't say I like it, though it is certainly going to be an exciting feature. Should ease up one or another thing. One is allowed to be a bit salty about Microsoft following the "popular path" even though it does not quite fit into C-Sharp, as you pointed out with what interfaces are.
     
  20. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    This weekend, I tinkered around a little bit, allowing me to sharpen my problem. The explicit typed approach as posted by @lordofduct works, however, I could not befriend it because it blows up the workload on so many levels and in some cases, it is not even sure that an object always is this simple. I could imagine when it comes to something like
    DamageEffect
    , requiring
    FireDamageEffect
    and such, this approach could become less and less usable.

    As a result, I tried a hardcore generic approach as displayed in the attachments, excluding modifiers for now. Here, my underlying problem becomes clear, which is dispatching. Before I get into detail, I would like to mention that I previously mixed up, seeing
    CharacterValue
    and
    CharacterStat
    as equal. The difference between these two is, a
    CharacterValue
    is a value associated with a character, such as maximum hitpoints, whereas a
    CharacterStat
    alters such a
    CharacterValue
    or multiple
    CharacterValue
    . In other words, a
    CharacterStat
    grants/influences
    CharacterValue
    . An example for such stats can be found here: https://pillarsofeternity.gamepedia.com/Attributes

    That being said, I limited my example to
    CharacterValue
    for now, as it already exposes my problem. Dispatching I said earlier in this post, telling an object "You should reference/target that!". If you look on the attached screenshot, you notice that there is a component called
    CharacterValue
    which references
    CharacterValueData
    . In this case, it is the maximum life of a character. So far, so fine, however, now look on
    CharacterValues
    , which is a component that should eventually provide references to all other
    CharacterValues
    of this GameObject. Immediatly, you see the problem. It is just a
    CharacterValue
    and it does not know that it is Life
    . If you observe the object from the outside, you have no idea what it is and remember that you cannot just reference the component itself because the objects wanting to get the reference aren't instanced in the editor, but at runtime, such as a modifier or other objects accessing it. They somehow need to know what they should reference.

    This is the root of my problems. How do I tell a modifier that it should alter a
    CharacterValue
    , for instance the maximum life of a character? There is no way to tell where the life of this character is. How do I solve this? My first thought are enumerations, using its values as indices for the array in the container
    CharacterValues
    . Sounds good, right? However, think again. Each instance of
    CharacterValue
    (instance!!!) can be referenced. If I create a new instance, a new character value, I also have to edit my enumeration for it. It also does not solve the problem of not knowing what something is, only enabling referencing character values using the container.

    I am at loss how to solve this, frankly. It is kind of embarassing, because I study computer science in college and thus feel as if I should know how to solve it, but with any experience I have I can't find the solution for this that really "fits", if you know what I mean, finding the balance between workload and usability. My only idea right now is to write a new
    ScriptableObject
    definition or a custom class definition and using the
    [SerializeReferece]
    for it, something like
    CharacterValueTarget
    , explicit typing
    CharacterValue
    , dumping the generic approach, explicit typing the components themselves and removing the container for now (or introducing an enumeration that can be used with the container, but is independent from
    CharacterValueTarget
    ). More or less, I would end up with the same code as @lordofduct already posted, only "externalizing" the retrieval of components that hold the value.

    What I like about the new approach is its "double effect", meaning that on the one hand, I have explicit typed
    CharacterValue
    objects, allowing customization, on the other hand I have a generalized retrieval of these components, because I don't have to care about which instance of
    CharacterValueData
    I have. Let's say, a modifier would only need to know the
    CharacterValueTarget
    , which resolves retrieving the actual component without caring which underlying data instance there is. This would be a
    CharacterValueModifier
    , which would know the
    CharacterValueTarget
    , eliminating the need for something like
    LifeModifier
    .

    For now, I think this is what I am going for and I would greatly appreciate input and opinions on the matter. I kow that I am overthinking it a little bit, but this very problem has stuck with me since 2014 when I started with Unity: The inability to reference specific objects without losing the generic design of easy workflow.
     

    Attached Files:

    Last edited: Mar 1, 2020
  21. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,998
    That page looks pretty simple. Plenty of systems use Primary stats only to compute Secondary stats (partly based on class) which then determine the stats that actually do things.

    I think the traditional way is each stat has it's own modifier list, and each modifier has an extra back-pointer to the stat. That's pretty much the way Unity gameObjects and Components work. Any change to a modifier and you recompute the stat.
     
  22. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    This approach, I understand it for singleplayer, if you only have one character, but as soon as multiple instances have to use this approach, I don't understand how one would pull this off. Looking on
    UnityEvent
    for instance, you have to hardcode the target object. However, this is exactly what I don't have, in the editor, I do not know which instance is going to be the target.

    Without hardcoding the type into the modifier, I don't know how I could tell it that it references this stat and not that stat.

    I ended up doing something like this, though it is still under development:
    Code (CSharp):
    1. [code=CSharp]// each prefab has one of these, making it a character
    2. public class Character : MonoBehaviour
    3. {
    4.     [SerializeField]
    5.     private CharacterData data;
    6.  
    7.     public CharacterData Data { get => data; }
    8. }
    Code (CSharp):
    1. using UnityEngine;
    2. using System;
    3.  
    4. [Serializable]
    5. public abstract class CharacterPropertyReference
    6. {
    7.     public abstract float Value(CharacterData data);
    8. }
    9.  
    10. // this is the data that does not change at runtime
    11. [CreateAssetMenu()]
    12. public class CharacterData : ScriptableObject
    13. {
    14.     [SerializeField]
    15.     private float life;
    16.  
    17.     [Serializable]
    18.     public class LifePropertyReference : CharacterPropertyReference
    19.     {
    20.         public override float Value(CharacterData data) => data.life;
    21.     }
    22.  
    23.     // many many more fields with their reference classes
    24. }
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. using static CharacterData;
    4.  
    5. // this stores the current value at runtime, only referencing the data that does not change.
    6. public class CharacterVitals : MonoBehaviour
    7. {
    8.     [SerializeField]
    9.     private Character character;
    10.  
    11.     [SerializeField]
    12.     private CharacterPropertyReference maximumLife = new LifePropertyReference();
    13.     }
    14. }
    I haven't thought about modifiers in this context yet, however, I am likely going to introduce a similar class like CharacterPropertyReference which handles the targetting for me.Then, I do not have to hardcode into the modifier, using explicit typed classes, to define which property should be modified.
     
  23. Tatertots

    Tatertots

    Joined:
    Oct 18, 2013
    Posts:
    2
    Hey, I'm going thorugh similar issues - did you find a solution to this?