Dynamic property conversion in custom property drawers.

Ciao !! :slight_smile:
Not entirely sure that what I’m trying to achieve is actually possible.
Are you ready? Let’s start the madness !!! :stuck_out_tongue:

I have a ScriptableObject that can execute a list of behaviours.
This behaviours are also ScriptableObjects.
Each behaviour type needs parameters to work properly.
Normally, each behaviour type would have serialized fields that can be changed in each instance BUT I don’t want this: each behaviour should just implement an algorithm and I want to inject the necessary data from the ScriptableObject used as “container”.

The critical part is that the container shouldn’t care about the final type of the behaviour: the proper parameters should be displayed and assigned using a custom drawer for a tuple class that defines a behaviour coupled to its data.

OK, the last sentence sounds really complicated… let me create some example classes.

Container class: ScriptableObject that triggers the behaviours.

using UnityEngine;

public class Container : ScriptableObject
{
    [SerializeField]
    private Tuple[] _behaviours = null;

    private void OnEnable()
    {
        for (int i = 0; i < _behaviours.Length; i++)
        {
            _behaviours[i].Behaviour.DoStuff(_behaviours[i].BehaviourData);
        }
    }
}

Behaviour class: common abstract base class for behaviours.

using System;
using UnityEngine;

[Serializable]
public abstract class Behaviour : ScriptableObject
{
    [Serializable]
    public abstract class Data { }

    public abstract void DoStuff(Data d);
}

Tuple class: defines a tuple with a Behaviour and its necessary Data.

using System;
using UnityEngine;

[Serializable]
public class Tuple
{
    [SerializeField]
    private Behaviour _behaviour = null;
    [SerializeField]
    private Behaviour.Data _data = null;

    public Behaviour Behaviour { get { return _behaviour; } }
    public Behaviour.Data BehaviourData { get { return _data; } }
}

Concrete Behaviour classes: the following defines a set of instruction to perform.

using UnityEngine;

public class BahaviourString : Behaviour
{
    public class DataInternal : Data
    {
        public string message = null;
    }

    public override void DoStuff(Data d)
    {
        Debug.Log((d as DataInternal).message);
    }
}
using UnityEngine;

public class BahaviourInt : Behaviour
{
    public class DataInternal : Data
    {
        public int number = 0;
    }

    public override void DoStuff(Data d)
    {
        Debug.Log((d as DataInternal).number);
    }
}
using UnityEngine;

public class BahaviourComplex : Behaviour
{
    public class DataInternal : Data
    {
        public string message = null;
        public int number = 0;
    }

    public override void DoStuff(Data d)
    {
        DataInternal ciao = d as DataInternal;
        Debug.Log(ciao.message + " | " + ciao.number);
    }
}

I don’t know if it’s clear what I want to achieve: a class that can run a series of actions that are composite from the inspector using ScriptableObjects.

What I’m trying to do and I’m not able to, is to create a custom drawer for the Tuple class that does something like:

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(Tuple), true)]
public class MyCustomDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);
        {
            SerializedProperty behaviourProperty = property.FindPropertyRelative("_behaviour");
            var behaviour = behaviourProperty.objectReferenceValue;
            SerializedProperty dataProperty = property.FindPropertyRelative("_data");
            EditorGUI.PropertyField(position, behaviourProperty, true);
            if (behaviour == null)
            {
                EditorGUI.LabelField(position, "Please, add a Behaviour to access its configuration.");
            }
            else
            {
                // TODO - make dataProperty to point to an object of type behaviour.GetType().GetNestedType("DataInternal")
                // OR    - get/create a serialized property that point to an object of type behaviour.GetType().GetNestedType("DataInternal")
                // OR    - do something so that
                EditorGUI.PropertyField(position, dataProperty, true);
                //        - will show in the inspector the fields needed for whatever the assigned Behaviour is.
                // TODO - make sure the Data is properly serialized and saved as Data field of the related Tuple.
            }
        }
        EditorGUI.EndProperty();
    }
}

I am out of my mind, am I not?
Maybe there can be a different and less complicated way to do it, but this is the best I was able to come out with.

Please remember: I want the Data needed for each behaviour to be saved in the Container via the Tuple defined there because I don’t want to create 50 different, for example, BehaviourString ScriptableObject instances to print 50 different strings. The behaviour should just be treated as a set of instruction to apply to the passed data. Does it make sense?

Please note: the part that I cannot do is described by the comments of the custom drawer class.
Please note: the shown code will compile and would potentially run but I did cut all the formatting part, therefore, the custom drawer may not show the tuple object correctly.

Many many thanks to all the people that will contribute to sort out this mess !! :smile:

UPDATE !!!

I changed the custom drawer as such:

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(Tuple), true)]
public class MyCustomDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);
        {
            SerializedProperty behaviourProperty = property.FindPropertyRelative("_behaviour");
            var behaviour = behaviourProperty.objectReferenceValue;
            SerializedProperty dataProperty = property.FindPropertyRelative("_data");
            EditorGUI.PropertyField(position, behaviourProperty, true);
            if (behaviour == null)
            {
                EditorGUI.LabelField(position, "Please, add a Behaviour to access its configuration.");
            }
            else
            {
                /* This portion of code allows me to:
                 *   - get the object pointed by the SerializedProperty
                 *   - check if the object is of the wanted type
                 *   - get an object of the wanted type
                 *   - set the converted object back to the SerializedProperty
                 *
                 * Please note: the GetObjectOfProperty and SetObjectOfProperty have been written by reading
                 *        https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBaseEditor/EditorHelper.cs
                */
                Type internalDataType = behaviour.GetType().GetNestedType("DataInternal");
                var dataInstance = UnityEditorHelper.GetObjectOfProperty(dataProperty);
                if (!dataInstance.GetType().IsInstanceOfType(internalDataType))
                {
                    dataInstance = Activator.CreateInstance(internalDataType);
                }
                UnityEditorHelper.SetObjectOfProperty(dataProperty, dataInstance);
                EditorGUI.PropertyField(position, dataProperty, true);
            }
        }
        EditorGUI.EndProperty();
    }
}

you can read what the changes are about from the comments.

IT STILL DOESN’T WORK !!
I don’t understand if it’s because the object assignment doesn’t work properly of if it is because the original serialized type is different, therefore, when reading the field the type assigned is lost.
I did debug the code through VisualStudio and

if (!dataInstance.GetType().IsInstanceOfType(internalDataType))
{
    dataInstance = Activator.CreateInstance(internalDataType);
}

is always executed every OnGUI…

Well, at least I’m still trying :stuck_out_tongue:

Another update.
I suppose what I’m trying to achieve is not possible: I can successfully change the object pointed to the property but I cannot save it. I think the property, when the new object is set, has to be somehow updated so to refresh the content to show in the inspector.
I also tried to manually iterate though it, but I’m getting null refs probably due to the fact that I’m changing the object but not refreshing the property with the new content.

If anyone has any input on this, it would be very very much appreciated :slight_smile:

Thanks

I haven’t looked into your custom drawer too much, but this was indeed impossible to achieve in the past (without re-writing the serializer). Serializing a Dog or a Cat into an Animal field would always truncate the object back to an Animal upon deserialization.

If you’re on Unity 2019.4 or above however, you might be able to use [SerializeReference]. I haven’t used it yet myself, but I think it should make your use case possible.

This instructs Unity to serialize your object as a reference, thus allowing child class saving.

That worked !!! :slight_smile:
The change I made was in the Tuple class:

using System;
using UnityEngine;

[Serializable]
public class Tuple
{
    [SerializeField]
    private Behaviour _behaviour = null;
    [SerializeReference]
    private Behaviour.Data _data = null;

    public Behaviour Behaviour { get { return _behaviour; } }
    public Behaviour.Data BehaviourData { get { return _data; } }
}

There is still the need for the custom drawer because Unity will not automatically convert the serialized object pointed by the property.

The issue, as per now, is that the changes made in the inspector to the InternalData are not saved !!! :smile:
I’m currently dealing with this now :stuck_out_tongue:

1 Like

Done !!! :slight_smile:
At the end it was

if (!dataInstance.GetType().IsInstanceOfType(internalDataType))

that was faulty. I changed it to

if (!dataInstance.GetType().IsAssignableFrom(internalDataType))

If you are curios of the final result, please, take a look to the sample project I created :wink:

Thank you very much for your help msfredb7, much appreciated :slight_smile:

6168353–674855–DynamicSerializedPropertyObjectReference.7z (15 KB)

Happy to help :slight_smile: