[SerializeReference] GenericSerializedReferenceInspectorUI

Project now should be accessed from github page. GitHub repository :
https://github.com/TextusGames/UnitySerializedReferenceUI

I have made generic [SerializeReference] UI library which allows assigning serialized reference to any child type available in the whole project even in different assemblies (via TypeChache).

There is 3 way you can use it

  • Add [SerialzeReference] [SerialzeReferenceButton] on top of field
    and there button will be drawn that allows to select child types.
  • Add [SerialzeReference] [SerialzeReferenceMenu] on top of field
    and you can press middle mouse button in order to assign child types
  • Via custom property drawer. Use simple method to show automatic menu.

Example of use

    [SerializeReference]
    [SerializeReferenceButton]
    public List<AnimalBase> ButtonAnimals = new List<AnimalBase>();

It proves that collecting all child types, making menu out of them, selecting and assigning it to serialized is possible.
I hope unity will adopt this in the future or make something similar because it is possible and people need this UI functionality for [SerializeReference].

Edited. 02.13.20.
Supports interfaces.
Excludes types derived from UnityEngine.Object.
Excluded abstract classes.
Moved to package.

Edited 02.28.20
Major clean up and refactoring
Core is now just 2 small files
But everething else is bigget becouse it contains type restriction classes.

Added Type restrictions (built in ones like include types, include interfaces, exclude types, exclude interfaces)
Adde type substring restriction( But search is not imblemented in UI yet)
Added ability to combine multiple restriction attributes.
Added ability to write params of restricted types in new restriction attributes.
Changed example.

Edited 02.29.20
GreatlySimplified type restriction code.
There are now 2 built in atributes for restriction that can take both type or interface type as a parameter.

    [Header("Field with multiple restrictions(Include, Exclude - Type, Interface")]
    [SerializeReference]
    [SerializeReferenceButton]
    [SerializeReferenceUIRestrictionIncludeTypes(typeof(MammalBase), typeof(IFish))]
    [SerializeReferenceUIRestrictionExcludeTypes(typeof(RedCat), typeof(IDog), typeof(BlackFish))]
    public IAnimal AnimalWithRestriction = default;

Edited 05.16.20
Project clean up.
Github repository was created for this project.
https://github.com/TextusGames/UnitySerializedReferenceUI
Uploaded new package.
But from now on new versions should be downloaded from github.

Package is in attachement.

5393394–622684–Textus.SerializedReferenceUI_1.0_Preview.unitypackage (12.6 KB)

13 Likes

Core class:

#if UNITY_EDITOR

using System;
using UnityEditor;
using UnityEngine;

public static class SerializedReferenceInspectorUI
{
    public static void ShowContextMenuForManagedReferenceOnMouseMiddleButton( this SerializedProperty property, Rect position )
    {
        var e = Event.current;
        if (e.type != EventType.MouseDown || !position.Contains(e.mousePosition) || e.button != 2)
            return;

        ShowContextMenuForManagedReference(property);
    }

    /// Must be drawn before DefaultProperty in order to receive input
    public static void DrawSelectionButtonForManagedReference(this SerializedProperty property, Rect position)
    {
        var backgroundColor = new Color(0f, 0.8f, 0.15f, 0.67f);
     
        var buttonPosition = position;
        buttonPosition.x += EditorGUIUtility.labelWidth + 1 * EditorGUIUtility.standardVerticalSpacing;
        buttonPosition.width = position.width - EditorGUIUtility.labelWidth - 1 * EditorGUIUtility.standardVerticalSpacing;
        buttonPosition.height = EditorGUIUtility.singleLineHeight;

        var storedIndent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;
        var storedColor = GUI.backgroundColor;
        GUI.backgroundColor = backgroundColor;
     
        var names = GetSplitNamesFromTypename(property.managedReferenceFullTypename);
        var className = string.IsNullOrEmpty(names.ClassName) ? "Null (Assign)" : names.ClassName;
        var assemblyName = names.AssemblyName;
        if (GUI.Button(buttonPosition, new GUIContent(className, className + "  ( "+ assemblyName +" )" )))
        {
            property.ShowContextMenuForManagedReference();
        }
     
        GUI.backgroundColor = storedColor;
        EditorGUI.indentLevel = storedIndent;
    }
    public static void ShowContextMenuForManagedReference(this SerializedProperty property)
    {
        var context = new GenericMenu();
        FillContextMenu();
        context.ShowAsContext();

        void FillContextMenu()
        {
            context.AddItem(new GUIContent("Null"), false, MakeNull);
            var realPropertyType = GetRealTypeFromTypename(property.managedReferenceFieldTypename);
            if (realPropertyType == null)
            {
                Debug.LogError("Can not get type from");
                return;
            }

            var types = TypeCache.GetTypesDerivedFrom(realPropertyType);
            foreach (var currentType in types)
            {
                AddContextMenu(currentType);
            }

            void MakeNull()
            {
                property.serializedObject.Update();
                property.managedReferenceValue = null;
                property.serializedObject.ApplyModifiedPropertiesWithoutUndo(); // undo is bugged
            }

            void AddContextMenu(Type type)
            {
                var assemblyName =  type.Assembly.ToString().Split('(', ',')[0];
                var entryName = type + "  ( " + assemblyName + " )";
                context.AddItem(new GUIContent(entryName), false, AssignNewInstanceOfType, type);
            }

            void AssignNewInstanceOfType(object typeAsObject)
            {
                var type = (Type) typeAsObject;
                var instance = Activator.CreateInstance(type);
                property.serializedObject.Update();
                property.managedReferenceValue = instance;
                property.serializedObject.ApplyModifiedPropertiesWithoutUndo(); // undo is bugged
            }
        }
    }
 
    public static Type GetRealTypeFromTypename(string stringType)
    {
        var names = GetSplitNamesFromTypename(stringType);
        var realType = Type.GetType($"{names.ClassName}, {names.AssemblyName}");
        return realType;
    }
    public static (string AssemblyName, string ClassName) GetSplitNamesFromTypename(string typename)
    {
        if (string.IsNullOrEmpty(typename))
            return ("","");
     
        var typeSplitString = typename.Split(char.Parse(" "));
        var typeClassName = typeSplitString[1];
        var typeAssemblyName = typeSplitString[0];
        return (typeAssemblyName,  typeClassName);
    }
}

#endif

Attribute

using System;
using UnityEngine;

[AttributeUsage(AttributeTargets.Field)]
public class SerializeReferenceButtonAttribute : PropertyAttribute
{
  
}

AttributeDrawer

#if UNITY_EDITOR

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(SerializeReferenceButtonAttribute))]
public class SerializeReferenceButtonAttributeDrawer : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return EditorGUI.GetPropertyHeight(property, true);
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);
        property.DrawSelectionButtonForManagedReference(position);
        EditorGUI.PropertyField(position, property, true);
        EditorGUI.EndProperty();
    }
}
#endif

Usage

using System.Collections.Generic;
using UnityEngine;

public class Test : MonoBehaviour
{
    [SerializeReference]
    [SerializeReferenceButton]
    public List<AnimalBase> ButtonAnimals = new List<AnimalBase>();
}
1 Like

I would like Unity to add a middle mouse click menu in order to initialize any field marked with [SerializeReference] with child classes. It will be invisible and useful at the same time. And it will not conflict with existing ui and right click menu.
Later that menu could be populated with copy and paste methods.
That is just one thing I think is needed in order to use serializeReference in the inspector.

Great work, I was really surprised that they add feature like this without proper inspector UI.

I will try it at home with scriptable objects. I’m waiting for polymorphic lists for ages. Now I’m doing some not so nice workarounds because I want to avoid 3rd party serialization.

So this is last part of puzzle. Well and I’m not so sure if SerializeReference is realiable enough for production usage too…

2 Likes

I have tried it yesterday with scriptable objects and it was working without problems.

I will try it today with heavier setup. But from fast look onto implementation I suppose that you doesn’t interfere with actual serialization you just add property drawer on top of it.

So actual core serialization may be working as without it.

Thanks
Peter

1 Like

This seems great!

Is there any way to make this work with interfaces too?

Do not tested this. This uses unity typechache api if it allows to get all types with interfaces than it could work.

One think that will not work definetly is scriptable objects or monobehaviours with interface ( they just are not serialized by interface with serialized reference attribute).

However it is possible to use serializable wrappers for this.
Make some base serializable class that inherits interface you want and than make serializable field of that type in another place.
Than you can create childs of that class and inside of them you can add property or field of your existing objects with interface. You can even add scriptable object filed and then return data of that object casted to your interface.

And serializable reference should be automatically populated with such wrapper types you create.

I recently started reworking my hobbyist to use scriptable objects and somewhere in that system, I have different goals that implement an IGoal interface. With your work, I can now select from these goals nicely in the editor, something I was wondering how to do. Thank you for sharing this!

1 Like

You can definitely serialize Interface assignments with SerializeReference. I do this in my VisualTemplates examples where I have a class, named EntityModel, which has an array IComponentData and another ISharedComponentData
From this I have some UI to add elements to those array using a type ahead search.

The serialization works fine in these cases, and the types and data are properly preserved through typical scenarios such as Copy/Pasta and save/close/load scene.

1 Like

@Miyavi

I updated the package to work with Interfaces.
Now it skips classes derived from UnityEngine.Object(because serializeReferecne does not work with UnityEngine.Object) and abstract classes(because they should not be instantiated).

2 Likes

I forwarded the thread to the team.

15 Likes

@TextusGames This is very awesome, thank you very much for sharing!

I tried to build a boolean expression tree, creating a data structure that allows to combine boolean values with boolean operators:

using System;
using System.Linq;

using UnityEngine;

public class Test : MonoBehaviour
{
    [SerializeReference]
    [SerializeReferenceButton]
    private BNode root;
}

public interface IValueProvider<T>
{
    T Value { get; }
}

[Serializable]
public abstract class BNode : IValueProvider<bool>
{
    public abstract bool Value { get; }
}

[Serializable]
public class ValueNode : BNode {
    [SerializeField]
    private bool value = false;

    public override bool Value => value;
}

[Serializable]
public class AndNode : BNode
{
    [SerializeField]
    private BNode[] children = default;

    public override bool Value => children.All(child => child.Value);
}

[Serializable]
public class OrNode : BNode
{
    [SerializeField]
    private BNode[] children = default;

    public override bool Value => children.Any(child => child.Value);
}

However, in the Unity inspector, the array field children when selecting the types AndNode or OrNode in the attribute dropdown are not drawn. Do you happen to know what is going on?

EDIT: Never mind, I am stupid. Using [SerializeField] is the culprit. Changing it to [SerializeReference][SerializeReferenceButton] for the array field children in both AndNode and OrNode works.

EDIT2: However, the following steps crash my editor every time:

  • In the inspector for a Test component, select the type AndNode from the dropdown of your type selection button for the field Root.

  • Expand the foldout Root, then expand the foldout Children.

  • Set the value of Size to 2.

  • Now it shows two buttons with AndNode as selected type in the Children foldout, below the Size property field.

  • Open the dropdown of your type selection button for either of them and select ValueNode as type.

  • Unity freezes for a moment and then crashes, closing itself and opening the “Report bug…” dialog.

Interestingly, after setting the array size to any value greater zero in step 3 above, the default type of the object referenced there is AndNode and not Null, as I would expect. When selecting OrNode, it selects OrNode as default type for the children, not Null.

At first I thought that using default as value for the array children was the cause, but even when setting it to null instead with private BNode[ ] children = null;, it still displays the behavior as described above.

Is there a way to force Null and enable selecting the type for nested elements?

EDIT3: After reading: [quote=“TextusGames, post:1, topic: 773303, username:TextusGames”]
Excluded abstract classes.
[/quote]
I changed my code to this, removing the abstract base class:

using System;
using System.Linq;

using UnityEngine;

public class Test : MonoBehaviour
{
    [SerializeReference]
    [SerializeReferenceButton]
    private IBooleanProvider root;
}

public interface IValueProvider<T>
{
    T Value { get; }
}

public interface IBooleanProvider : IValueProvider<bool> {}

[Serializable]
public class ValueNode : IBooleanProvider
{
    [SerializeField]
    private bool value = false;

    public bool Value => value;
}

[Serializable]
public class AndNode : IBooleanProvider
{
    [SerializeReference]
    [SerializeReferenceButton]
    private IBooleanProvider[] children = null;

    public bool Value => children.All(child => child.Value);
}

[Serializable]
public class OrNode : IBooleanProvider
{
    [SerializeReference]
    [SerializeReferenceButton]
    private IBooleanProvider[] children = null;

    public bool Value => children.Any(child => child.Value);
}

However, it does not change anything about the problem described in EDIT2. The problem might be related to nesting, since I serialize a class that contains at least one field that is also serialized using the attribute.

EDIT4: To confirm that nested elements with [SerializeReference] are supported, I wrote this:

[ExecuteInEditMode]
public class Test : MonoBehaviour
{
    [SerializeReference]
    private IBooleanProvider root;

    void OnEnable()
    {
        root = new AndNode();
        (root as AndNode).children = new []{ new ValueNode(), new ValueNode() };
    }
}

Unity does not crash at this and does show the right property fields, so presumably something goes wrong when using SerializeReferenceButtonAttribute?

You are making a recursion.
It is possibly crashing unity.

This could explain the crashing, however, it shouldn’t be making a recursion.

  • If I select AndNode as my type, its field children should be null or empty because its Size is zero.

  • If I then add elements to the array by increasing its size in the inspector, something already chooses for me that every new child element is an AndNode. Every array element should be Null. Same happens if I choose OrNode in my first step. Then adding new elements to the array makes them OrNode. Note that this, although being a recursion, does not crash Unity because Unity has some kind of recursion depth, if I recall correctly.

  • However, if I now try to change the type of an element in my array of AndNode or OrNode by using your dropdown from the [SerializeReferenceButton], then Unity crashes.

As a little addendum, it looks as if the serialized nested elements themselves are repeated. If you look on the screenshot that I attached, then the three nested elements from (A) are created by using the method I used in EDIT4 a few posts earlier, manually doing it in OnEnable().

If I now expand the foldout Children the last array element, the OrNode in (A), then it repeats the pattern, as seen in (B). This happens without me ever touching the dropdown created by SerializeReferenceButton.

I can do this again a third time (not visible in the screenshot), but the inspector eventually appears to give up, refusing to expand the foldout Element0 of such an element.

5501554--564202--component_serializereferencebutton_repeat.png

Unity has a serialisation depth limit equal to 7. It is possible that it is not properly implemented in serializeReference yet.
In general [SeralizeReference] is still buggy. May be you need to report a bug to unity about a crash, and they will investigate it. because crashes should never happen.

That looks to be the case, thanks for clarifying!

Well, I hoped that maybe you know what is going on, because the crash does appear to come from using [SerializeReferenceButton] in combination with [SerializeReference]. In EDIT4 of my previous post, I confirmed that Unity does support using nested [SerializeReference] fields or in other words, fields for types who themselves contain fields with [SerializeReference]. However, using SerializeReferenceButton something goes havoc when trying to do it. Since your code presumably creates an instance of the selected type for the property, I hoped that you might have an idea why it selects the wrong type, not defaulting to null but creating this strange repeating pattern.

Whenever I use the Reset option from the Test component context menu, I also get this error:

Maybe it is related.

I used the report feature after Unity crashes to submit the crash, linking here to the discussion. I hope that this is going to be resolved soon, as the combination of [SerializeReference] and a solution to select the type is essential for my project. If [SerializeReference] is ultimately supported in Unity and working, it is going to be a huge game changer. The main thread about the attribute highlights a lot of the fantastic thing that one can do with it.

1 Like

After digging around a bit, I documented an interesting scenario that might provide insight into what is going on. Boiled done, it seems Unity does not like your Activator.CreateInstance(type) that you use for creating an instance, @TextusGames . As the official documentation by Microsoft states, it calls the default ctor(). Adding such a constructor to my classes, I revealed an interesting detail. The constructor looks like this, for instance for AndNode:

    [SerializeReference]
    [SerializeReferenceButton]
    private IBooleanProvider[] children;

    public AndNode()
    {
        children = new IBooleanProvider[] { null };
    }

This is actually a working workaround. Well, it kind of is, but later more. Using constructors like this, Unity does not crash anymore when changing the type and thus the serialized object using [SerializedReferenceButton] and it adds a new element to the array being Null, as expected (Attachment 1).

5502676--564313--component_serializereferencebutton_attachment1.png

Increasing the array size of such a serialized object now works fine. As expected, Unity creates a clone of the serialized object of the previously last element in the array (Attachment 2). Also as expected, these are actual true references. Which means, if I tick the Value property of Element0, the Value property of Element1 is ticked, too. This is still expected, I guess? Though kinda annoying, but a different issue after all (or is it?).

5502676--564325--component_serializereferencebutton_attachment2.png

The interesting stuff happens when you decrease the array size to zero and then increase it again, looking into the inspector. Taking the setup from Attachment 3 as base, set the Size of Children to zero. If you now increase it to one again, it is no longer Null, as we would expect with any array/list that has the [SerializeField] attribute, but it is a reference to the very first property of the nested structure (?).

See Attachment 3 at the bottom of this post. This is a tree and I made sure that every element in there is “unique”, which means there is no object in there that appears elsewhere, too. What I do now is to set the Size of Root.Children[1].Children to zero, then to one again. Now look at Attachment 4. Root.Children[1].Children[0] now references all other elements starting with Root, including itself, which means we have a recursion, explaining we we can expand the foldouts as long as the inspector allows it.

To come back to the original issue, for whatever reason, when Unity serializes an array with size greater zero, it takes the reference to the very first instance of the nested structure, in this case Root. It should become Null, but for whatever weird reason it does not. This means, if I change the type from the dropdown of [SerializeReferenceButton] anywhere in this nested structure, I actually try to change the type of all of the references and Unity crashes.

To conclude, the bug appears to be located around the array serialization, when its size becomes greater zero for the first time, with Unity not making the new element Null in this particular case, but a reference to the property that the child property with [SerializeReference] is located in.

I created a strongly simplified example script that hightlights this problem:

using System;
using UnityEngine;

[ExecuteInEditMode]
public class Test2 : MonoBehaviour
{
    [SerializeReference]
    private MyClass[] children;

    private void OnEnable()
    {
        children = new MyClass[] { new MyClass() };
    }
}

[Serializable]
public class MyClass
{
    [SerializeReference]
    MyClass[] children;

    [SerializeField]
    private int changeThisNumber;

    public MyClass()
    {
        children = new MyClass[] { null };
        changeThisNumber = 0;
    }
}

Try changing the array size from one to zero to any number, then foldout, foldout and foldout, then change the value for changeThisNumber anywhere. Though remember that the problem is what Unity uses as element when changing the array size from zero to one and not that all these elements are the same instance.

5502676--564328--component_serializereferencebutton_attachment3.png

In edit4 you creating instance from code and assign it to an instance of the object. And after that the whole monobehaviour is serialized.

SerializeReferenceButtonAttribute uses PropertyDrawer and is working with serializedProperty.ManagedReference.
May be then setting serializedProperty.ManagedReference = newIClassInstance inside a recursively drawing SerializeReference fields, crash can happen.

Again I do not think that recursive serialisation is officially allowed in unity.

Without recursiveness SerializedPropertyButton is working great.

Report this example project as a bug to unity. Then let us know that they will say.

Thank you for your efforts to make serialize reference a better thing. I personally think that this new serialize feature with inspector UI will be a game changer and must be Finalized and polished.

1 Like