Search Unity

Question How do I write/serialize instance-independent code/data?

Discussion in 'Scripting' started by Ardenian, Jul 28, 2020.

  1. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Situation: I have a character component and many small property components. The character component knows all property components. These components could look like this:
    Code (CSharp):
    1. public class Character : MonoBehaviour
    2. {
    3.     public Endurance endurance;
    4.  
    5.     public Stamina stamina;
    6.  
    7.     public Vitality vitality;
    8.  
    9.     //...
    10. }
    Code (CSharp):
    1. public class Endurance : CharacterProperty
    2. {
    3.     public float value;
    4. }
    Problem: In my game, I would like to introduce a class
    Validator
    that can check if a character property satisfies a condition. Such an example could be "Is the value of endurance higher than 500?". The validator would return true or false, depending on whether the condition is fulfilled or not.

    However, in Unity, everything seems to be instance-dependent. If I add a component
    Buff
    , for instance, which grants a modifier to a character property as long as a condition is fulfilled, it could look like this:
    Code (CSharp):
    1.  
    2. public class Buff : MonoBehaviour
    3. {
    4.     public CharacterProperty target;
    5.     public Validator validator;
    6.     public Modifier modifier;
    7.     private boolean inEffect;
    8.  
    9.     private void Update(){
    10.         if(validator.Validate(target)){
    11.             if(!inEffect) target.Add(modifier);
    12.         else {
    13.             if(inEffect) target.Remove(modifier);
    14.         }
    15.     }
    16. }
    17.  
    The problem here is I always need an instance to work with the buff. However, this is not something that I know in the editor, I don't know which target character property instance is supposed to get the buff. Furthermore, thus I cannot tell the validator of the buff that it should check the endurance value of the target and not vitality, for instance. Regardless how I organize my objects, be it component or asset (ScriptableObject), I don't see any way to explicitely tell my object which character property it should target.

    How do I tell the validator of a buff that it should target the endurance property value of the character? UnityEvent appears to enable some kind of serialization that almost does what I aim for, however, it depends on specified instances. It is not quite what I am looking for, because I am not dealing with events, but rather looking for some kind of dispatching/selection option.

    How would I do this without
    • introducing an enumeration that has a value for every character property in existence and a dispatcher that switches based on the given enumeration value and retrieves the related character property.
    • introducing a dictionary and hashing character properties or other identifying values to retrieve the related character property (think of string-property dictionary and storing the string key with objects that should target the related character property).
    From my own research, the only way I found is something that I call selectors, either as custom classes using the SerializeReferenceAttribute or ScriptableObjects with only one instance per definition:
    Code (CSharp):
    1.  
    2. [Serializable]
    3. public class EnduranceSelector : PropertySelector<Endurance> { }
    4.  
    5. public class PropertySelector<TProperty> where TProperty : CharacterProperty
    6. {
    7.     public TProperty Get(Character target) {
    8.         return target.gameObject.GetComponent<TProperty>();
    9.     }
    10. }
    11.  
    This doesn't taste so good, because it doesn't scale well with character properties. For every character property, I would have to add another class deriving from PropertySelector for it to be able to referenced.
     
  2. AnthonySharp

    AnthonySharp

    Joined:
    Apr 11, 2017
    Posts:
    88
    I think the complexity of the problem you face is probably the result of the fact that you are using classes too readily. I admire your OCD, but I think having separate classes for things like health, endurance etc is overkill (unless you have really specific design requirements in mind).

    I can see why you might want to do something like that for buffs/curses/spells, because those come and go (unlike stamina etc, which is usually always there). But even in that case I think representing buffs etc in code as opposed to classes is probably the way to go. For instance, you could do:

    Code (CSharp):
    1.     List<Buffs> OurBuffs = new List<Buffs>();
    2.  
    3.     void AddBuff(Buffs b)
    4.     {
    5.         OurBuffs.Add(b);
    6.  
    7.         switch(b.type)
    8.         {
    9.             case "increase_max_health":
    10.                 MaxHealth += b.Var1;
    11.                 break;
    12.             case "poison":
    13.                 break;
    14.         }
    15.     }
    16.  
    17.     void RemoveBuff(Buffs b, int index)
    18.     {
    19.         switch (b.type)
    20.         {
    21.             case "increase_max_health":
    22.                 MaxHealth -= b.Var1;
    23.                 break;
    24.             case "poison":
    25.                 break;
    26.         }
    27.  
    28.         OurBuffs.RemoveAt(index);
    29.     }
    30.  
    31.     void CheckBuffs()
    32.     {
    33.         for (int i = 0; i < OurBuffs.Count; i++)
    34.         {
    35.             OurBuffs[i].timeRemaining -= Time.deltaTime;
    36.             if (OurBuffs[i].timeRemaining <= 0) RemoveBuff(OurBuffs[i], i);
    37.         }
    38.     }
    39.  
    40.     void PerformBuffLogic()
    41.     {
    42.         for (int i = 0; i < OurBuffs.Count; i++)
    43.         {
    44.             switch (OurBuffs[i].type)
    45.             {
    46.                 case "increase_max_health":
    47.                     break;
    48.                 case "poison":
    49.                     Health -= OurBuffs[i].Var1 * Time.deltaTime;
    50.                     break;
    51.             }
    52.         }
    53.     }
    54.  
    55.     void Update()
    56.     {
    57.         CheckBuffs();
    58.         PerformBuffLogic();
    59.     }
    I would say that's more managable and straightfoward than creating a new class for every buff etc, and then getting their references, and then doing all the logic. But I'm kinda biased, since I was brought up on the C++ procedural way of doing things as opposed to the class-based way.
     
    Joe-Censored likes this.
  3. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Agreed to what's been said. You will most likely not need separate classes for stats that are plain values without special logic.

    I would not "encode" their actual purpose into the type itself, but rather define a generic character property, like so:

    Code (CSharp):
    1. public abstract class Property<TValue>
    2. {
    3.     [SerializeField]
    4.     private TValue _value;
    5.  
    6.     public TValue Value
    7.     {
    8.         get { return _value; }
    9.         set { _value = value; }
    10.     }
    11. }
    12.  
    13. [Serializable]
    14. public sealed class IntProperty : Property<int> { }
    15.  
    16. [Serializable]
    17. public sealed class FloatProperty : Property<float> { }
    Using these types, endurance is not a type itself, but its type can be any of those general purpose property types:

    Code (CSharp):
    1. public class Character : ...
    2. {
    3.     [SerializeField]
    4.     private IntProperty _endurance;
    5. }
    The generic base class could also inherit from SO, or if you want to support both, plain and SO types, add an interface like so:
    Code (CSharp):
    1. public interface IProperty<TValue>
    2. {
    3.     TValue Value { get; set; }
    4. }
    5.  
    6. public abstract class ScriptableProperty<TValue> : ScriptableObject, IProperty<TValue>
    7. {
    8.     [SerializeField]
    9.     private TValue _value;
    10.  
    11.     public TValue Value
    12.     {
    13.         get { return _value; }
    14.         set { _value = value; }
    15.     }
    16. }
    17.  
    18. public sealed class ScriptableIntProperty : ScriptableProperty<int> { }
    19.  
    20. // etc...

    As for the buffs, I'm not entirely sure how you intend to implement those. Since you said you're stuck because you always need instances of these properties (and possibly their owner) at design time, my understanding is that you want to design those regardless of who owns the properties, more a like template solution but you'll want to resolve (at runtime) the actual instances to operate on.

    Regardless of how you solve the this, there will be another issue with this approach. It attempts to pull the actual logic into the buffs. For instance, if you had a character with endurance, your buff would - somehow - get that endurance property and modify it. Correct?

    And here, the struggle begins. Because even though this appears to be very flexible in the beginning, it's your buffs that have to do all the legwork and need to take any possible change and factor into account.

    Example:
    Character has endurance - 100 for example.
    Character gets a buff, double endurance for x seconds.

    That'd be easy. Character should have 200 endurance, correct?

    Extend the system with a new buff. Apply it to the character.
    Character gets a buff - add 50 endurance.

    Does the character have
    1) 250 endurance (buffs with percentage > fixed amounts)
    2) 300 endurance (fixed amounts > buffs with percentage)
    3) 250 or 300 (based on the order of applied buffs)

    #3 would be the only option that doesn't require additional knowledge about the buff system in the buffs themselves.

    Now try to extend this again.
    Character gets a debuff - endurance buffs are temporarily ineffective

    How does this buff alter your character's endurance? It'd need to know which buffs were applied, or whats the general base value for that character's endurance. That's certainly going to be a mess. And we're only dealing with 3 types of modifiers. With #3 from above, if that debuff was applied first, it'd be a) useless if other buffs are applied after it or b) it'd need to update the endurance again and again in order to be effective and wipe out all endurance buffs as long as the debuff is active.

    With that in mind, it's probably more straightforward to have a data-driven state and buff system. Stats and buffs are - at least when you look at their public interface - pure data providers. If you like, you can still distinguish them by "types" or "categories" which helps to run the (de-)buff resolve&apply algorithm.

    Note that with the latter, the actual legwork is done in a single (or a few different) place(s).
    Benefits? Order / precedence is defined in just a few places or even a single of if the same rules apply to all entities in your game, you won't even need to expose your targets properties such as endurance, strength, wisdom etc.. You can still feed in information upon applying the (de-)buffs so that external "factors" can still be taken into account. Buffs are easy to extend and maintain, neither dependencies to others buffs, the buff system and nor any details about its algorithm...
     
    Last edited: Jul 29, 2020
    SlimeProphet likes this.
  4. AnthonySharp

    AnthonySharp

    Joined:
    Apr 11, 2017
    Posts:
    88
    Agreed, when buffs begin to potentially overlap, they can cause a bit of conflict. If the logic is scattered around in different classes, it begins to become hard to manage those conflicts.
     
    Suddoha likes this.
  5. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Well, I have objects that all work exactly the same, so I agree with you that this looks like separate classes are not necessary, however, my problem is about identifying what is what in my game. Which character property is endurance, which is vitality and so on.

    All buffs follow the same logic how they alter a character property, my problem is not being able to define in my objects which character property should be altered. Somehow, I need to define for my buff that it should target the endurance character property for a given character, for instance. I do not know which character it is going to be at runtime, which means I cannot use the component reference that I have in the editor.

    Let's define the data for a buff like this:
    Code (CSharp):
    1. public class BuffData : ScriptableObject
    2. {
    3.    [SerializeField]
    4.    private float duration;
    5.    [SerializeField]
    6.    private ValueOption valueOption;
    7.    [SerializeField]
    8.    private LayerOption layerOption;
    9.  
    10.    [SerializeReference]
    11.    private Modifier modifier;
    12.  
    13.    public float Duration => duration;
    14.    public ValueOption ValueOption => valueOption;
    15.    public LayerOption LayerOption => layerOption;
    16.  
    17.    public Modifier Modifier => modifier;
    18. }
    When a buff is created, the data is loaded into a wrapper which keeps track of runtime stuff:
    Code (CSharp):
    1. public class Buff : MonoBehaviour
    2. {
    3.    private BuffData data;
    4.    private CharacterProperty target;
    5.  
    6.    private float remainingTime;
    7.  
    8.    public static Buff Create(BuffData data, GameObject target)
    9.    {
    10.        GameObject obj = target.gameObject;
    11.        Buff buff = obj.AddComponent<Buff>();
    12.  
    13.        buff.data = data;
    14.        buff.target = /*???*/;
    15.        buff.target.Add(data.Modifier, data.ValueOption, data.LayerOption);
    16.        buff.remainingTime = data.Duration;
    17.  
    18.        return buff;
    19.    }
    20. }
    Do you notice something? How do I get the required character property? How do I define it in the buff data?

    As I said in my opening post, I do not wish to introduce an enumeration to dispatch based on its value and get the required property, nor do I want to use a switch case with string-based evaluation to find the right property.

    Is there no way to serialize paths like "target.GetComponent<Character>().endurance" into my buff data? Or serialize that if I have a character object, that I want to get the endurance property of it? This is where my
    PropertySelector<TProperty>
    from my opening post comes from, because I see no other way to do this.

    Code (CSharp):
    1. public class BuffData : ScriptableObject
    2. {
    3.    [SerializeField]
    4.    private float duration;
    5.    [SerializeField]
    6.    private ValueOption valueOption;
    7.    [SerializeField]
    8.    private LayerOption layerOption;
    9.  
    10.    [SerializeReference]
    11.    private Modifier modifier;
    12.    [SerializeReference]
    13.    private IPropertySelector selector;
    14.  
    15.    public float Duration => duration;
    16.    public ValueOption ValueOption => valueOption;
    17.    public LayerOption LayerOption => layerOption;
    18.  
    19.    public Modifier Modifier => modifier;
    20.    public IPropertySelector Selector => selector;
    21. }
    Code (CSharp):
    1. public class Buff : MonoBehaviour
    2. {
    3.    private BuffData data;
    4.    private CharacterProperty target;
    5.  
    6.    private float remainingTime;
    7.  
    8.    public static Buff Create(BuffData data, GameObject target)
    9.    {
    10.        GameObject obj = target.gameObject;
    11.        Buff buff = obj.AddComponent<Buff>();
    12.  
    13.        buff.data = data;
    14.        buff.target = data.Selector.Select(target.GetComponent<Character>());
    15.        buff.target.Add(data.Modifier, data.ValueOption, data.LayerOption);
    16.        buff.remainingTime = data.Duration;
    17.  
    18.        return buff;
    19.    }
    20. }
     
    Last edited: Jul 29, 2020
  6. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Yes, I got that part. However, it's not necessarily an issue that you solve by declaring a pile of unnecessary types that carry the semantic information that you need in order to distinguish one value from another.
    Whether you use a special type or not, you'll need to resolve that property in some way. With types, you could quickly build a GetComponent-like service locator pattern. But in your particular case that'd be a code-smell.

    That was also pretty clear. Again, types won't solve the problem much differently than any other way to resolve the properties. The approach you described needs some sort of look-up, let it be by type, id, name whatever.
    The problem you face wouldn't really exist if you dropped (some parts of) the concept that you're trying to implement with this system.


    So far, so good.


    Yes, that's an inherent flaw of the design decisions you made. You pass a GameObject, and you want a CharacterProperty. Not only that, you want a specific one. And the one who's got to solve that problem is a static helper that is supposed to serve all types of buffs. That method is missing information, how's it ever going to know which property to assign in the first place?

    Suppose you used some sort of type per character property (your original idea), and you had a service locator built into the "target" (which should definitely be a different type in the first place), then you could solve all of that by turning "Create" into a generic method, which then uses the supplied type information to query the property from the target.

    Note how that's still a lookup, so you could as well get properties by ID (string, integer, custom struct) and it will not be any less efficient.

    Still, with that system you're most-likely going to be stuck, no matter which path you take . Why? Just because. I mentioned which problems you might be running into (sooner or later) and which might not be very obvious by now. The fundamental issue will be that every single buff is only able to do what it was designed for - which sounds about right, but which is not going to scale very well and doesn't support more complex scenarios without making it aware of everything that might be going on.

    Again, a lookup mechanism (type, string id, int id, ...) solves that problem, even without a special "property selector".
    However, you seem to dislike this as well (first post):
     
    Last edited: Jul 29, 2020
  7. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Okay, you convinced me, I am going to use some kind of unique identifier and build in a service to get a property.

    If I organize my properties as ScriptableObjects, will this do?

    Code (CSharp):
    1. public class PropertyData : ScriptableObject
    2. {
    3.    [SerializeField]
    4.    private int identifier;
    5.  
    6.    public int Identifier => identifier;
    7.  
    8.    public override bool Equals(PropertyData other)
    9.    {
    10.        return this.identifier == other.identifier;
    11.    }
    12.  
    13.    public override int GetHashCode()
    14.    {
    15.        return identifier;
    16.    }
    17. }
    I chose ScriptableObject because it allows you to add more properties without potentially breaking serialization, as it would be a danger when using C# enumerations. The only issue here is that when using a
    Dictionary<TKey,TValue>
    , using
    PropertyData
    as
    TKey
    hashes the asset, whereas a simple unique integer number is enough, hence the overrides to Equals and GetHashCode? Somewhere in the background, I would add a tool that auto-increments the identifier value of new assets. What do you think about this?

    Example with BuffData:
    Code (CSharp):
    1.     public class BuffData : ScriptableObject
    2.     {
    3.        [SerializeField]
    4.        private float duration;
    5.        [SerializeField]
    6.        private ValueOption valueOption;
    7.        [SerializeField]
    8.        private LayerOption layerOption;
    9.  
    10.        [SerializeField]
    11.        private PropertyData targetProperty;
    12.     }
    13.  
    When I apply a buff now, I can read out what property is the target property.
     
  8. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    I've re-read your requirements and it seems like you actually plan to have more than a few entities with those properties. Since you want to modify the data, you probably don't wanna create the SOs manually, because that's gonna be tedious. And if you were to create them at runtime, you won't need SO as a base class anyway, because their strengths is the ability to be serialized in order to have data and functional assets that you can drag&drop in the editor.

    Also, unless I'm horribly mistaken, if you use SOs as a key in a dictionary for example, it's not gonna hash the entire asset, it's not gonna look at the contents in order to calculate the hashcode and determine equality - how would it? It'd need to reflect the entire type and run some heavy comparisons. Instead, it'll use the object reference (like a normal C# class) which is really efficient.

    Anyways, it'd probably be better to use a plain class if you want to use these.

    I feel like you're trying to generalize this a little too much. Can you elaborate what's the purpose of value and layer options?
     
    eisenpony, Ardenian and AnthonySharp like this.
  9. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    I would like to use these SO instances the same way as one would use a C# enumeration to represent a bunch of options. My reason behind not using a C# enumeration is that it is easy to break the serialization when changing or expanding it, making it not a very maintainable solution.

    In other words, not every entity gets its own property definitions which would require me to manually create these SO instances. Is that what you meant? I create a bunch of them and they are all shared between all entities that do have these properties. Note that this refers to the type of property (endurance, vitality, ...), not the actual value that every entitiy has for that property, which is unique for each entity, being wrapped around the property type.

    With a C# enumeration, it could look like this:
    Code (CSharp):
    1. public enum PropertyType { Vitality, Endurance, /*...*/ }
    2.  
    3. [Serializable]
    4. public class Property
    5. {
    6.     [SerializeField]
    7.     private PropertyType type;
    8.     [SerializeField]
    9.     private float value; // ignoring different primitive types for properties for this example.
    10. }
    Ah, that's great! I wonder if this always works properly, but I would assume so, since ScriptableObject is an unique instance (or somehting like that). My idea behind changing how it is hashed and compared was that if I wanted to support modding, which is likely in my project, you could, as a modder, add your own property definitions from files and the game converts it to SO instances, though maybe I have to handle this anyways, whether I have to get the SO instance reference that I create for the modded content or compare by some other logic like numbers or names, which appears to be easier on first glance.

    Would
    PropertyData
    in your case have a string or a number, for instance, to get what property it represents?

    I would like to give some context here. There is a game called Starcraft 2 (RTS) which features an official modding tool called Galaxy Editor. This editor allows you to create your own custom maps in Starcraft 2 which you can then play with other players online. In this modding tool, there is something called a Data Editor which allows you to create data-only objects and link them with each other, for instance Unit (represents data on a controllable unit), Behaviour (represents data for a behaviour on a unit), Validator (represents data for an execution condition, for instance for a Behaviour) and so on. The huge advantage of this system is that with a few clicks, you can create entire ability trees for units, behaviours, how they work with each other and so on.

    As a result, I try to replicate this system in Unity, to greatly enhance my work flow. ScriptableObjects being my data that I use, with wrappers for instances that use the data. However, I greatly struggle to link my data and my runtime objects together, prompting threads like this one. I don't understand how I can work with my data properly, in this particular case how I can tell buff data (behaviour data) that it is supposed to target a particular property of a given character, for instance.

    I don't know how difficult it is to get around the Data Editor, for instance Data Types provides an overview about this Data Editor in Starcraft 2. In our particular case, Behaviors and Buff might be interesting, to give you an idea what I try to achieve.

    It is a little bit misplaced there, it belongs into the
    Modifier
    object of a buff (unless the buff itself is the modifier). The idea here is to sort the modifier into the right "slot", adressing your previous example:

    If I apply a modifier to a character/property, the ValueOption decides whether it is a flat value or a bonus value and the LayerOption decides on what layer (modifies base value, bonus value or total value) this value is applied. It is more a rough idea here, not an actual implementation. You can find more about it here: How could I optimize this formula and structure?

    I greatly appreciate your answers so far, thank you, if you have some time, could you look on How can I perform a type-based lookup? as well, please? I tried to implement your suggestion of having properties with different primitive types (float, int, bool, ...), which is necessary for my structure, but I struggle with it and would appreciate your input.
     
    Last edited: Aug 1, 2020
  10. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    This sounds fishy to me. Don't you need to provide overrides for Equals and GetHashCode for objects you intend to use as keys, especially if you want to support serializing and deserializing the keys or the dictionary? You don't want to be relying on the object reference between game sessions.

    @Ardenian, if you intend to allow the end user to create custom data types without using the Unity editor to recompile, you have to accept a level of dynamic typing. For instance, creating specific classes for property types won't be possible. There's no sense in having an HP property class if the user is intended to create their own concept of a units life force. Instead, you'll need something like the Property class in your last post except, unless you can anticipate every possible property name a user might want, you won't even be able to use an enumeration. You're stuck using a string.

    I've never used the StarCraft editor, but if it's anything like the Warcraft 3 editor, mods are done using a built-in script language. This is quite a different beast than just modding game files. Maybe Google for how to incorporate something like LUA script into your Unity game.
     
    Last edited: Aug 2, 2020
  11. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    So would my idea with using a ScriptableObject instead of a C# enumeration for enumeration values work here? Since I can write some kind of manager that auto-increments the related identifiers of each enumeration value.

    Let's say that a modder of my game introduces their own character property called "Rage", which does not exist in my game, I would then create a SO instance for it at runtime and assign an unique integer/string value to it that no other property uses as its identifier yet. Wheresoever the custom property called "Rage" is referenced in the modded content, I would then instead replace it with the unique property identifier that I generated.

    It is, I agree, and not what I have in mind to offer in my game. No actual scripting shall be supported for modders, but using official modding tools to create their own content based on existing blueprints/templates. Let's say they want their player character to have a property called "Rage", then they can add it to their character using the modding tools. Furthermore, they can use other tools to add behaviours and effects that add additional functionality to the property and its character, but always within the range of what I give them as tools.

    In the Warcraft 3 editor, there also exists a data editor, if I am not mistaken. You can create your own units, doodads and so on, based on the object definitions that Blizzard offers you to work with. The Galaxy Editor in Starcraft 2 goes a lot further and allows you to modify plenty of different object types.

    My goal is not to enable this as modding in my game, but to implement a similar system structure that allows me to quickly create complicated objects.
     
  12. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Sorry for the late reply.

    Yes, that's what I originally thought and that could be an annoying task.
    But reading your post, it got more obvious what you intend to do, which you explained here:

    So yes, that's different from what I had guessed when I wrote my last replies.


    First of all, that's very ambitious.
    I was about to write "well, why do you need the property type to exist as part of the property", because you - as a developer - generally know what exists. So the "property type" field would carry information that one would usually declare as a part of an interface. For instance, in order to get the character's "strength", you'd have a "Strength" getter (c# property / method).
    But this part "you could, as a modder, add your own property definitions" has already answered it and explains why you're not doing it.

    Depends on whether you want them to use the Unity Editor or not. If they're not supposed to use the Unity Editor, SO's are not gonna be any help I guess, because you'd have your own "asset" format that they feed in - in that case, you're not getting any benefits from SOs compared to plain classes.

    Oh I've read about such extensive tools in these forums, there was someone who had a huge rant about why Unity hasn't such tools built-in. :) That's also much closer to what I recommended in my first reply when I said you might be better off using a data-driven system.
    Except that these tools you mentioned are way more powerful than what I had in mind.

    Dealing with pure data that will be applied to existing "properties" will probably be the easiest part. I cannot give any in-depth advice on behaviour extensions through modding tools, but I think @eisenpony has nailed it... You need - at least - a processor for data and behaviour objects. One that's generic enough to interpret what the modder has come up with. You may also be interested in behaviour trees.
    If you want even more than that, you probably need to implement or use a custom scripting language.
     
    Last edited: Aug 3, 2020
    eisenpony likes this.
  13. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    You don't need to, unless you need to.
    I mean sure, if you create two SOs and link them, and they happen to "be equal" by your own design, override it. It depends on what they're supposed to represent.
    I think most of the time you wouldn't want to do that, though, as their primary use is being a unique asset. If I create multiple assets (same values or not) there is most-likely a reason for it.

    Custom serialization is a different topic... You'd solve that similarly to how Unity has solved this already.

    Personally I rather keep the default equality comparer for SO's, as in "this one is a different asset than this one" and supply a custom equality comparison whenever it's required.
     
    eisenpony likes this.
  14. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    This point is bugging me a bit. I do want to use them in the Unity Editor, but obviously modders adding their own content will do so with definition files whose format I define. However, this raises the question why I wouldn't use that format myself for my content. It is basically double work if I use SOs in the Unity Editor, but then still add some kind of processor that reads out modded definition files and translates them into my own classes.

    Right now, I do use SO instances because they are neat to work with, but obviously the whole idea has yet to be fully cooked out. As for my current progress:

    Code (CSharp):
    1. [Serializable]
    2. public abstract class Property
    3. {
    4.     public abstract int Identifier { get; }
    5. }
    6.  
    7. [Serializable]
    8. public abstract class Property<T> : Property
    9. {
    10.     [SerializeField]
    11.     private int identifier;
    12.  
    13.     [SerializeField]
    14.     private T value;
    15.  
    16.     public Property(int identifier, T value)
    17.     {
    18.         this.identifier = identifier;
    19.         this.value = value;
    20.     }
    21.  
    22.     public T Value => value;
    23.  
    24.     public override int Identifier => identifier;
    25.  
    26.     public override bool Equals(object obj)
    27.     {
    28.         var property = obj as Property<T>;
    29.         return property != null &&
    30.                identifier == property.identifier;
    31.     }
    32.  
    33.     public override int GetHashCode()
    34.     {
    35.         return 1442482158 + identifier.GetHashCode();
    36.     }
    37. }
    38.  
    39. [Serializable]
    40. public class FloatProperty : Property<float>
    41. {
    42.     public FloatProperty(int identifier, float value) : base(identifier, value)
    43.     {
    44.     }
    45. }
    46.  
    47. [Serializable]
    48. public class BooleanProperty : Property<bool>
    49. {
    50.     public BooleanProperty(int identifier, bool value) : base(identifier, value)
    51.     {
    52.     }
    53. }
    54.  
    55. [Serializable]
    56. public class IntegerProperty : Property<int>
    57. {
    58.     public IntegerProperty(int identifier, int value) : base(identifier, value)
    59.     {
    60.     }
    61. }

    Code (CSharp):
    1. public abstract class PropertyData : ScriptableObject
    2. {
    3.     [SerializeField]
    4.     private int identifier = -1;
    5.  
    6.     public int Identifier { get => identifier; }
    7.  
    8.     public abstract Property Create();
    9. }
    10.  
    11. public abstract class PropertyData<T> : PropertyData
    12. {
    13.     [SerializeField]
    14.     private T defaultValue = default(T);
    15.  
    16.     public T DefaultValue { get => defaultValue; }
    17. }
    Code (CSharp):
    1. [CreateAssetMenu(fileName = "New Float Property Data", menuName = "Create/Data/Property Data/Float Property Data")]
    2. public class FloatPropertyData : PropertyData<float>
    3. {
    4.     public override Property Create()
    5.     {
    6.         return new FloatProperty(this.Identifier, DefaultValue);
    7.     }
    8. }
    Code (CSharp):
    1. [CreateAssetMenu(fileName = "New Boolean Property Data", menuName = "Create/Data/Property Data/Boolean Property Data")]
    2. public class BooleanPropertyData : PropertyData<bool>
    3. {
    4.     public override Property Create()
    5.     {
    6.         return new BooleanProperty(this.Identifier, DefaultValue);
    7.     }
    8. }
    Code (CSharp):
    1. [CreateAssetMenu(fileName ="New Integer Property Data", menuName = "Create/Data/Property Data/Integer Property Data")]
    2. public class IntegerPropertyData : PropertyData<int>
    3. {
    4.     public override Property Create()
    5.     {
    6.         return new IntegerProperty(this.Identifier, DefaultValue);
    7.     }
    8. }

    Code (CSharp):
    1. public class PropertyProvider : MonoBehaviour, ISerializationCallbackReceiver
    2. {
    3.     [SerializeField]
    4.     private PropertyData[] data = null;
    5.  
    6.     [SerializeReference]
    7.     private List<Property> properties = null;
    8.  
    9.     [NonSerialized]
    10.     private Dictionary<int, Property> indexedProperties = null;
    11.  
    12.     public bool TryGetProperty(int identifier, out Property property)
    13.     {
    14.         return indexedProperties.TryGetValue(identifier, out property);
    15.     }
    16.  
    17.     void ISerializationCallbackReceiver.OnAfterDeserialize()
    18.     {
    19.         indexedProperties = new Dictionary<int, Property>();
    20.         for (int propertyIndex = 0; propertyIndex < properties.Count; propertyIndex++)
    21.         {
    22.             var property = properties[propertyIndex];
    23.             if (property != null && !indexedProperties.ContainsKey(property.Identifier))
    24.             {
    25.                 indexedProperties.Add(property.Identifier, property);
    26.             }
    27.         }
    28.     }
    29.  
    30.     void ISerializationCallbackReceiver.OnBeforeSerialize()
    31.     {
    32.         if (data != null && properties != null)
    33.         {
    34.             for (int dataIndex = 0; dataIndex < data.Length; ++dataIndex)
    35.             {
    36.                 if (data[dataIndex] != null && !properties.Any(item => item.Identifier.Equals(data[dataIndex].Identifier)))
    37.                 {
    38.                     properties.Add(data[dataIndex].Create());
    39.                 }
    40.             }
    41.  
    42.         }
    43.     }
    44. }



    Using
    [SerializeReference]
    allows me to add properties of any type to the same provider, however, at the same time I lose type information which is one of the issues that bugs me about this solution. As you can tell, I can only return a general
    Property
    from the provider and then I still have to type cast it, although I already know that a particular property has a value of a particular type.

    The relation between
    PropertyData
    and
    Property
    is also not great yet. The idea behind
    PropertyData
    is to tell my character which properties they have (therefore, those objects provide meta data) and then use them as factories to create the correct property object for it.

    However, then the relation is kinda lost and having identifiers being mere numbers doesn't really tell me as a developer which property is which, so I think about stuffing the property data into the property object. It does make the factory methods kinda dull, though, giving the whole data object instead of only the identifier into the property object that is created:
    Code (CSharp):
    1.     public override Property Create()
    2.     {
    3.         return new FloatProperty(this);
    4.     }
     

    Attached Files:

  15. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Is the solution that I currently build not a data-driven system? I read about pluggable AI and such, but isn't my system kinda the same approach? Writing specialized data containers that hold all logic and then have generic buffers wrapped around it at runtime that take care of some default stuff?

    Code (CSharp):
    1. public class Buff : MonoBehaviour
    2. {
    3.     private BuffData data;
    4.     private Character character;
    5.  
    6.     private void Update()
    7.     {
    8.         data.Do(character);
    9.     }
    10.  
    11.     public static Buff Create(Character character, BuffData data)
    12.     {
    13.         //...
    14.     }
    15. }
    16.  
    17. public abstract class BuffData : ScriptableObject
    18. {
    19.     public abstract void Do(Character character);
    20. }
    With an implementation looking like:
    Code (CSharp):
    1. public sealed class RegenerationBuff : BuffData
    2. {
    3.     [SerializeField]
    4.     private PropertyData targetProperty;
    5.  
    6.     [SerializeField]
    7.     private float amount;
    8.  
    9.     public void Do(Character character)
    10.     {
    11.         var provider = character.GetComponent<PropertyProvider>();
    12.         var property = provider.GetValue(targetProperty.identifier) as (FloatProperty);
    13.  
    14.         if(property != null) property.Value += (amount * Time.deltaTime);
    15.     }
    16. }
     
    Last edited: Aug 3, 2020
  16. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    I meant this in regards to objects placed in dictionaries. Doesn't matter if they are scriptable objects or whatever. If they are going in a dictionary, especially in a system that is recreated through a serialization process of any type, then they need to have value based equality rather than reference based.

    Maybe I missed something, like the dictionaries are not being serialized, or the keys are something more static, like the type. Anyways, nevermind, it seems like minutia to the larger topic.


    Something else that is confusing me.. aren't SO just classes? @Ardenian, are you proposing to create new SOs based on user created mods? I think if you want to use SOs, they have to be compiled -- not something you can do from data files at runtime. Furthermore, I think your RegenerationBuff is supposed to be an example of something a user might create, but how would you implement the Do method you showed without Unity and without a scripting language?
     
    Last edited: Aug 3, 2020
  17. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    I make a difference between SO definition and SO instances. A SO definition is the class definition, yes, they are just classes, the SO instances are, well, instances of that definition as assets. If a modder creates a new property from a definition, he doesn't create a new SO definition, but a SO instance that shares its type, the SO definition, with all other properties. There are no explicitely typed properties anymore, which would require compilation at runtime.

    A definition by a modder could look like this:
    Code (XML):
    1.  
    2. <property>
    3.     <identifier>Rage</identifier>
    4.     <defaultValue>0</defaultValue>
    5. </property>
    6.  
    Which I translate internally into a new SO instance with ScriptableObject.CreateInstance at runtime. Of course, in my case I would create a
    PropertyData
    instance, not the actual property, which is another story.

    The
    RegenerationBuff
    is both something that I can use to create buffs with that particular behaviour for my game as well as what users can use to create their own buffs (although using an entirely different method). In this particular case, all
    RegenerationBuff
    objects share the same logic, modders and I myself as a developer are bound to what it can do, there is no scripting involved to change the behaviour on that level.

    Only changing the values of each individual instance is something that one can do. One such buff might change a lot of vitality, another buff of the same type might just change a little bit of stamina.

    I provide the "blueprint", the object definiton, then modders and I create the data, the instances, based on it, I myself do it in the Unity editor, modders do it with definition files that I read out at runtime and compile them into objects that work with the rest of my game. I think the game RimWorld does something like that.
     
    Last edited: Aug 3, 2020
  18. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    I was interested if you understood the limitations of what you were proposing, and it seems like you do ..

    I think Suddoha has probably answered a lot of your original questions. Just to bring the conversation back into focus, do you want to restate the questions you still have?
     
  19. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    It depends. If you serialize it by value, you end up with something that you can often see in Unity. For instance, write a tool that shares instances of normal classes, i.e. it generates data and sets the data on multiple behaviours (it's even more obvious when you do so with collections).
    Now, let Unity serialize and deserialize it... these will no longer be "shared" but the instances will be distinct objects instead. And if you did it with an array or a list and the contents weren't UnityEngine.Objects, but normal reference types, not only the array/list but also the contents are re-created as distinct instances.

    Hence the popular demand for reference serialization, which is (not entirely, but for references on the same UnityEngine.Object) addressed by SerializeReference in recent versions of Unity.
    For UnityEngine.Objects, this is done all the time in order to re-inject deserialized instances to fields that they had been linked to via the inspector.

    So yes, overriding equality helps to a certain extent, until you need to recover references, i.e. deserialize once per let's say UUID and for every occurrence of that UUID, inject that specific instance.

    Also note that you generally want to pick certain fields for equality checks and hash-code calculation when you need to use those types reliably in dictionaries, hashsets etc. Whenever that's a requirement, and you override those methods, you have to guarantee the data that's included in the hash-calculation and equality check is not going to change. The data should be immutable, at least per application session.

    Anyway, there is not definite rule that says "always override" or "do not override".
     
  20. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Why don't you build that system first if you need it anyway?
    Build it, try to add your property definitions and buffs. That'd be a proof of concept, enables you to test and re-iterate etc...

    If everything goes well, you'll have a small API that you can use in the editor (in order to build Unity tools for your system) as well as in a separate modding tool, which you can either integrate into your game or deploy and give away separately.

    I mean, you're doing a ton a work trying to get it work in Unity using SOs and all the stuff, in the end you'll start to develop the modding tools just to figure out you've focused too much on SOs, whereas these aren't really beneficial for the data that you load in at runtime.

    I'd also try to tackle the whole system from different directions. Try to come up with a concept on paper, write it down, or draw a sketch of the whole system and come up with all requirements.

    What should the data look like that's fed into the system. What are the supported formats? Define that format.
    What's required for that? Property definition! Data/Buff Defintion. How to reference existing Property Definitions to which the data will be applied. How to reference routines/behaviour that's pre-defined in your game in order to create complex buff mechanisms?
    How do you load the data in? Where is it stored, processed? How is it going to be added to specific characters? How can the affected characters be identified per type/category/group/condition/individually or whatever you may need.

    Then try to identify, isolate and build the core system and extent by the most important feature, continue step by step.

    As I mentioned earlier, this is an ambitious project. And there are so still many things we don't know all the details about. You've already provided much information, but with every step we take, you reveal new information and I'm afraid we might push you in the wrong directions if we don't know all the details. I've already went into the wrong direction a few times, because I didn't even know that your original "small problem" is just a piece of a huge system that includes such advanced features.
     
  21. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Thank you for your responses so far, guys!
    Yes, it is intentionally limitating the abilities on what one can do in the game from a development point of view in favour of a stable and easy to understand system. Looking at the systems in Starcraft 2. it is amazing that they got a whole RTS game running with only around a dozen different behaviour types (with buff being one of them). Of course, Starcraft 2 also has custom scripting which is heavily used as well by the developers and modders alike, so the system itself is not flawless and cannot cover everything.

    I thought about this a lot and I still don't quite understand the difference between explicitely typing everything and having generic classes with serialized instances. I come from Java and was told there that you should always type everything explicitely (as in, have everything derive from a base class) so that one knows what one deals with. There wouldn't be a general script
    Character
    , for instance, with prefabs of that character, but instead one would create classes deriving from
    Character
    for each individual character type that one would have, like
    Skeleton
    ,
    Archer
    ,
    Knight
    and so on.

    Explaining my confusion in Unity simply by having a entirely different system, a component system, I am left with one question and that is, when should I use a component and when should I not? In particular in this case, I made my
    Property
    a custom class and the property provider which provides all properties a component. However, can one justify this decision? In particular when it comes to referencing properties elsewhere. It is the nature of objects being serialized by
    [SerializeReference]
    that these objects do not share their reference and are not the same object. In other words, if I create an object in the Unity editor under a
    [SerializeReference]
    and reference it in multiple other places under
    [SerializeReference]
    , these objects are not the same object anymore at runtime. I think this is wat Suddoha means by:

    With the Scripting API mentioning as well:
    Does this justify making each
    Property
    a component deriving from MonoBehaviour instead of a custom class to enable being able to reference it in the editor? How do I decide whether I should make something a component or a custom class, what are the rules? This is the last question and struggle that I have for this thread and I would greatly appreciate some insight.

    Would I re-invent the wheel doing that? Since the Unity editor enables me to create data instances with SO instances, wouldn't such a tool merely give me the ability to load such instances from file definitions without me actually using those because I use the Unity editor?

    It resembles what I am doing, but it is hard not to get stuck in a loop with particular details. Do you have a recommendation on software in that direction? I know a bunch of tools around Java that greatly help with planning and forging systems, like UML tools, but I have yet to see something like this around Unity.

    I see what you mean. You helped me a lot so far and I think it is enough to get me started and refine on my ideas into actual implementation, not mere ideas as I shaped them here previously. With the properties being done as an element or at least having a basis with what is currently on table, I think it will greatly help me to get more done.
     
  22. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    That's merely an engineering decision. One may be better than the other. Sometimes a trivial and the most apparent approach is enough or even better than a more complex one that you won't ever really need.
    You just have to stop at some point, and that's where we stepped in and said you're better off dropping classes like "Endurance" which only exists in order to carry the semantics for a specific float property. Reason being that you usually provide that information with the identifier (a field, property, method name...), as I already mentioned earlier which can be part of a type's public interface.

    Often enough you'll also find yourself getting stuck with inheritance, because one tends to overuse it every now and then. Composition solves a lot of problems and you have to try to get the best out of both worlds.

    The example you've given can be implemented in various ways. One developer may choose to use pure inheritance, another uses components only, and yet another combines both techniques.

    Component based design is very important if you want to build complex systems and want to have the flexibility to construct new entity types by throwing components arbitrarily together, without the need to always create a new class and override + repeat everything.

    Most of the time, people subclass MonoBehaviour, because that's the easy way to get things working. And because you get the nice editor stuff. But you do not always need it. You can also build applications with just a few behaviours that open up the communication with the engine's API.
    If your type relies on the GO, the transform or the component system, or Unity's special methods, there's a good chance you're not doing things horribly wrong when you subclass MonoBehaviour.
    Contrary to that, if you implement data types, utility types, common algorithms, file IO, database access, core networking, .... anything that has no real dependency on Unity-specific things, you should probably avoid MonoBehaviour and instead, implement them as plain classes. For the ease of use, you can always wrap around them in order to leverage the editor stuff for providing values, dependencies, configurations,...

    As for your example with properties and the property provider. Well yes, you probably don't want to attach each property to a GO, hence it's probably a good decision not to inherit MonoBehaviour. The property provider wouldn't need to inherit MonoBehaviour either, but apparently you've found it convenient to hook things up via the inspector. If that's the purpose of it, let it be.
    However, your character component could use such provider internally. You'd then hook up the properties for the character, and the provider would do it's job when instructed by the character (for example in awake or start).
    In the end, it depends on how you implement your characters. That's really up to you.

    Yes and no.
    For your own workflow in the Editor, you'd re-invent the wheel. However, considering that you want to enable modding outside of the Unity Editor, you won't get around it anyway. So just give it a try, unless modding is a feature that you want to add way much later.


    There are lots of tools out there that you can use. Unless you want code-generation from UML, you can use whatever you want. Personally I like to sketch my systems on a whiteboard or a sheet of paper every now and then, because some upcoming problems are easier to recognize when you visualize the dependencies.
    Sometimes I also create UMLs in order to get a rough idea about the dependencies I've introduced with my decisions, but honestly I haven't done that for a long time. I'm probably a bit biased towards MS tools anyways, because that's what I used in college and that's what I have to use in my day job.
     
    eisenpony likes this.
  23. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Hm, I get your points and yet I am still conflicted. One huge advantage of making each property its own MonoBehaviour is the ability to reference it in the editor freely (using instances of characters and their properties that exist in the scene). I feel that not doing this, making each property a component, would mean throwing away this advantage for no good reason.

    I mean, consider my next development step, which is a
    CharacterValue
    . Instead of a
    CharacterProperty
    , which is just a value (think of "maximum health points"), a
    CharacterValue
    has a changing value between a min and a max (think of "current health points"), with the max definitely being a
    CharacterProperty
    . If I made
    CharacterProperty
    a MonoBehaviour, I could reference that component directly in the
    CharacterValue
    objects. If I don't do this and stick to my current solution, I am forced to look up the character property and its value every time that I change the character value.

    Also think about stuff like UI elements, displaying a character's health, both current and max, other vitals and who knows what else. One could easily do this by referencing the components in the editor (since the player character does exist in the editor), whereas it would get more complicated to do it when using a custom class.

    Would you say that this does justify making
    CharacterProperty
    and
    CharacterValue
    a MonoBehaviour component?

    I do think that I am going to do this later, once I have something to work with. It might be a bit early to think about modding since there is no game there yet that could be modded, but it is also not stupid to always take a step back and consider how one could mod something even without going through immediately.

    That sounds like a decent idea, thank you, I will try it out to see if I like it and can work with it.
     
  24. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    I should say that I'm not trying to convince you not to use MonoBehaviours or SOs.
    If you don't want to miss the benefits of the Unity Editor, the component system and the linking via inspector, that's your personal preference and if you feel like that's great, just go for it. Especially when it helps to make some quick progress in your development.
    I was just trying to answer the questions and mention some pros/cons with regards to your concerns of the design.

    So yes, if you can see a great advantage of having these properties as separate components, just try it. Inside the editor that'll work like a charm.

    That sounds great and fine, but remember that you're trying to give every single value an object identity, a wrapper if you will. I'm not saying this is bad, nor that it's useless, inefficient or anything alike. Just ask yourself if you really need that. If the answer is yes, go for it.

    IMO: No, because that'd be too many components for my personal taste. I'd rather have an interface that exposes accessors and such to deal with these values. If you still want to make them behaviours, expose the properties as an interface type. With that, you'll be free to switch between property components, and plain c# types, or anything that satisfies the contract.
    However, I already mentioned earlier, that if I had to come up with a solution spontaneously, I'd probably try a different approach to begin with. Though that'll take time and who knows, perhaps I'd change my mind. But I'm pretty certain that I wouldn't create a MB for every single value for the sake of convenience.
     
    Ardenian likes this.
  25. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Yeah, I think that I try to convince myself, but I cannot bring myself to make properties a component. Considering that there are dozens of properties per character and then again character values equal or less to the amount of properties, it is overloading the inspector quite a bit, cluttering it.
    Nonetheless, the benefits that I see, especially the drag and dropping of components, being able to reference them directly is golden in my eyes. Earlier, there was a discussion in this thread about hashing and referencing, do you know if there would be any performance loss if one uses components excessively, referencing them in many places as opposed to looking up objects every time? In particular the comment about the requirement of Unity preserving references of objects deriving from Unity.Object catched my eye, making me wonder how heavy this hits on performance in comparison to only store information that you need to look up the object from a provider (in this particular case, a
    PropertyData
    asset and a
    Character
    component).

    I plan to break with the single responsiblity principle when it comes to properties. Right now, properties only provide their value, however, on a latter stage in development, they also hold all modifiers to that property (think of a buff "this buff increases your character's maximum health points by 100") and calculate the final value of themselves. I think the benefits in work flow and organizing things when doing this is great enough to step over the single responsiblity principle.

    Would it be a solution to move the property provider into a child GameObject of the original GameObject, so they don't clutter the original GameObject that has the Character component or would you say that this is merely mitigating the problem of having too many components, not solving it?

    That sounds like a great idea, thanks! I am going to write my property provider this way. At least with having custom classes, the provider acts as a facade, not allowing access to the properties elsewhere, which is something that I dislike about my idea of making properties components, because they can be accessed easily and unintenionally and no object is forced to go through the provider as a facade.

    At runtime, a reference to a serialized C# custom class is guaranteed to be stable, right? Let's say I have a UI element that displays a property value. If my property was a component, I could just reference the component, but if it is a custom class, I have to get it through the provider. Doing this once at runtime, the reference to that property is stable, so I don't have to get it through the provider again in the same session? Assuming no external influence impacts the UI element and the referenced property, of course.
     
  26. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    It's a design decision you have to make, if that's what you need and want, do it.
    UT has shown something similar in their very first proper talks/tutorials about SOs. It was basically the same idea, except that they used SOs in order to distribute wrapped values all over the place in game systems, mechanics, UI, etc. so that there's no more code involved. Of course it's not exactly the same as using MBs, as they exist per instance in a scene, but the overall idea is the same - wrap a value in a reference that you can drag/drop anywhere, and save time doing everything in code.

    There's always a cost. Of course it takes time to deserialize it for each instance. And if you have Awake, Start, ... etc, it'll need some extra cycles. But seriously, the actual performance impact can only be determined by profiling. I doubt you'll notice it though. That should be one of your least worries.


    I've commented that earlier. That'd require more and more logic in the properties. And if you had dependencies to other properties, you'll end up with an unmaintainable mess because every property would need to take care of everything else. But that's up to you.

    I don't see how that would solve anything besides reducing the clutter on the main object.

    Facades are a good choice, because there's a general problem I've seen happening in so many of the projects I've worked on. Component based systems are great, no doubt, i love them, but being able to query all components individually and doing so can quickly introduce bugs and inconsistencies in so many places, basically whenever multiple components are part of an action. A thin facade that hides components, treats them as implementation detail and orchestrates the communication and usage of such sub-components is something that's definitely going to make your life much easier.


    Not sure what you mean by "stable". If it's about getting a reference to an instance once in order to access that exact instance later, that's how it works.
     
    Ardenian likes this.
  27. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    Thanks a lot for sticking with me and answering my questions, @Suddoha, I am good to go now!
     
    Suddoha and eisenpony like this.
  28. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    I ended up making my properties into MonoBehaviours. Being able to drag and reference them in the editor is too tempting to ignore it, so I created a structure like this:
    property-player.png
    property-property.png
    property-behaviour.png

    My biggest problem with other solutions was that all the time, I felt as if I was re-building what Unity already offers, especially when I introduced EntityBehaviour, as one can see it in the third screenshot. I still kinda re-build what Unity already offers, but it is a lot more embedded into the default stuff, rather than re-inventing the wheel.

    The only thing that nags at me is having to make each property/value (or both) a separate child GameObject to its entity. As it turns out, Unity doesn't highlight which script is referenced on an object if an object has multiple of the same type and although it is rather easy to write a PropertyDrawer for that particular type that allows you to see which property exactly (Endurance, Stamina, ..., defined by the
    PropertyData
    field of a
    Property
    ) is referenced somewhere, I did make them separate objects.
     
  29. Ardenian

    Ardenian

    Joined:
    Dec 7, 2016
    Posts:
    313
    After progressing a lot in my project, I would like to mention that despite my opposition to this suggestion, I ended up following it. By building that system first, one identifies so, so many issues and open questions and can clear them up along the way. It also helps me a lot that examples for such modded files are great blueprints for identifying the need for the actual objects that I have to create in the editor.

    There are still some open questions from building that system, such as how to link a XML syntax to your actual code syntax if they don't match, but I believe that better fits into a new thread.