Home > OS >  Unity custom property drawer set `SerializeReference` to UnityEngine.Object
Unity custom property drawer set `SerializeReference` to UnityEngine.Object

Time:01-24

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.

CodePudding user response:

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

  • Related