Serializing types in Unity
In this post, I went over the inventory and item system, and mentioned that unity can’t actually serialize types.
In this blog post, I will go over a solution that I found which lets me serialize a Type field like any other, with a nice dropdown.
Go to final code#
Generic TypeReference class#
The system works by having a wrapper class, which I’ll call SerializedType<T>
. It takes one generic argument; a type that the value must derive from. For example in my case, if T is Item
, you can assign it things like Weapon
or Potion
, but not Player
.
// NOT FINAL CODE!
[System.Serializable]
public class SerializedType<T>
{
[SerializeField] string fullTypeName = typeof(T).Name;
public static implicit operator Type(SerializedType<T> t) => Type.GetType(t.fullTypeName);
public static implicit operator SerializedType<T>(Type t)
{
SerializedType<T> s = new();
s.fullTypeName = t.FullName;
return s;
}
}
The class stores the type as a string containing its fullName.
I’ve defined two implicit conversion operators; from Type
to SerializedType
, and vice versa, which means you can simply replace any field of Type
with it in order to show it in the inspector.
I added caching so that Type.GetType()
doesnt get called every time you access this as a Type, which looks like this:
[System.Serializable]
public class SerializedType<T>
{
[SerializeField] string fullTypeName = typeof(T).Name;
string cachedFullTypeName;
Type cachedType;
public static implicit operator Type(SerializedType<T> t)
{
if (t.cachedFullTypeName != t.fullTypeName)
{
t.cachedFullTypeName = t.fullTypeName;
t.cachedType = Type.GetType(t.cachedFullTypeName);
}
return t.cachedType;
}
public static implicit operator SerializedType<T>(Type t)
{
SerializedType<T> s = new();
s.fullTypeName = t.FullName;
return s;
}
}
Custom Property Drawer#
Now that the logic of the class is complete, it’s time to add a Property drawer so that it can be drawn nicely in the inspector.
[CustomPropertyDrawer(typeof(SerializedType<>))]
public class SerializedTypeDrawer : PropertyDrawer
{
IEnumerable GetInheritedClasses(Type baseType)
{
return Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t));
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// get generic type
Type t = fieldInfo.FieldType;
if (t.IsGenericType)
{
t = t.GenericTypeArguments[0];
}
SerializedProperty prop = property.FindPropertyRelative("fullTypeName");
string typeName = prop.stringValue;
// dropdown button rect
Rect dropdownRect = position;
dropdownRect.x += EditorGUIUtility.labelWidth + 2;
dropdownRect.width -= EditorGUIUtility.labelWidth + 2;
dropdownRect.height = EditorGUIUtility.singleLineHeight;
Type currentType = Type.GetType(typeName);
EditorGUI.PrefixLabel(position, label);
if (EditorGUI.DropdownButton(label.text != "" ? dropdownRect : position, new(currentType?.Name ?? "Unknown Type"), FocusType.Keyboard))
{
GenericMenu menu = new GenericMenu();
// inherited types
foreach (Type type in GetInheritedClasses(t))
{
menu.AddItem(new GUIContent(type.Name), typeName == type.FullName, () =>
{
prop.stringValue = type.FullName;
property.serializedObject.ApplyModifiedProperties();
});
}
menu.ShowAsContext();
}
}
}
I won’t explain this fully, but essentially the OnGUI
method finds all classes that inherit from the generic parameter and puts them into a dropdown menu.
Final code#
using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
[System.Serializable]
public class SerializedType<T>
{
[SerializeField] string fullTypeName = typeof(T).Name;
string cachedFullTypeName;
Type cachedType;
public static implicit operator Type(SerializedType<T> t)
{
if (t.cachedFullTypeName != t.fullTypeName)
{
t.cachedFullTypeName = t.fullTypeName;
t.cachedType = Type.GetType(t.cachedFullTypeName);
}
return t.cachedType;
}
public static implicit operator SerializedType<T>(Type t)
{
SerializedType<T> s = new();
s.fullTypeName = t.FullName;
return s;
}
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(SerializedType<>))]
public class SerializedTypeDrawer : PropertyDrawer
{
IEnumerable GetInheritedClasses(Type baseType)
{
return Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t));
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// get generic type
Type t = fieldInfo.FieldType;
if (t.IsGenericType)
{
t = t.GenericTypeArguments[0];
}
SerializedProperty prop = property.FindPropertyRelative("fullTypeName");
string typeName = prop.stringValue;
// dropdown button rect
Rect dropdownRect = position;
dropdownRect.x += EditorGUIUtility.labelWidth + 2;
dropdownRect.width -= EditorGUIUtility.labelWidth + 2;
dropdownRect.height = EditorGUIUtility.singleLineHeight;
Type currentType = Type.GetType(typeName);
EditorGUI.PrefixLabel(position, label);
if (EditorGUI.DropdownButton(label.text != "" ? dropdownRect : position, new(currentType?.Name ?? "wtf"), FocusType.Keyboard))
{
GenericMenu menu = new GenericMenu();
// inherited types
foreach (Type type in GetInheritedClasses(t))
{
menu.AddItem(new GUIContent(type.Name), typeName == type.FullName, () =>
{
prop.stringValue = type.FullName;
property.serializedObject.ApplyModifiedProperties();
});
}
menu.ShowAsContext();
}
}
}
#endif
Usage#
To use this code, in a place where you might have
public Type weaponType;
simply replace it with
public SerializedType<Weapon> weaponType;
with the generic parameter (in this case Weapon
) being a base class the type must derive from. Use System.Object
for any type.