Search code examples
c#unity-game-engineunity-editor

Unity custom property drawer set `SerializeReference` to UnityEngine.Object


I am creating a custom drawer for a custom attribute that helps me initialize the value of a field marked with the attribute SerializeReference.

Basically, the custom drawer will show a dropdown menu that allows me to select the Type to create and assign to the field.

I have the following code so far, to test the different scenarios:

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
    var targetObject = property.serializedObject.targetObject;
    var field = targetObject.GetType().GetField(property.name, FieldBindingFlags)!;

    if (field.GetValue(targetObject) == null)
        field.SetValue(targetObject, new OperaSinger());
    else
    {
        EditorGUI.BeginProperty(position, label, property);
        EditorGUI.PropertyField(position, property, label, true);
        EditorGUI.EndProperty();
    }
}

And those are the test classes:

interface ISinger
{
    void Sing();
}

[System.Serializable]
class OperaSinger : ISinger
{
    [SerializeField] private string name;
    void Sing(){}
}

class StreetPerformer : MonoBehaviour, ISinger
{
    [SerializeField] private string streetName;
    void Sing(){}
}

The above code seems to work fine, it initializes the property to a new instance of the OperaSinger and shows the editor.

But, when I try to do the same with the MonoBehaviour implementation, I get an error:

Attempt #1:

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
    var targetObject = property.serializedObject.targetObject;
    var field = targetObject.GetType().GetField(property.name, FieldBindingFlags)!;

    if (field.GetValue(targetObject) == null)
    {
        var x = ((Component)targetObject).gameObject.AddComponent<StreetPerformer>();
        field.SetValue(targetObject, x);
    }
    else
    {
        EditorGUI.BeginProperty(position, label, property);
        EditorGUI.PropertyField(position, property, label, true);
        EditorGUI.EndProperty();
    }
}

Error: [SerializeReference] cannot serialize objects that derive from Unity.Object.


Attempt #2:

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
    var targetObject = property.serializedObject.targetObject;
    var field = targetObject.GetType().GetField(property.name, FieldBindingFlags)!;

    if (field.GetValue(targetObject) == null)
    {
        var x = ((Component)targetObject).gameObject.AddComponent<StreetPerformer>();
        property.objectReferenceValue = x;
    }
    else
    {
        EditorGUI.BeginProperty(position, label, property);
        EditorGUI.PropertyField(position, property, label, true);
        EditorGUI.EndProperty();
    }
}

Error: type is not a supported pptr value


Attempt #3:

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
    var targetObject = property.serializedObject.targetObject;
    var field = targetObject.GetType().GetField(property.name, FieldBindingFlags)!;

    if (field.GetValue(targetObject) == null)
    {
        var x = ((Component)targetObject).gameObject.AddComponent<StreetPerformer>();
        property.objectReferenceInstanceIDValue = x.GetInstanceID();
    }
    else
    {
        EditorGUI.BeginProperty(position, label, property);
        EditorGUI.PropertyField(position, property, label, true);
        EditorGUI.EndProperty();
    }
}

Error: type is not a supported pptr value


What am I doing wrong, and how can I fix it?

As far as I understand, the error from attempts 2 and 3 is because the field is defined as the interface type ISinger.


Solution

  • Well, after trying the following:

    [SerializeReference] private ISinger singer;
    
    private void Reset()
    {
        singer = gameObject.AddComponent<StreetPerformer>()
    }
    

    And getting the same error as attempt 1, I realized that it is not possible to do it.

    So, the solution I came up with is creating a wrapper-class to the StreetPerformer class that will delegate all methods to it like so:

    internal sealed class StreetPerformer_ISing_Wrapper : ISing
    {
        [SerializeField] private StreetPerformer _instance;
    
        public void Sing()
        {
            _instance.Sing();
        }
    
    }
    

    Obviously, doing so to every class that I need will be tedious so I simply wrote a code generator that creates (and updates) this class for me.

    So now I can have an interface field in my script that references a regular class, a ScriptableObject or a MonoBehaviour with minimum effort 😁