Search Unity

Question Referencing components on an unknown instance?

Discussion in 'Scripting' started by Ardenian, Apr 4, 2021.

  1. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    With the new LTS 2020.3, I read up on Adressables and how awesome they are. What intrigues me most is how one is even to reference sub-assets there. What I wonder now, is there a solution for adressables but instead of referencing an asset on the disk, you reference components on an instanced object, without knowing the instanced object itself yet?

    Consider this example, a character sheet in a RPG:
    • I have a character prefab that has a finite number of components that represent properties associated with the character, such as health, shield and energy.
    • These property components are not typed like that, however, there is only a generic
      Property
      component, resulting in
      FloatComponent
      for a float property,
      IntComponent
      for an int property and so on.
    • I have a facade component
      Character
      which references each individual property component under its appropriate name. It looks something like this:
    Code (CSharp):
    1. public class Unit : MonoBehaviour
    2. {
    3.     public Value<float> currentHealth;
    4.     //...
    5.  
    6.     public Property<float> maximumHealth;
    7.     public Property<float> maximumShield;
    8.     public Property<float> maximumEnergy;
    9.     //...
    10. }
    I use components for properties to be able to reference individual properties within the editor in other objects. Consider a healthbar object for instance which references the health property component of a character and displays its value.

    This works very well so far, but causes huge problems elsewhere. Coming from modding environments such as Starcraft 2, I would like to define very basic objects that do something elemental. An example for this could be a
    Buff
    object. Per my definition, a buff changes one or more properties on a unit, adding to its value or subtracting from it.

    The problem is, how do I define which properties a particular buff instance adds or subtracts from? I explicitely do not wish to hardcode this behaviour into my buff component that represents the buff, such as something like this would do:

    Code (CSharp):
    1. public class HealthBuff : MonoBehaviour
    2. {
    3.     private float value;
    4.  
    5.     public void Apply(Unit unit)
    6.     {
    7.         unit.maximumHealth.Value += value;
    8.     }
    9. }
    As you can imagine, this is not feasible, I would have to do this for every single property that exists on my character. Even further, other objects which interact with a particular property instance would have the same problem. Think about a
    Validator
    which checks if the value of a particular property on a particular character fulfills certain conditions. You would have to do this all over again. Even if you introduced something like a
    PropertyLink
    class that hardcodes getting a particular property from a character this still would require one to do this for every single property:

    Code (CSharp):
    1. public class Buff : MonoBehaviour
    2. {
    3.     private float value;
    4.     private PropertyLink link;
    5.  
    6.     public void Apply(Unit unit)
    7.     {
    8.         Property<float> property = unit.GetComponent(link.PropertyName) as Property<float>;
    9.         property.Value += value;
    10.     }
    11. }
    How do I solve this problem?

    My first thought was to have "named types", so instead of a bunch of
    FloatProperty
    I then would have
    MaximumHealth
    ,
    MaximumEnergy
    and so on, but creating such a huge load of new type definitions seems to not be overkill but also plain out wrong to me. If you had a
    Book
    class, you wouldn't create
    StephenKingsIt
    as a new class deriving from it, would you.

    What I basically look for is a solution to pseudo-reference a component on a GameObject with that GameObject not being known/created yet, but also without resorting to type names to resolve the reference once the instance is known. Do you have an idea what I could do to solve this?
     
    Last edited: Apr 4, 2021
  2. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    I looked a bit around some more but I couldn't find any clear solution. The problem seems to revolve around "identity by name versus identity by type" as well, whatever that means.

    In 2020.3, we can use C#8, so switching based on property name is one idea:
    Code (CSharp):
    1. [ExecuteInEditMode]
    2. [Serializable]
    3. public class MyClass : MonoBehaviour
    4. {
    5.     [SerializeField]
    6.     private string input;
    7.  
    8.     [SerializeField]
    9.     private float health;
    10.     [SerializeField]
    11.     private float shield;
    12.     [SerializeField]
    13.     private float energy;
    14.  
    15.     public void Test(string property)
    16.     {
    17.         switch (property)
    18.         {
    19.             case nameof(health):
    20.                 Debug.Log(nameof(health) + ":\\s" + health);
    21.                 break;
    22.             case nameof(shield):
    23.                 Debug.Log(nameof(shield) + ":\\s" + shield);
    24.                 break;
    25.             case nameof(energy):
    26.                 Debug.Log(nameof(energy) + ":\\s" + energy);
    27.                 break;
    28.             default: Debug.Log("There is no property with this name.");
    29.                 break;
    30.         }
    31.     }
    32.  
    33.     private void Update()
    34.     {
    35.         Test(input);
    36.     }
    37. }
    Not very maintainable though, since one has to make sure that wheresoever one inputs a string that represents a property name, that it matches the property name of the actual property since that one is not exposed to public.

    Another idea that I had was bundling several properties into smaller structs and then working with small bundles. This doesn't solve the problem once you do want to take a look at exactly one property, like in an event when the property value of exactly one property changes, one would have to drag all other properties with it in the event data as well, since they are bundled.

    Last but not least some shenanigans with using Reflection, but that kind of boils down to something similar as in the proposed solution with using property names in a switch statement.

    I feel like I am missing something fundamental in programming here. It is something that has been confusing me in Unity ever since I started tinkering around in it and it drives me crazy, like a last missing puzzle piece that I am missing before I can confidently dive into programming a game.
     
  3. Brathnann

    Brathnann

    Joined:
    Aug 12, 2014
    Posts:
    7,188
    Personally, if I had a simple number as all I wanted to track, I would possibly just make a single script. Let's just call it "Stat". This script could implement the ability to add to it, subtract from it, and do whatever else I needed to adjust that number.

    Then, I would set it to add as many of these components as I needed to a character object. But I add the components, I would save it to a dictionary with each key being the value the stat represents. So, "Health" would target the component tracking health.

    Then, when I wanted to modify a stat, I simply reference it from the dictionary, which would give me the Stat component that was tracking Health, for example, and I could pass that to the appropriate area to have it buff, deal damage, whatever.

    I can't say this solution is perfect, as I just sort of was thinking through it briefly.
     
  4. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Thank you for your reply! Is there a solution that doesn't require hashing as a means to resolve referencing? I am curious because if I know that a GameObject has a component, why would I need to look up for it. I mean, we do not know that a GameObject has a particular component, but it strikes me as odd that I have to resolve retrieving a component with hashing instead of getting it... more "deterministically", one could say?

    In C/C++, one could for instance remember at which offset in memory an object is located and then load the object at that point in memory with no need to hash anything to find out where an object is. Is there nothing like this in C#/Unity?

    The only thing that I can think about is something like storing the properties in an array, with each property type representing an index in an enumeration (health=0, shield=1, ...) and then retrieve the object from a particular index, although this would require inheritance and abstract base classes, which then in turn requires typecasting when storing properties of different types (float, int, bool, ...) in the same array, so I don't know if jumping through all these loops is worth it in the end, performance or design wise.
     
  5. Brathnann

    Brathnann

    Joined:
    Aug 12, 2014
    Posts:
    7,188
    Hashing? uhh...What do you mean you need to retrieve through hashing?

    Sorry, been a long time since I touched c++, so I don't really know much about it anymore.

    Components on gameobjects are instances of the class, so you use GetComponent to retrieve a reference. The reason I suggest you could use a dictionary is because as you add components, if you tie them to a key, then it's easy to retrieve and you don't have to worry about it. You just grab the value out of your dictionary using the key.

    The idea is similar to doing an array, but with an array I'd expect confusion could happen. How easy is it to remember what number corresponds to which stat?

    I may not be understanding enough of your desired results. I don't find Unity/c# that hard to program in with how it's designed within the Unity setting. So not sure if I'm the best option to help you.
     
  6. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Okay, let me start from a different point of view. The issue itself is not hard to understand, but I struggle to explain it to someone who doesn't know what I mean, I apologize.

    I have a character and a finite fixed number of properties for that character that keep track of values for that character, such as health, shield and energy. Now I have another object that wants to interact with exactly one particular property of that character, let's call it an effect. An effect could add to a property value, subtract from it or also just read it. The effect and its logic works exactly the same for all properties of the same type (float, int, bool, ...).

    Now how do I tell an effect that it is supposed to read the health property of a character? I can't use GetComponent<T> because I would have to select T somehow. I could use GetComponent(string) and somehow make sure that the names are correct by using editor extensions and inspector drawers.

    However, this doesn't change the fact that I have to find the component although I know that it is already supposed to be there. This also applies to your solution with the dictionary. I have to hash the key and return the value stored under the key, "find" it in a bunch of values. That's what I don't understand, why there is no way to directly grab the component.

    If I have a
    Character
    component, is there no way to introduce a constraint in one way or another that enforces that the same GameObject has a
    Health
    component /
    Property
    component representing health and then allows me to access it immediately without having to search it through a dictionary or other solutions with hashing?

    I delved into DOTS a bit and there they seem to hardcode properties, some examples I saw had stuff like
    Health
    and
    HealthRegeneration
    as literal components and then a system that uses both components to create the regeneration system, but how can this be feasible if one has dozens if not hundreds of properties and countless behaviour systems like buffs interacting with them.
     
  7. Brathnann

    Brathnann

    Joined:
    Aug 12, 2014
    Posts:
    7,188
    I don't know what you mean by you need to hash the key for a dictionary.

    You can grab the component just fine. Normally you would use a GetComponent call, but in your setting I understand you want an easier way to reference it. So...this might be a bit messy since I'm just going to type it in the forum.

    Code (CSharp):
    1. public Dictionary<string, Stat> charStats = new Dictionary<string, Stat>();
    2. public GameObject playerChar;
    3.  
    4. public void SetupCharStat()
    5. {
    6.    //Load up some save file. Json works well here for keyValuePairs, but for the example purpose, I'm just hardcoding this.
    7.    charStats.Add("Health", playerChar.AddComponent<Stat>());
    8.    charStats.Add("Shield", playerChar.AddComponent<Stat>());
    9. }
    10.  
    11. public void AddToStat(string statValue, int value)
    12. {
    13.    charStats[statValue].increaseVal(value);
    14. }
    15.  
    So, in this basic example, I could easily create a save file that would have all the keys I need. Then you could loop through those keys and having a simple Stat class that gets added to the player for example. Note, you could do interfaces also if you needed your stats to be something different, but for this example, I'm just basing off every Stat is just an int and methods can modify that int. (note I didn't modify starting values in this example...)

    If you try to access a key that doesn't exist, you could easily setup things to handle that as well. For example, if the playerChar doesn't have a shield at all.

    DOTS is not really going to solve what you are trying to do either.

    There are ways to set it up where if one script is added to a GameObject that other scripts must also be added to that gameobject, but again, you still have to get a reference to that instance. Which really is all the Dictionary does for you, it maintains a collection of references to the stats you want to modify with easily understandable keys.

    If this wasn't much help, sorry, that might be the limit of what I can offer you. Hopefully someone else might be able to step in and offer more.
     
  8. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Well, we ourselves don't need to hash the key, that's what the dictionary is doing internally to try to find the object stored under that key, that's also the reason why an error is thrown when it doesn't one.

    Therefore, hashing a string is expensive, hashing a number is already better, but the best would be not having to use hashing at all and I know that at least in languages like C/C++, this is done with tricks like macros and memory offset for instance, so I was hoping that there is a straighforward design for it in C# as well.

    Thank you for your example, would you say that it is better to do it that way or by creating types with the property names? As in, one has a
    Property<float>
    and then creates
    Health
    ,
    Shield
    and so on as (empty) child classes from it. That's what they seem to be doing in DOTS with component data.

    I am just so confused by this because it adds such a huge load of class types to the project, although it does allow one to use
    GetComponent(string)
    instead of having to manage the keys oneself with custom strings, since then one can use
    GetComponent
    once again to get the required one.