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

Default values for serializable class not supported?

Discussion in 'Scripting' started by RazorCut, Mar 2, 2010.

  1. RazorCut

    RazorCut

    Joined:
    May 7, 2009
    Posts:
    393
    Hello, I'm trying to have an editor-inspectable class have default values, but those default values don't seem to work. Here is a simple example:

    Code (csharp):
    1.  
    2. [System.Serializable]
    3. public class AudioChannel
    4. {
    5.     public float volume = 1;
    6. }
    7.  
    When I declare an AudioChannel is my monobehavior, it's volume shows up as 0, not 1.

    How can I have default values appear?

    Thanks.
     
    DragonCoder and agate-pris like this.
  2. andeeeee

    andeeeee

    Joined:
    Jul 19, 2005
    Posts:
    8,768
    If you've modified the value of a variable in the inspector, then the modified value always overrides the value supplied in the code, even if you change it.
     
  3. mepine

    mepine

    Joined:
    Aug 4, 2011
    Posts:
    7
    I got the same problem and I didn't change its value. It's changed to 0 when the serializable class instance is created.

    Here is my code:
    Code (csharp):
    1.  
    2. [System.Serializable]
    3. public class AnimationStep {
    4.     public AnimationClip[] animations;
    5.     public float playNextInPercent = 1f;
    6.     public AnimationStep() {
    7.     }
    8. }
    9.  
    And maybe razorcut's problem is the same.

    It's weird.
     
  4. Pharan

    Pharan

    Joined:
    Oct 28, 2013
    Posts:
    102
    Does this have a solution yet?

    I want my float to default to 1 but newly added components or instantiated ScriptableObjects with an array of [Serializable] members with floats in them always default to 0.

    I guess it has something to do with having an array of [Serializable] classes with primitives, or other similar situations?
     
    Last edited: Dec 31, 2014
    spider853 and far20shid like this.
  5. GingerLoaf

    GingerLoaf

    Joined:
    Dec 5, 2012
    Posts:
    18
    Here is the solution I came up with:

    Code (CSharp):
    1.  
    2.     private static Dictionary<string, int> monitoredArrayMap = new Dictionary<string, int>();
    3.     private static UnityEngine.Object m_lastHandleArchetypeId = null;
    4.      
    5.     public static void MonitorArray(UnityEngine.Object targetObject, string arrayKey, Array array, Action<int> needsInitFunction)
    6.     {
    7.         if (targetObject == null || array == null || string.IsNullOrEmpty(arrayKey))
    8.         {
    9.             return;
    10.         }
    11.  
    12.         if (m_lastHandleArchetypeId == null || m_lastHandleArchetypeId != targetObject)
    13.         {
    14.             m_lastHandleArchetypeId = targetObject;
    15.             monitoredArrayMap.Clear();
    16.             monitoredArrayMap.Add(arrayKey, array.Length);
    17.         }
    18.  
    19.         if (!monitoredArrayMap.ContainsKey(arrayKey))
    20.         {
    21.             monitoredArrayMap.Add(arrayKey, array.Length);
    22.         }
    23.  
    24.         int previousLength = monitoredArrayMap[arrayKey];
    25.         if (array.Length != previousLength)
    26.         {
    27.             monitoredArrayMap[arrayKey] = array.Length;
    28.             if (array.Length > previousLength)
    29.             {
    30.                 for (int i = previousLength; i < array.Length; i++)
    31.                 {
    32.                     if (needsInitFunction != null)
    33.                     {
    34.                         needsInitFunction(i);
    35.                     }
    36.                 }
    37.  
    38.                 EditorUtility.SetDirty(targetObject);
    39.             }
    40.         }
    41.     }
    42.  
    This can be used within any custom editor by simply calling the static method and providing a unique key:

    Code (CSharp):
    1.  
    2. public class MyComponent : MonoBehaviour
    3. {
    4.  
    5.         public string[] FirstArray = null;
    6.         public int[] SecondArray = null;
    7.  
    8. }
    9.  
    Code (CSharp):
    1.  
    2. [CustomEditor(typeof(MyComponent))]
    3. public class MyComponentEditor : Editor
    4. {
    5.  
    6.     public override void OnInspectorGUI()
    7.     {
    8.  
    9.         MyComponent castedComponent = target as MyComponent;
    10.         InspectorArrayDefaultValueWatcher.MonitorArray(target, "arrayone", castedComponent.FirstArray, (int i) => { castedComponent.FirstArray[i] = string.Format("Element {0}", i+1);  });
    11.         InspectorArrayDefaultValueWatcher.MonitorArray(target, "arraytwo", castedComponent.SecondArray, (int i) => { castedComponent.SecondArray[i] = i+1; });
    12.  
    13.         base.OnInspectorGUI();
    14.     }
    15.  
    16. }
    17.  
    If you are very concerned with the amount of work being done you might even consider wrapping these calls with the "EditorGUI.BeginChangeCheck" and "EditorGUI.EndChangeCheck" calls and only operate when the end check returns true. Not sure if that works.. I haven't tried it yet.


    This isn't what i'd call an elegant solution, but given the lack of any easy hook, this is the best I came up with.

    Important notes:
    1. The unity object is used as the key in order to prevent memory from piling up as you traverse your hierarchy. Once the key changes, the in-memory map is wiped.
    2. you can handle as many arrays as you like as long as you provide unique keys.
    3. because arrays can be null, the decision was made to pass in the array reference so that the null check could be encapsulated.

    Hope this helps! It certainly helped me :)
     
    Last edited: Jan 6, 2015
  6. Pharan

    Pharan

    Joined:
    Oct 28, 2013
    Posts:
    102
    (1) The basic logic here is to have the editor watch for when the array increases in length, right? And then if it does, it calls your custom delegate to set values.

    I guess this would work.
    Another caveat would be this:
    It makes sense when you set the length of the array from 0 to 1, because that's when the setting-defult-values behavior fails.

    But after that, Unity's default behavior is also to make a copy of the last item of the array when you add to its length. This is actually desired behavior sometimes (though maybe not always) when you want to make several items with similar values. Point is though that it's useful enough that it should probably still be an option. I suppose it's a simple change to only check a change in length from 0 to 1 instead of from any previousLength to any number higher.

    It's unfortunate then that this also means anywhere you have the array of [Serializable] stuff, you'd need to make a new custom Editor for that class.

    Could the MonitorArray call possibly be put in a PropertyDrawer/PropertyAttribute to remove the need for a custom Editor for each class that needs it? (I'm not too experienced with Editor stuff so I don't know what's possible or what makes sense in this area. I've always found this part of the docs really vague too. The js code doesn't help)

    [edit] oh, they actually added c# examples now.

    (2) You're passing `MyComponent.FirstArray` as the third parameter. What is that? Is .FirstArray passed as a static property of MyComponent? Is that really the syntax?
     
  7. fire7side

    fire7side

    Joined:
    Oct 15, 2012
    Posts:
    1,819
    Why not just use a private and give it a default and then test it against the public, or do privates default to 0 also?
     
  8. GingerLoaf

    GingerLoaf

    Joined:
    Dec 5, 2012
    Posts:
    18
    I completely agree with you about how inconvenient this is. I would love for unity to expose a better solution, but for the time being, this is (for my project) the best solution. I investigated using a property drawer before I investigated this. I like the idea of just adding a [DefaultArrayValue] tag (or something) to a variable and having it automatically pull in defaults whenever the collection is increased. Unfortunately, I found that there is no elegant way for that to work either. Just by adding a custom property drawer (even if you don't override any functionality or even if you call the base functionality) the default UI becomes disabled and you get an ugly "No GUI Implemented" message stacked on top of some other text rendering it unreadable. If this were an array of some other monobehaviour, the Editor.CreateEditor would be enough to draw the proper default GUI, but this does not work for Serializable objects. I've spent some time in the past creating a function that renders a serializable object's default editor, but it is incredibly complex and I stopped wasting my time on it. Sadly, custom property drawers did not provide a better solution than the custom editor solution.

    I like what you said about the repeating elements, however, in my game I want to force my new elements to the defaults. You may want it a different way, which is why I exposed delegates for adding custom behaviour (that as well as not liking the idea of creating an interface that tightly couples your production and editor code together).

    Lastly, the MyComponent.FirstArray section was the result of me trying to transform my game code from something I don't want to share on the internet to something generic for everyone. It was a mistake :). I have edited my code to make that more clear. MyComponent was supposed to be a reference to the component you are editing, and FirstArray is the array reference (not the elements, but the array reference itself). I will add a class declaration to clear things up.

    Once again, I wish there were a more elegant solution, but this is the best I could come up with :). Feel free to find your own solutions and post them!
     
  9. Nodrap

    Nodrap

    Joined:
    Nov 4, 2011
    Posts:
    77
    Bugger. Just hit this limitation. Real shame.
     
    spider853 likes this.
  10. Bloody-Swamp

    Bloody-Swamp

    Joined:
    Jul 30, 2012
    Posts:
    37
    It feels veeery bad after all these years this is still a problem. I mean, 7 years and still no fix?! Really?
     
  11. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Erm, fix for what?
     
  12. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    Log a bug report. Unity can't fix problems it doesn't know about.
     
  13. Bloody-Swamp

    Bloody-Swamp

    Joined:
    Jul 30, 2012
    Posts:
    37
    You're right. Nothing to fix since it's not a bug. I meant "no implementation yet", sorry.

    Well, since it's not a bug (I shouldn't say "fix") but a missing feature of the serialization process - that I'm sure the Unity team is aware of - and it has been 7 years with no one to look into it, it makes it hard to write clean code in that context when working with the Editor and Editor tools.
     
  14. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,769
    Sounds to me like you are the most qualified to look into it, implement a great package to do it, sell it on the Unity Asset Store and make millions!
     
  15. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    897
    I've ran into this problem when I was writing my own serializable List<T> class (yes unity already serializes lists, my class just adds an inspector drawer plus a couple features). And the solution for fixing this is a simple one-liner:

    Set the new instance to default(Type).

    Thats it. The serializer system will then handle the rest and insert the default values you've inserted in code.
     
    Last edited: Sep 17, 2017
    Bloody-Swamp and KelsoMRK like this.
  16. Bloody-Swamp

    Bloody-Swamp

    Joined:
    Jul 30, 2012
    Posts:
    37
    Although it works on single instances (and thank you a million for that!), it won't work on regular arrays (which I understand why).

    As for
    I didn't know trolls were allowed on Unity forums.
     
    Kurt-Dekker likes this.
  17. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    897
    which is one of the reasons I made my own serializableList class. That way when I add an instance to the list (via the inspector), it can load in the default as the new element for that list.
     
    Bloody-Swamp likes this.
  18. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,495
    Why not just put your defaults in a custom method, and have that method be called from void Reset() ?
     
    Bloody-Swamp likes this.
  19. Bloody-Swamp

    Bloody-Swamp

    Joined:
    Jul 30, 2012
    Posts:
    37
    Sure, everything can be done. Is it the right way to do it? Weeeell, although there is no right way but the one that works, I try to avoid "hacks". It produces nasty garbage code and it's definitely bad practice and hard to maintain.
     
    zORg_alex and thebarryman like this.
  20. amisner2k

    amisner2k

    Joined:
    Jan 9, 2017
    Posts:
    43
    In the future, if anyone else comes across this issue with List<T> custom serializable classes, the following worked really well for me.

    The Reset() method is your friend. :)

    With the code below, when you add your component in the Inspector, your array of MyCustomClass objects should get created for you automatically with the default value being set as expected.

    In your MonoBehaviour class:

    Code (CSharp):
    1.  
    2. public List<MyCustomClass> MyList = new List<MyCustomClass>();
    3.  
    4. [System.Serializable]
    5. public class MyCustomClass
    6. {
    7.      public Color MyCustomColor = new Color(1, 0, 0, 0.5f);
    8. }
    9.  
    10. void Reset()
    11. {
    12.      MyList = new List<MyCustomClass>()
    13.      {
    14.            new MyCustomClass()
    15.      };
    16. }
    17.  
     
  21. ALKubo

    ALKubo

    Joined:
    Mar 3, 2017
    Posts:
    3
    thank you amisner2k. You solved my problem :)
     
    NortalNeis and amisner2k like this.
  22. ETGgames

    ETGgames

    Joined:
    Jul 10, 2015
    Posts:
    88
    For people coming from the future, I fixed it by making the class implement
    ISerializationCallbackReceiver, and set the default value in OnAfterDeserialize method
     
    Kaideu, mondzi, far20shid and 5 others like this.
  23. Jamez0r

    Jamez0r

    Joined:
    Jul 29, 2019
    Posts:
    200
    Thanks, worked for me too. Was having trouble with a custom System.Serializable class that I had a List of, and then had a CustomEditor for it. That Reset() made it load with the correct default values
     
    amisner2k likes this.
  24. digital_monk

    digital_monk

    Joined:
    Jun 2, 2017
    Posts:
    10
    Thanks, this one worked!
     
    ETGgames likes this.
  25. SufferinPup

    SufferinPup

    Joined:
    Jul 7, 2012
    Posts:
    24
    When using this method, how do you know when the value has been modified and not to set it to the default value?
     
  26. ThePilgrim

    ThePilgrim

    Joined:
    Apr 25, 2013
    Posts:
    14
    Thanks, this helped me in my case because the default value of 0 for my float variable was not something the value would ever be set to, so I could safely set it to my custom default if it was found to me zero in OnAfterDeserialize().

    I'm not sure how to make this solution work if the default of the value is something it might need to be set to.
     
  27. Phoder1

    Phoder1

    Joined:
    Nov 6, 2018
    Posts:
    4
    This worked for me:

    [SerializeField, HideInInspector]
    private bool _serielized = false;

    //Change the values to default right before the first seriealization.
    public void OnBeforeSerialize()
    {
    if (_serielized)
    return;

    //set to default values
    threadPriority = ThreadPriority.Normal;
    allowSceneActivation = true;

    //Block future seriealizations overrides
    _serielized = true;
    }
    public void OnAfterDeserialize() { }
     
  28. DearUnityPleaseAddSerializableDictionaries

    DearUnityPleaseAddSerializableDictionaries

    Joined:
    Sep 12, 2014
    Posts:
    135
    But what if you add a new field later on? _serialized will always be true.

    Thanks for the solution, I guess this will have to do! (This is the best solution I have come across so far).

    I put it on OnAfterDeserialize, does it make a difference? Life saver btw but I hope Unity can help give this feature instead of us hacking it.

    Anyways, thanks for posting the code explicitly because it's the quickest way for us to understand the solution. Took me 10 seconds to understand. Might be nice for the thread OP to link it from the top to help people find this solution quicker.
     
    Last edited: Sep 2, 2021
    Seromu and skilani like this.
  29. Crayden

    Crayden

    Joined:
    Dec 20, 2019
    Posts:
    6
    A full example that works:

    Code (CSharp):
    1. [Serializable]
    2. public class YourCustomClass : ISerializationCallbackReceiver
    3. {
    4.     public float valueA;
    5.     public float valueB;
    6.     [SerializeField] private bool _serialized = false;
    7.  
    8.     public void OnBeforeSerialize()
    9.     {
    10.     }
    11.  
    12.     public void OnAfterDeserialize()
    13.     {
    14.         if (_serialized == false) {
    15.             valueA = 1.0f;
    16.             valueB = 2.0f;
    17.         }
    18.     }
    19. }
    Not very pretty that the _serialized field shows up in the Editor, but hiding it made the changes reset somehow..
     
    Last edited: Apr 26, 2022
    skilani and far20shid like this.
  30. MartyMcFly

    MartyMcFly

    Joined:
    May 2, 2013
    Posts:
    14
    You should set _serialized = true in line 15. It will always stay false.
     
    skilani and Bunny83 like this.
  31. skilani

    skilani

    Joined:
    Aug 31, 2013
    Posts:
    7
    To any future onlookers, you still need to keep [SerializeField] on _serialized, otherwise when you press Play your changes to any fields that you are setting a default value to will be overridden. Nice solution though Crayden, cheers. You can also modify it (like an above example) to be [SerializeField, HideInInspector] so it doesn't show in the Editor.
     
    AlundraFlint likes this.
  32. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    897
    I think the use of OnBeforeSerialized and "_serialized" is overkill. OnBeforeSerialize makes sense if one is trying to serialize a type that is not supported by Unity's serialization, like Dictionaries. But serialization of default values is something that is already supported by the language itself. Just setting what the default should be on the declaration line is more than enough.

    This also works:
    Code (CSharp):
    1.  
    2. public class MyBehaviour : Monobehaviour
    3. {
    4.   public List<YourCustomStruct> myList = new List<YourCustomStruct>()
    5.       {
    6.           new YourCustomStruct(),
    7.           new YourCustomStruct(){valueA = 3f, valueB = 5f}
    8.       };
    9. }
    10.  
    11. [Serializable]
    12. public struct YourCustomStruct
    13. {
    14.     public float valueA = 1.0f;
    15.     public float valueB = 2.0f;
    16. }
    By default, YourCustomStruct.valueA will be 1.0, myList will have 2 elements, and the 2nd element will have a valueA of 3f.

    In this case, theres no need for OnBeforeSerialize, no Reset() method needed, no _Serialized. The defaults will set the instant the component is created. and resetting it via the inspector also works with no issue. if the type is a value type like a struct, you can even use "default" and get the same thing without all this serialization boiler code. I can do "YourCustomStruct myStruct = default;" and myStruct.valueA will be 1.0f, not 0.

    If you want to set a default reference to some unity object. for example you have a monobehaviour that needs to reference a scriptableObject singleton and want that reference to be assigned by default on all new instances. you can select the monobehaviour script in your project and the inspector willl let you assign a default value for unity object references for all newly created/reset components.
     
  33. huulong

    huulong

    Joined:
    Jul 1, 2013
    Posts:
    223
    Yes, that's compact, and it works as long as default should be to have 1 or 2 values indeed.

    However, if default should be to have 0 entries (and it is valid), then it forces the user to remove the entry automatically added that way except for the times they need it.

    It must also be written for every script embedding YourCustomStruct, but this may be acceptable if only 1 or few scripts do, or if each of those scripts would need different default values anyway.

    It wouldn't work in my case where the array/list should be empty by default, but this could be useful for all the people who expect at least 1 entry indeed.
     
  34. huulong

    huulong

    Joined:
    Jul 1, 2013
    Posts:
    223
    On a repository that provides an improved version of Unity's ReorderableList used in editor to represent sequential containers (Array/List<T>), I found a similar issue: https://github.com/cfoulston/Unity-Reorderable-List/issues/19

    The trick is to define a custom editor (inspector) and create a ReorderableList to wrap the sequential container, then plug your custom behaviour in onAddCallback event (like @Pharan mentioned, Unity's native behaviour is to copy the last element if any, so maybe you could support that by checking the element count and only setting custom default values if adding the first element, and copy the last element else).

    This should be applicable to the native ReorderableList which also has the event.
     
  35. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,533
    Actually it does not, really :)

    First of all field initializers for structs were added in C#10 ( I think .NET 6+). Before that your code would throw an error that instance field initializers are not allowed for structs. Even though C# 10+ allows such field initializers, they are only executed when you explicitly call a constructor for your struct. using default would not work and would still result in "0" values. Unlike C++, C# does not implicitly call constructors for new values types. So creating an array of your struct type with 10 elements would not invoke any constructor. Using "default" would also just result in the usual memory filling and does not cause the field initializers to execute. See this example.

    I haven't really looked into how they actually implemented that new feature. However I guess they simply coupled the field initializer code to the explicit constructor call. When you don't explicitly call a constructor, there would be no field initializer code running.

    Apart from that the default Unity inspector behaviour for arrays is that when you increase the size of the array, the new elements will be clones / duplicates of the last element in the array.

    So at the time this question was asked we did no have C# 10 available. Also without custom editor code this feature would not really help much.
     
  36. Romaleks360

    Romaleks360

    Joined:
    Sep 9, 2017
    Posts:
    69
    Another possible hack is to use OnValidate. This works pretty similar to OnAfterDeserialize solution, but simpler. This will only be usable if the default value is not allowed.

    Code (CSharp):
    1.         void OnValidate()
    2.         {
    3.             if (targets != null)
    4.                 foreach (var target in targets)
    5.                     if (target.Multiply < float.Epsilon)
    6.                         target.Multiply = 1f;
    7.         }