CustomPropertyDrawer and Monobehaviours

Say I have this kind of setup:

[System.Serializable]
public class Animal
{
  public int animalValue;
}

// Display class with a list of animals
public class Animals : MonoBehaviour
{
  public List<Animal> animals;
}

// CustomPropertyDrawer for Animal class
[CustomPropertyDrawer(typeof(Animal))]
public class AnimalEditor : PropertyDrawer
{
  public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
  {
  EditorGUI.BeginProperty(position, label, property);

  EditorGUI.PropertyField(position, property, false);
  position.y += 20;
  position.height = 16;
  EditorGUI.PropertyField(position, property.FindPropertyRelative("animalValue"));

  EditorGUI.EndProperty();
  }
}

If the class Animal is not a Monobehaviour (like in this example above), and I just set List of animals to say length of 3 in inspector, I can get ā€œanimalValueā€ field with FindPropertyRelative…

However, if I were to change Animal to MonoBehaviour, add Animal as component to a empty GameObject, then drag this into Animals class list, FindPropertyRelative doesn’t work like described.

I’m pretty inexperienced with editor scripting, and it’s been 1+ year since I’ve last touched editor scripting… I did some testing, but couldn’t get any meaningful property path printout with Debug.Log from SerializedProperty property, in PropertyDrawer class…

So I’m not quite sure how and where I can get my hands to Animal items in List… maybe I’m just looking into wrong direction, maybe I should use fieldInfo or such - which I haven’t used at all.

Any pointers to articles / threads are welcome.

If you make ā€˜Animal’ a MonoBehaviour, then the list is referencing other ā€˜Animal’ scripts attached elsewhere.

See… the serialization (and therefore editor) of classes in unity do NOT respect reference. UNLESS the reference is to a unity object of some sort (MonoBehaviour, Component, GameObject, etc).

FindProperty and FindPropertyRelative are returning SerializedProperty. BUT because your property is now a unity object (a MonoBehaviour), it’s a SerializedProperty that references another object, as opposed to actually being a serialized object. So there is no relative property.

@lordofduct

Thank you for quick reply!

I vaguely suspected something like that, but definitely couldn’t put it to words… Now I’d have to just solve how to go about this… so:

If I now don’t have SerializedProperty in my use for Animal instances in List of Animals… then I gather that I have just instances of Animal class in my use?

So instead of using property.FindPropertyRelative, I now have to get values from these like from any typical class?

This seems to be working. This would place the animalValue fields value…

Animal animalRef = property.objectReferenceValue as Animal;
rect.y += 20;
rect.width = 150;
rect.height = 16;
EditorGUI.IntField(rect, animalRef.animalValue);

So this is actually bit less fluent way to get and output values to CustomPropertyDrawer, if I can’t use EditorGUI.PropertyField… unless I’m on wrong track.

That’s about the right way to do it.

If animalValue is an int, an IntField is what would be used.

Note, there is an overload of IntField that can take in a label as well.

1 Like

Generally, you don’t want to draw a different MonoBehaviour as a property - you want to change the values on that other MonoBehaviour on that other MonoBehaviour!

You can, however, draw a different MonoBehaviour’s editor in your own editor. So if you have a CustomInspector, you can do:

[CustomEditor(typeof(AnimalList))]
public class AnimalListEditor : Editor {

    private AnimalList script;
    private Dictionary<Animal, Editor> editors;

    private void OnEnable() {
        script = (AnimalList) target;
        editors = new Dictionary<Animal, Editor>();
    }


    public override void OnInspectorGUI() {
        var before = script.animals.Count;
        base.OnInspectorGUI();
        var after = script.animals.Count;

        if(before != after) {
            return; //prevent an exception due to Unity doing this in two passes
        }

        for (int i = 0; i < script.animals.Count; i++) {
            if(script.animals[i] == null) {
                EditorGUILayout.LabelField("Null animal");
            }
            else {
                var animal = script.animals[i];
                EditorGUILayout.LabelField(animal.name);
                EditorGUI.indentLevel++;
                FindEditorFor(animal).OnInspectorGUI();
                EditorGUI.indentLevel--;
            }
        }
    }

    private Editor FindEditorFor(Animal animal) {
        Editor animalEditor;
        if(!editors.TryGetValue(animal, out animalEditor)) {
            animalEditor = CreateEditor(animal);
            editors[animal] = animalEditor;
        }
        return animalEditor;
    }
}

This will look like this:

2898338--213314--Untitled.png

If you write a CustomInspector for Animal, that’ll show up instead, so you can get rid of the Script-field. You could also, of course, just write custom layout code for the animal, but this will allow you to use the same code both for Animal’s drawer and for AnimalList’s, preventing duplication.

The before != after thing up there is due to custom editors being a bit finnicky. I believe drawing is done in two passes, one to figure out the layout and one that actually creates the interactable elements. If the number of things you draw changes during the second pass, Unity throws exceptions at you. It’s generally annoying but harmless. If you return early, that doesn’t happen.

2 Likes

Thank you @Baste for your code example… was away from forums for a day.

What I actually wanted to do is avoid something like that = custom editor.

While learning editor scripting, I was sort of liking the idea of PropertyDrawers, meaning no need for specific editors which are quite a lot of work for beginner level coder - IMHO.

Where there are instances of property, it’s rendered like it should, no matter if it’s in some custom editor, or ā€œnormalā€ Array / List. However, this concept doesn’t seem to work like I ā€œwantedā€.

Using this code:

  EditorGUI.BeginProperty(position, label, property);
  EditorGUI.PropertyField(position, property, true);
  EditorGUI.EndProperty();

So when non MonoBehaviour classes are rendered using CustomPropertyDrawer, it looks like image below, when the third parameter is set to true - it automatically renders fields of class Animal like this:

https://dl.dropboxusercontent.com/u/17399934/Unity_misc/20161228_Unity_customPropertyDrawer_class.png

So I could do some formatting, then print out the fields as they are, if needed.

However with MonoBehaviour classes, the case is different, most likely because of reasons @lordofduct mentioned. The result is this with same lines of code:

https://dl.dropboxusercontent.com/u/17399934/Unity_misc/20161228_Unity_customPropertyDrawer_MB.png

And like the image shows, I only get the field for MonoBehaviour that is attached to some empty GameObject that I then dragged to slot/field in list… no fields of Animal class like with standard C# classes.

So I actually just thought this would be nice and easy way to combine drag and dropable GameObjects and CustomPropertyDrawers…

If anyone else finds this thread and for future self… I post this here too.

I found another thread about this question:

In addition to what @lordofduct and @Baste said, this post shows how to create editor for Monobehaviour class instead of creating CustomPropertyDrawer, how to use it was already covered in reply by @Baste .