Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question How Can I Have A Scriptable Object Inherited Serialized Property With Different Default Values?

Discussion in 'Scripting' started by Katerlad, May 18, 2023.

  1. Katerlad

    Katerlad

    Joined:
    Mar 26, 2015
    Posts:
    16
    Hello!

    I am running into an issue where I am building out Inherited Scriptable Objects for my Item Types in my game.

    Id like to have it where the base variable is one default, but child classes have a different default.

    This works until I serialize the property so that I can override the default in the inspector if I would like.

    I get this error: The same field name is serialized multiple times in the class or its parent class. This is not supported: Base(Weapon) <GripType>k__BackingField

    Why doesnt Unity only try to serialize the last childs backing field instead of each one? Is there a work around? Using regular fields in its place gives the same error.

    Code (CSharp):
    1. public class Item : ScriptableObject
    2. {
    3.     [field:SerializeField]
    4.     public virtual GripType GripType { get; set; } = GripType.Item;
    5. }
    6.  
    7. Public class Weapon : Item
    8. {
    9.     public override GripType GripType { get; set; } = GripType.Weapon;
    10. }
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,378
    In your code you've marked your property virtual and overriden it. And in your override you've implemeneted with the implicit 'get/set' which creates a private backing field. That's the k__BackingField, and you've effectively done it twice. Here's the thing about private members, child classes can have private members with identical names to the private members in the class it inherited from. This is because private members have an access modifier relative to the explicit class in question, so there is no naming conflict.

    Unraveling some of the syntax sugar, you've effectively written:
    Code (csharp):
    1. public class Item : ScriptableObject
    2. {
    3.     [SerializeField]
    4.     private GridType <GripType>k__BackingField = GridType.Item;
    5.     public virtual GripType GripType
    6.     {
    7.         get => <GripType>k__BackingField;
    8.         set => <GripType>k__BackingField = value;
    9.     }
    10. }
    11.  
    12. public class Weapon : Item
    13. {
    14.     [SerializeField]
    15.     private GridType <GripType>k__BackingField = GridType.Weapon;
    16.     public override GripType GripType
    17.     {
    18.         get => <GripType>k__BackingField;
    19.         set => <GripType>k__BackingField = value;
    20.     }
    21. }
    (note - you can't actually write this as valid C#, the compiler makes a special exception for implicit backing fields and the <...> naming. This naming convention avoids accidental naming collisions amongst other things)

    BUT

    When you go to serialize, it does become a naming conflict, because the serializer doesn't actually care what the access modifier nor what the compiler thinks. It has its own set of rules. And it will serialize your concrete class as a flattened hierarchy of all fields and each name has to be unique.

    ...

    I should also point out that without serialization you've effectively bloated your Weapon class with 2 fields, only 1 of which is actually used. Sure... it's only a few bytes, but you likely weren't even aware you were doing this.

    ...

    Now, with that said. Don't mark it virtual. Marking it virtual isn't necessary for modifying the property. The property is publicly read/write... so just write to it the default you want. This will work:

    Code (csharp):
    1. public class Item : ScriptableObject
    2. {
    3.  
    4.     [field: SerializeField]
    5.     public GripType GripType{ get; set; } = GripType.Item;
    6.  
    7. }
    8.  
    9. public class Weapon: Item
    10. {
    11.  
    12.     public Weapon()
    13.     {
    14.         this.GripType = GripType.Weapon;
    15.     }
    16.  
    17. }
    When Weapon is constructed it'll call the constructors up the chain initializing its fields from top down. So Item will set GridType default to Item, but then Weapon's constructor will change it to Weapon. And then the serializer will get involved all after that resulting in the correct defaults getting serialized on creation of the asset in your Assets folder.

    I will point out that using the constructor for ScriptableObject or MonoBehaviours is generally frowned upon in the Unity documentation. But really this is just because the constructor can be called at unknown times and accessing ANY of the API isn't possible during it. But it's fine if you're just using it for initializing defaults using simple value types (enums, numbers, etc)... I do it all the time, for the very same reason you're wanting to do it here.

    Under the spoiler you can see it in my current project I just wrapped up 2 days ago:
    Code (csharp):
    1.     [DefaultExecutionOrder(SPEntity.DEFAULT_EXECUTION_ORDER)]
    2.     public class IEntity : SPEntity
    3.     {
    4.  
    5.         #region Fields
    6.  
    7.         [SerializeField]
    8.         private EntityType _entityType;
    9.  
    10.         #endregion
    11.  
    12.         #region CONSTRUCTOR
    13.  
    14.         public IEntity() : base()
    15.         {
    16.  
    17.         }
    18.  
    19.         public IEntity(EntityType defaultType)
    20.         {
    21.             _entityType = defaultType;
    22.         }
    23.  
    24.         #endregion
    25.  
    26.         #region Properties
    27.  
    28.         public EntityType EntityType
    29.         {
    30.             get => _entityType;
    31.             set => _entityType = value;
    32.         }
    33.  
    34.         #endregion
    35.  
    36.     }
    37.  
    38.     [DefaultExecutionOrder(SPEntity.DEFAULT_EXECUTION_ORDER)]
    39.     public class PlayerEntity : IEntity, PlayerHealth.IOnPlayerHealthChanged
    40.     {
    41.  
    42.         public static readonly UniqueToEntityMultitonPool<PlayerEntity> PlayerPool = new UniqueToEntityMultitonPool<PlayerEntity>();
    43.  
    44.         public static readonly System.Func<PlayerEntity, bool> FindPlayer = (e) => e.EntityType == EntityType.Player;
    45.  
    46.         #region CONSTRUCTOR
    47.  
    48.         public PlayerEntity() : base(EntityType.Player) { }
    49.  
    50.  
    51.         protected override void OnEnable()
    52.         {
    53.             PlayerPool.AddReference(this);
    54.             base.OnEnable();
    55.         }
    56.  
    57.         protected override void OnDisable()
    58.         {
    59.             PlayerPool.RemoveReference(this);
    60.             base.OnDisable();
    61.         }
    62.  
    63.         #endregion
    64.  
    65.         #region IOnPlayerDeath Interface
    66.  
    67.         void PlayerHealth.IOnPlayerHealthChanged.OnHealthChanged(PlayerEntity entity, PlayerHealth health, float prevHealth)
    68.         {
    69.             if (entity != this) return;
    70.  
    71.             if (this.TryGetComponent<IMotor>(out IMotor m)) m.Paused = health.CurrentHealth <= 0;
    72.         }
    73.  
    74.         #endregion
    75.  
    76.     }
     
    Last edited: May 18, 2023
    Katerlad likes this.
  4. Katerlad

    Katerlad

    Joined:
    Mar 26, 2015
    Posts:
    16
    @lordofduct - I cannot thank you enough for taking the time to write all of that out. I see your posts frequently throughout unity forums and I swear you have touched on some of the harder hitting questions I always google, and your name is somewhere lol.

    Thanks for the lesson, I was coming to some of those conclusions in my testing, but having you spell it out clarifies it and helps me understand to a T.

    Getting rid of the virtual and just setting the default in the constructor seems to make sense to me! Will Serialization let me override the default in the Inspector if I chose to do so? I guess I can just test it out and see right now.

    @Kurt-Dekker - Dont worry I see you helping all the time as well, I appreciate the response but I don't think its gonna solve what I am looking for this time around.
     
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Reset() is called when a scriptable object has just been created. It's a perfectly fine way to set default values in scriptable objects, and will also be called when you right-click -> Reset on the object's context menu.
     
    lordofduct likes this.
  6. Katerlad

    Katerlad

    Joined:
    Mar 26, 2015
    Posts:
    16
    Ahh then I should give @Kurt-Dekker more props then, I think when I opened the Docs page I mis- interpreted the use, but really it kinda solves this problem in a simpler way compared to lordofduct.

    I will try these methods out and thank you for making sure you kept me in line spiney199. haha
     
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    I feel it is the BEST way actually, because when you do
    public int foo = 1;
    and then later change it to
    public int foo = 2;
    in the source code, to the average programmer just starting out in Unity it is absolutely baffling why
    foo
    still stubbornly remains 1.

    And it's such a common misunderstanding on this forum that I have a blurb! I have a lot of blurbs:

    Serialized properties in Unity are initialized as a cascade of possible values, each subsequent value (if present) overwriting the previous value:

    - what the class constructor makes (either default(T) or else field initializers, eg "what's in your code")

    - what is saved with the prefab

    - what is saved with the prefab override(s)/variant(s)

    - what is saved in the scene and not applied to the prefab

    - what is changed in Awake(), Start(), or even later etc.

    Make sure you only initialize things at ONE of the above levels, or if necessary, at levels that you specifically understand in your use case. Otherwise errors will seem very mysterious.

    Here's the official discussion: https://blog.unity.com/technology/serialization-in-unity

    Field initializers versus using Reset() function and Unity serialization:

    https://forum.unity.com/threads/sensitivity-in-my-mouselook-script.1061612/#post-6858908

    https://forum.unity.com/threads/crouch-speed-is-faster-than-movement-speed.1132054/#post-7274596
     
    Katerlad likes this.
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,378
    The only clarifying point I want to add about 'Reset' is that 'Reset' is only called in the editor when the asset is created or when you click the Reset option in its popup (upper right of inspector ... thing).
    upload_2023-5-18_12-15-50.png

    This means if you say call 'ScriptableObject.CreateInstance' at runtime (in editor or in a build), Reset isn't called, and if you are relying on Reset to set default values... they won't be set.

    With the same respect... I almost never create instances of ScriptableObjects at runtime. I almost exclusively create them as assets at editor time. But it's another "hidden" bug you could run into if you went this route and then one day decided to call CreateInstance and your object doesn't initialize correctly.

    I personally use a combination of field initializer, constructor, and Reset in the context they're called for. If I want a field initialized to simple value types... field initializer/constructor. If I want to initialize field relative to the unity API, it's Awake. And if I want to cross reference between objects, I use Start. And if I want to perform an action when the object is Reset, I use Reset. May any of those actions include changing a field.

    Basically... I couldn't say which one is the BEST way. Each have gotchas. Like field initializers can only take constants or other staticly constructable types. The constructor can't access unity api. Awake can't cross-talk between unity objects and happens after deserialize. Start happens way late (after deserialize as well) and isn't on ScriptableObject. And Reset only occurs in the editor and not during CreateInstance.
     
    Last edited: May 18, 2023
    orionsyndrome and Kurt-Dekker like this.
  9. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    This is an EXTREMELY good callout... Thanks lord... same goes for .AddComponent<T>() obviously.

    This is why whenever I am fabbing stuff in code I (almost) always follow this pattern:

    Factory Pattern in lieu of AddComponent (for timing and dependency correctness):

    https://gist.github.com/kurtdekker/5dbd1d30890c3905adddf4e7ba2b8580

    Most things I make will either be authored via the Unity Editor inspector interface, OR produced entirely in code. Rarely does one of my classes do both.