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

Situation: I have a character component and many small property components. The character component knows all property components. These components could look like this:

public class Character : MonoBehaviour
{
    public Endurance endurance;

    public Stamina stamina;

    public Vitality vitality;

    //...
}
public class Endurance : CharacterProperty
{
    public float value;
}

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:

public class Buff : MonoBehaviour
{
    public CharacterProperty target;
    public Validator validator;
    public Modifier modifier;
    private boolean inEffect;

    private void Update(){
        if(validator.Validate(target)){
            if(!inEffect) target.Add(modifier);
        else {
            if(inEffect) target.Remove(modifier);
        }
    }
}

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:

[Serializable]
public class EnduranceSelector : PropertySelector<Endurance> { }

public class PropertySelector<TProperty> where TProperty : CharacterProperty
{
    public TProperty Get(Character target) {
        return target.gameObject.GetComponent<TProperty>();
    }
}

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.

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:

    List<Buffs> OurBuffs = new List<Buffs>();

    void AddBuff(Buffs b)
    {
        OurBuffs.Add(b);

        switch(b.type)
        {
            case "increase_max_health":
                MaxHealth += b.Var1;
                break;
            case "poison":
                break;
        }
    }

    void RemoveBuff(Buffs b, int index)
    {
        switch (b.type)
        {
            case "increase_max_health":
                MaxHealth -= b.Var1;
                break;
            case "poison":
                break;
        }

        OurBuffs.RemoveAt(index);
    }

    void CheckBuffs()
    {
        for (int i = 0; i < OurBuffs.Count; i++)
        {
            OurBuffs[i].timeRemaining -= Time.deltaTime;
            if (OurBuffs[i].timeRemaining <= 0) RemoveBuff(OurBuffs[i], i);
        }
    }

    void PerformBuffLogic()
    {
        for (int i = 0; i < OurBuffs.Count; i++)
        {
            switch (OurBuffs[i].type)
            {
                case "increase_max_health":
                    break;
                case "poison":
                    Health -= OurBuffs[i].Var1 * Time.deltaTime;
                    break;
            }
        }
    }

    void Update()
    {
        CheckBuffs();
        PerformBuffLogic();
    }

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.

1 Like

Agreed to what’s been said. You will most likely not need separate classes for stats that are plain values without special logic.

Keeping character property types

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

public abstract class Property<TValue>
{
    [SerializeField]
    private TValue _value;

    public TValue Value
    {
        get { return _value; }
        set { _value = value; }
    }
}

[Serializable]
public sealed class IntProperty : Property<int> { }

[Serializable]
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:

public class Character : ...
{
    [SerializeField]
    private IntProperty _endurance;
}

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:

public interface IProperty<TValue>
{
    TValue Value { get; set; }
}

public abstract class ScriptableProperty<TValue> : ScriptableObject, IProperty<TValue>
{
    [SerializeField]
    private TValue _value;

    public TValue Value
    {
        get { return _value; }
        set { _value = value; }
    }
}

public sealed class ScriptableIntProperty : ScriptableProperty<int> { }

// 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…

1 Like

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.

1 Like

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:

public class BuffData : ScriptableObject
{
   [SerializeField]
   private float duration;
   [SerializeField]
   private ValueOption valueOption;
   [SerializeField]
   private LayerOption layerOption;

   [SerializeReference]
   private Modifier modifier;

   public float Duration => duration;
   public ValueOption ValueOption => valueOption;
   public LayerOption LayerOption => layerOption;

   public Modifier Modifier => modifier;
}

When a buff is created, the data is loaded into a wrapper which keeps track of runtime stuff:

public class Buff : MonoBehaviour
{
   private BuffData data;
   private CharacterProperty target;

   private float remainingTime;

   public static Buff Create(BuffData data, GameObject target)
   {
       GameObject obj = target.gameObject;
       Buff buff = obj.AddComponent<Buff>();

       buff.data = data;
       buff.target = /*???*/;
       buff.target.Add(data.Modifier, data.ValueOption, data.LayerOption);
       buff.remainingTime = data.Duration;

       return buff;
   }
}

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().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.

public class BuffData : ScriptableObject
{
   [SerializeField]
   private float duration;
   [SerializeField]
   private ValueOption valueOption;
   [SerializeField]
   private LayerOption layerOption;

   [SerializeReference]
   private Modifier modifier;
   [SerializeReference]
   private IPropertySelector selector;

   public float Duration => duration;
   public ValueOption ValueOption => valueOption;
   public LayerOption LayerOption => layerOption;

   public Modifier Modifier => modifier;
   public IPropertySelector Selector => selector;
}
public class Buff : MonoBehaviour
{
   private BuffData data;
   private CharacterProperty target;

   private float remainingTime;

   public static Buff Create(BuffData data, GameObject target)
   {
       GameObject obj = target.gameObject;
       Buff buff = obj.AddComponent<Buff>();

       buff.data = data;
       buff.target = data.Selector.Select(target.GetComponent<Character>());
       buff.target.Add(data.Modifier, data.ValueOption, data.LayerOption);
       buff.remainingTime = data.Duration;

       return buff;
   }
}

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):

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?

public class PropertyData : ScriptableObject
{
   [SerializeField]
   private int identifier;

   public int Identifier => identifier;

   public override bool Equals(PropertyData other)
   {
       return this.identifier == other.identifier;
   }

   public override int GetHashCode()
   {
       return identifier;
   }
}

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:

    public class BuffData : ScriptableObject
    {
       [SerializeField]
       private float duration;
       [SerializeField]
       private ValueOption valueOption;
       [SerializeField]
       private LayerOption layerOption;

       [SerializeField]
       private PropertyData targetProperty;
    }

When I apply a buff now, I can read out what property is the target property.

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?

3 Likes

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:

public enum PropertyType { Vitality, Endurance, /*...*/ }

[Serializable]
public class Property
{
    [SerializeField]
    private PropertyType type;
    [SerializeField]
    private float value; // ignoring different primitive types for properties for this example.
}

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.

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.

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.

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.

[quote=“Ardenian, post:9, topic: 802126, username:Ardenian”]
Would PropertyData in your case have a string or a number, for instance, to get what property it represents?
[/quote] 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. :slight_smile: 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.

1 Like

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.

1 Like

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:

I introduced a custom class Property that wraps a primitive value that is associated with a character

[Serializable]
public abstract class Property
{
    public abstract int Identifier { get; }
}

[Serializable]
public abstract class Property<T> : Property
{
    [SerializeField]
    private int identifier;

    [SerializeField]
    private T value;

    public Property(int identifier, T value)
    {
        this.identifier = identifier;
        this.value = value;
    }

    public T Value => value;

    public override int Identifier => identifier;

    public override bool Equals(object obj)
    {
        var property = obj as Property<T>;
        return property != null &&
               identifier == property.identifier;
    }

    public override int GetHashCode()
    {
        return 1442482158 + identifier.GetHashCode();
    }
}

[Serializable]
public class FloatProperty : Property<float>
{
    public FloatProperty(int identifier, float value) : base(identifier, value)
    {
    }
}

[Serializable]
public class BooleanProperty : Property<bool>
{
    public BooleanProperty(int identifier, bool value) : base(identifier, value)
    {
    }
}

[Serializable]
public class IntegerProperty : Property<int>
{
    public IntegerProperty(int identifier, int value) : base(identifier, value)
    {
    }
}

Then I created a SO definition PropertyData which holds the information about a property type, such as Strength, Endurance, …

public abstract class PropertyData : ScriptableObject
{
    [SerializeField]
    private int identifier = -1;

    public int Identifier { get => identifier; }

    public abstract Property Create();
}

public abstract class PropertyData<T> : PropertyData
{
    [SerializeField]
    private T defaultValue = default(T);

    public T DefaultValue { get => defaultValue; }
}
[CreateAssetMenu(fileName = "New Float Property Data", menuName = "Create/Data/Property Data/Float Property Data")]
public class FloatPropertyData : PropertyData<float>
{
    public override Property Create()
    {
        return new FloatProperty(this.Identifier, DefaultValue);
    }
}
[CreateAssetMenu(fileName = "New Boolean Property Data", menuName = "Create/Data/Property Data/Boolean Property Data")]
public class BooleanPropertyData : PropertyData<bool>
{
    public override Property Create()
    {
        return new BooleanProperty(this.Identifier, DefaultValue);
    }
}
[CreateAssetMenu(fileName ="New Integer Property Data", menuName = "Create/Data/Property Data/Integer Property Data")]
public class IntegerPropertyData : PropertyData<int>
{
    public override Property Create()
    {
        return new IntegerProperty(this.Identifier, DefaultValue);
    }
}

Last but not least I bring everything together in a PropertyProvider that each character has

public class PropertyProvider : MonoBehaviour, ISerializationCallbackReceiver
{
    [SerializeField]
    private PropertyData[] data = null;

    [SerializeReference]
    private List<Property> properties = null;

    [NonSerialized]
    private Dictionary<int, Property> indexedProperties = null;

    public bool TryGetProperty(int identifier, out Property property)
    {
        return indexedProperties.TryGetValue(identifier, out property);
    }

    void ISerializationCallbackReceiver.OnAfterDeserialize()
    {
        indexedProperties = new Dictionary<int, Property>();
        for (int propertyIndex = 0; propertyIndex < properties.Count; propertyIndex++)
        {
            var property = properties[propertyIndex];
            if (property != null && !indexedProperties.ContainsKey(property.Identifier))
            {
                indexedProperties.Add(property.Identifier, property);
            }
        }
    }

    void ISerializationCallbackReceiver.OnBeforeSerialize()
    {
        if (data != null && properties != null)
        {
            for (int dataIndex = 0; dataIndex < data.Length; ++dataIndex)
            {
                if (data[dataIndex] != null && !properties.Any(item => item.Identifier.Equals(data[dataIndex].Identifier)))
                {
                    properties.Add(data[dataIndex].Create());
                }
            }

        }
    }
}

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:

    public override Property Create()
    {
        return new FloatProperty(this);
    }

6164607--674304--property-provider.png

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?

public class Buff : MonoBehaviour
{
    private BuffData data;
    private Character character;

    private void Update()
    {
        data.Do(character);
    }

    public static Buff Create(Character character, BuffData data)
    {
        //...
    }
}

public abstract class BuffData : ScriptableObject
{
    public abstract void Do(Character character);
}

With an implementation looking like:

public sealed class RegenerationBuff : BuffData
{
    [SerializeField]
    private PropertyData targetProperty;

    [SerializeField]
    private float amount;
 
    public void Do(Character character)
    {
        var provider = character.GetComponent<PropertyProvider>();
        var property = provider.GetValue(targetProperty.identifier) as (FloatProperty);

        if(property != null) property.Value += (amount * Time.deltaTime);
    }
}

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?

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:

<property>
    <identifier>Rage</identifier>
    <defaultValue>0</defaultValue>
</property>

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.

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?

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”.

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.