Draw Inspector For Most Derived Type Of Collection Elements

Alright, that title is a bit of a mouthful, but that concept is pretty straightforward. I’m working on a short project that is essentially a quiz game. The player is presented with a question and must choose the correct answer. There are different types of questions, such as multiple choice, and true/false.

public class Question { }
public class MultipleChoiceQuestion : Question { }
public class TrueFalseQuestion      : Question { }

I have a base Question class, and a few derived classes MultipleChoiceQuestion and TrueFalseQuestion. For a given quiz, I have a ScriptableObject with a list of the questions.

public class QuizData : ScriptableObject
{
	public Question[] questions;
}

Using a custom PropertyDrawer for QuizData I can add new Questions of any type to the array.

But here’s my problem: the Inspector always draws for the base class Question, not for the derived type. So, even if element 1 in the array is MultipleChoiceQuestion, only the fields in Question are shown.

How can I get Unity to draw the Inspector for the most derived type when showing an element in an array?

Alternatively, if I have a SerializedProperty for a Question, but I know the underlying type is MultipleChoiceQuestion, how can I call the Editor for the MultipleChoiceQuestion?

EDIT: Going by “General Array Serialization” in http://forum.unity3d.com/threads/155352-Serialization-Best-Practices-Megapost, the correct method seems to be for Question to derive from ScriptableObject. Indeed, this get Unity serialize everything properly, survive assembly reloads, etc, but the Inspector is still a problem.

By default, the Inspector shows the elements of the array as ‘object reference field’. It doesn’t fold out and show all the fields within the Question (in fact, it just says ‘type mismatch’ even for the base class Question). I’m trying to get it to show the actual fields for each item.

I managed to solve this is a satisfactory manner.

Create a QuizData ScriptableObject:

[Serializable] public class QuizData : ScriptableObject
{
	public string quizText;
	public List<Question> questions = new List<Question>();
}

Create a Manager to hold an instance of QuizData:

public class Manager : MonoBehaviour
{
	public QuizData quizData;
}

And an Editor to create an instance:

[CustomEditor(typeof(Manager))]
public class ManangerEditor : Editor
{
	private void OnEnable ()
	{
		Manager target = (Manager) base.target;

		if ( target.quizData == null )
		{
			string projectRelativeFilePath = "Assets/" + typeof(QuizData).Name + ".asset";

			//Try to load existing asset.
			QuizData asset = (QuizData) AssetDatabase.LoadAssetAtPath(projectRelativeFilePath, typeof(QuizData));

			//If none exists, create a new one.
			if ( asset == null )
			{
				asset = ScriptableObject.CreateInstance<QuizData>();
				AssetDatabase.CreateAsset(asset, projectRelativeFilePath);
			}

			target.quizData = asset;
		}
	}
}

Create the question types:

[Serializable] public class Question : ScriptableObject
{
	[Range(0, 100)]
	[SerializeField] protected int m_IntField;
}

[Serializable] public class MultipleChoiceQuestion : Question
{
	[SerializeField] private float m_FloatField;
}

And finally, the meat of it. Create a custom Editor for QuizData:

[CustomEditor(typeof(QuizData))]
public class QuizDataEditor : Editor
{
	public override void OnInspectorGUI ()
	{
		serializedObject.Update();

		//QuizData GUI
		DrawPropertiesExcluding(serializedObject, "questions");
		serializedObject.ApplyModifiedProperties();

		//Question GUI
		arrayProp = serializedObject.FindProperty("questions");
		EditorGUILayout.LabelField("Questions: " + arrayProp.arraySize);

		for ( int i = 0; i < arrayProp.arraySize; ++i )
		{
			EditorGUILayout.Space();

			SerializedProperty itemProp = arrayProp.GetArrayElementAtIndex(i);
			Question item = (Question) itemProp.objectReferenceValue;

			string label = "Question " + (i+1) + " (" + item.GetType() + ")";
			itemProp.isExpanded = GUILayout.Toggle(itemProp.isExpanded, label, EditorStyles.foldout);

			if ( itemProp.isExpanded )
			{
				Editor drawer = Editor.CreateEditor(item);

				EditorGUI.indentLevel += 2;

				drawer.OnInspectorGUI();

				if ( DrawDeleteButton(itemProp, i) )
				{
					if ( --i > 0 ) continue;
					else return;
				}

				EditorGUI.indentLevel -= 2;
				EditorGUILayout.Space();
			}
		}
	}
}

The main idea here is to draw the normal Inspector for QuizData except for the list of Questions. Then, loop over the Questions, create an Editor for each one, and call the created Editor’s OnInspectorGUI. These objects may not need to be ScriptableObjects, I did that because I’m saving all this data to an .asset file.

The above doesn’t account for being able to edit the collection (i.e. add and remove items). You can either display the arraySize property of the array or write a full custom array Editor.

For a more complete example, with extra goodies, I uploaded my test project to Github.

As far as I am aware, there is no good way to do what you’re trying to do. Class is a reference type, which is why Unity displays the field as an object reference. You can make it a struct, but then it can’t derive from Serializable Object. It sucks. I’m actually grappling with a very similar problem right now, and here is my solution, implemented in terms of your problem:

struct Question {
    enum QuestionType {
        TrueFalse,
        MultipleChoice,
    };

    QuestionType questionType;
    string questionText;
    bool trueFalseAnswer;
    List<string> multipleChoiceText;
    int multipleChoiceAnswer;
}

And then of course you chose which fields to display in the UI based on the value of questionType.

Take a look into Jacob Dufault’s FullInspector. Best 25$ I’ve ever invested. No more problem with serialization.