How to inherit from List<T> / make a List<T> propertyDrawer ?

I have a Serializable class that inherits from List. (Where T is a given monobehaviour)
To my dismay, the inspector does not naturally draws this class as a list, only the property’s name appears there. (And sub properties if I try adding some to the class).

So I tried to create a custom PropertyDrawer for this class, but I can’t figure out how to access the list’s content from here :
First, I tried using property.arraySize and property.GetArrayElementAtIndex(), buit I get error saying the property isn’t an Array.

Then I thought, maybe the array is actually a sub-property of Lists. So I tried using property.Next() to explore it… but I don’t find any sub-property at all except those I added to the class.

If I switch the inspector to debug mode, I can actually see the list and its content, but it’s readonly. The content of the list is also no longer remembered when closing a scene. (I had another script fill it up in OnValidate.)

If you can not see anything then it means that nothing is being serialized. A custom property drawer will not help here.
I believe we have special handling for lists and arrays so its possible that inheriting from them will not work.

You may be better just having a class that has a list property internal and implementing a PropertyDrawer for that.

[Serializable]
public class MyList<T> : IList
{
    [SerializeField]
    List<T> m_List;
 
    // Implement IList fields
}
2 Likes

@karl_jones Is it not possible to override unity’s default handling of lists and arrays in the editor. I don’t really want to create a custom drawer for each data and object type I use, and the default list/array drawer is less than ideal.

The primary reason for this relates to this comment indicating that the “inspector overwrites values of serialized items after construction” which seems counter intuitive as it removes the ability for default states without either a custom drawer and/or editor. (Massive time-sink)

No this is not possible. You could do what was suggested in the thread and do initialization after serialization.

e.g

[System.Serializable]
public class TestClass : ISerializationCallbackReceiver
{
    [SerializeField, HideInInspector]
    bool m_Initialized = false;

    private const int DefaultValue = 50;

    public int test_int = DefaultValue;

    public TestClass()
    {
        m_Initialized = true;
        Debug.LogError("constructor");
        test_int = 50;
        Debug.LogError("constructor_end = " + test_int);
    }
  
    public void OnAfterDeserialize()
    {
        if (!m_Initialized)
        {
            m_Initialized = true;
            test_int = DefaultValue;
        }
    }

    public void OnBeforeSerialize(){}
}
1 Like

For a one or two off’s this is fine and I’ve done it before with multi-scene entity reference serialization, but for 20-30 serializable classes (so far) that have default states which are used in multiple other locations from ScriptableObjects to MonoBehaviors, it becomes expensive from a time and maintenance perspective.

The amusing part is I have no clue how I’ve not run into this need before in over a decade of unity development.

If you could have a base class for all your 20-30 classes, it could be relatively easy:

public abstract class Initializable : ISerializationCallbackReceiver
{
    [SerializeField, HideInInspector]
    bool m_Initialized = false;

    public TestClass()
    {
        m_Initialized = true;
        OnInitialize();
    }

    public virtual void OnInitialize() { }

    public void OnAfterDeserialize()
    {
        if (!m_Initialized)
        {
            m_Initialized = true;
            OnInitialize();
        }
    }
    public void OnBeforeSerialize() { }
}

Then you just use OnInitialize instead of your constructor. It doesn’t prevent new added items from being copies of the previous item, I don’t think you could do that with the default list drawers, but it should at least allow you to set defaults for initial objects and to prevent undesired type defaults from being used.

That said, karl_jones suggestion about creating a wrapper class is not that hard now that Unity 2020 added generics serialization. First, you create a wrapper struct or class* around Lists or Arrays. Then, you implement your property drawer on the generic wrapper type, with your own custom behavior for adding and removing things from the list:

[System.Serializable]
// It doesn't have to be sealed, but it helps us to get the type of T in the property drawer.
public sealed class ListWrapper<T>
{
    public List<T> list;
}

//From Unity 2020 on, you can use it like this:
public class MyScript : MonoBehaviour
{
    public ListWrapper<float> aListOfNumbers;
}

Then, the PropertyDrawer could be:

[CustomPropertyDrawer(typeof(ListWrapper<>))]
class ListWrapperDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var listProperty = property.FindPropertyRelative("list");
        //Do your own thing here. If you need to get the list type, you can do:
        var type = fieldInfo.FieldType.GetGenericArguments()[0];
    }
}

The main caveat is that you would have to draw your own list. Reordering lists can be a bit hard from inside property drawers, as you can’t retain the state of the UI inside them. I know of this repo on Github that has worked for me in the past, and it works with property drawers: https://github.com/cfoulston/Unity-Reorderable-List. It might work for you.

If you can work with UIToolkit, it’s easier, because the UI is already retained. 2021.2 ListViews already support items of multiple heights. If you want to support older versions, this repo of mine might help you in a pinch: https://github.com/OscarAbraham/UITKEditorAid.

  • A struct wrapper would eliminate memory overhead, but you’d have to be careful when passing it as a function parameter, because it will be copied (unless passed with the ref keyword).

There is some base class sharing already so it’s not beyond the realm of possibility. I’ll have to take a close look to see what side effects present themselves, but this may be viable.

Sadly I can’t upgrade to 2020 LTS for this project as unity threw-out the XRInputSystem that they had introduced in 2018.x/2019.x which causes some serious re-work to our entire cross-platform user input solution. Additionally 2020 LTS for some reason that I’ve not delved too deeply into yet, breaks the scriptable objects and singleton scriptable objects that I use for managing scene independent data and entity references. I’ve just not got the sanity to re-engineer that system, then re-populate all of the blanked data. (It took months fulltime to create/test, and currently manages, 135 multi-scene entity references and the config data for another 167 nodes that also reference those entities. The frightful part is it may have as many as 1200 by the time this is all finished. Looking back, I should have implemented a proprietary custom serialization solution and configuration tool for it instead… lesson learnt.)

Ough. I get it. I’ve been dealing with a couple of projects that use Scriptable Objects quite a bit; although I think I’ve got the hang of it, it’s not been without a learning curve and handling some quirks. I never could get SO singleton assets to work well, though, specially when they referenced lists of objects: they tended to cause chaos in Version Control, as it was not unusual for multiple people to need to change them at the same time, sometimes even without consciously noticing it. If you want, I’d be more than happy to exchange notes about SOs over DM.

I was thinking that my suggestion about overriding the list drawer wouldn’t serve you anyway; the problem doesn’t happen in the drawers. It happens every time the SerializedProperty of an array adds an element, because the new element’s child properties are already set before the item is constructed at the moment of calling ApplyModifiedProperties. The only way I can think of solving that issue from a single drawer, without having to do a lot of reflection, is to have the [SerializeReference] attribute on all your lists. That way, you could ensure the constructor is used by assigning Activator-created instances to managedReferenceValue every time an element is added. SerializeReference has a lot its own quirks, though. So, even if you could update to 2020, using ISerializationCallbackReceiver seems like the most convenient solution for you, in my opinion.