Default values for serializable class not supported?

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:

[System.Serializable]
public class AudioChannel
{
	public float volume = 1;
}

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.

2 Likes

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.

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:

[System.Serializable]
public class AnimationStep {
    public AnimationClip[] animations;
    public float playNextInPercent = 1f;
    public AnimationStep() {
    }
}

And maybe razorcut’s problem is the same.

It’s weird.

2 Likes

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?

2 Likes

Here is the solution I came up with:

    private static Dictionary<string, int> monitoredArrayMap = new Dictionary<string, int>();
    private static UnityEngine.Object m_lastHandleArchetypeId = null;
     
    public static void MonitorArray(UnityEngine.Object targetObject, string arrayKey, Array array, Action<int> needsInitFunction)
    {
        if (targetObject == null || array == null || string.IsNullOrEmpty(arrayKey))
        {
            return;
        }

        if (m_lastHandleArchetypeId == null || m_lastHandleArchetypeId != targetObject)
        {
            m_lastHandleArchetypeId = targetObject;
            monitoredArrayMap.Clear();
            monitoredArrayMap.Add(arrayKey, array.Length);
        }

        if (!monitoredArrayMap.ContainsKey(arrayKey))
        {
            monitoredArrayMap.Add(arrayKey, array.Length);
        }

        int previousLength = monitoredArrayMap[arrayKey];
        if (array.Length != previousLength)
        { 
            monitoredArrayMap[arrayKey] = array.Length;
            if (array.Length > previousLength)
            {
                for (int i = previousLength; i < array.Length; i++)
                {
                    if (needsInitFunction != null)
                    {
                        needsInitFunction(i);
                    }
                }

                EditorUtility.SetDirty(targetObject);
            }
        }
    }

This can be used within any custom editor by simply calling the static method and providing a unique key:

public class MyComponent : MonoBehaviour
{

        public string[] FirstArray = null;
        public int[] SecondArray = null;

}
[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : Editor
{

    public override void OnInspectorGUI()
    {
  
        MyComponent castedComponent = target as MyComponent;
        InspectorArrayDefaultValueWatcher.MonitorArray(target, "arrayone", castedComponent.FirstArray, (int i) => { castedComponent.FirstArray[i] = string.Format("Element {0}", i+1);  });
        InspectorArrayDefaultValueWatcher.MonitorArray(target, "arraytwo", castedComponent.SecondArray, (int i) => { castedComponent.SecondArray[i] = i+1; });

        base.OnInspectorGUI();
    }

}

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:

  • 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.
  • you can handle as many arrays as you like as long as you provide unique keys.
  • 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 :slight_smile:

(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?

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?

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!

Bugger. Just hit this limitation. Real shame.

1 Like

It feels veeery bad after all these years this is still a problem. I mean, 7 years and still no fix?! Really?

2 Likes

Erm, fix for what?

Log a bug report. Unity can’t fix problems it doesn’t know about.

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.

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!

3 Likes

I’ve ran into this problem when I was writing my own serializable List 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.

2 Likes

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.

1 Like

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.

1 Like

Why not just put your defaults in a custom method, and have that method be called from void Reset() ?

1 Like

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.

2 Likes

In the future, if anyone else comes across this issue with List custom serializable classes, the following worked really well for me.

The Reset() method is your friend. :slight_smile:

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:

public List<MyCustomClass> MyList = new List<MyCustomClass>();

[System.Serializable]
public class MyCustomClass
{
     public Color MyCustomColor = new Color(1, 0, 0, 0.5f);
}

void Reset()
{
     MyList = new List<MyCustomClass>()
     {
           new MyCustomClass()
     };
}
10 Likes