Table of Contents Show
Recently I worked on a color picker package. I used different components for selecting color values, e.g. the default slider from Unity and my custom radial slider. Of course, a simple input field would have worked as well.
For a color picker, I combined 3 or 4 of these basic components. Sounds simple – just use a common interface for the components and use any combination of different components.
Unfortunately, Unity does not support interfaces in the inspector. That makes very much sense for serialization because serialization needs to know the actual data of an object. But in my case, I only wanted to serialize a reference to another object.
While it does not work out-of-the-box, there is a way to support interfaces in the Unity inspector.
Implementation
The implementation consists of three parts:
- the Interface which I like to be referenced
- a PropertyAttribute to decorate the property
- the PropertyDrawer for the custom editor functionality
Interface
There is nothing special about the interface. In my case, it has a public property to set or get a value.
public interface ISingleInput
{
float Value { get; set; }
}
Attribute
The attribute defines a specific type for a property. The attribute itself is not doing anything. It just marks a property with the required type. The enforcement itself is done with the property drawer.
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class InterfaceAttribute : PropertyAttribute
{
public Type RequiredType { get; private set; }
public InterfaceAttribute(Type type)
{
RequiredType = type;
}
}
PropertyDrawer
A property drawer defines how a script variable is visualized in the Unity inspector. It is applied to every property which is decorated with the InterfaceAttribute.
[CustomPropertyDrawer(typeof(InterfaceAttribute))]
public class InterfacePropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// code is inserted here.
}
}
First, I make sure that the property is valid for assigning an interface. It can’t be applied to multiple objects at the same time and it has to be an ObjectReference.
if (property.serializedObject.isEditingMultipleObjects) return;
if (property.propertyType != SerializedPropertyType.ObjectReference) return;
Then I extract the required type from the attribute.
var interfaceAttribute = this.attribute as InterfaceAttribute;
var requiredType = interfaceAttribute.RequiredType;
Object references are treated differently on different Unity versions. Sometimes the component is directly referenced and sometimes the game object is referenced. Therefore it’s necessary to check for different types.
EditorGUI.BeginProperty(position, label, property);
var reference = EditorGUI.ObjectField(position, label, property.objectReferenceValue, typeof(UnityEngine.Object), true);
if (reference is GameObject go)
{
reference = go.GetComponent(requiredType);
}
property.objectReferenceValue = reference;
EditorGUI.EndProperty();
Example usage
I serialize the reference as a UnityEngine.Object. This includes MonoBehaviours, GameObjects, and many other Unity-specific classes.
Unfortunately, it’s necessary to type it as Object. I add another member with the interface type. I assign it in the Awake method and use it throughout the whole script. The serialized object is really just used for referencing another object in the editor.
[SerializeField, Interface(typeof(ISingleInput))]
private UnityEngine.Object m_hueInputObject = null;
private ISingleInput m_hueInput;
void Awake()
{
m_hueInput = (ISingleInput)m_hueInputObject.
}
It’s also possible to use SerializeReference instead of SerializeField. It’s not necessary though, because UnityEngine.Object is always serialized as a reference.
Conclusion
The solution is far from perfect. The editor shows the field as Object instead of the required interface. If a GameObject has multiple components with the required interface, only the first one is chosen.
Most of these issues can be handled by a more complex editor script. But for internal usage, it’s good enough for me. Most importantly, it allows me to set up everything with interfaces. Making the inspector nicer is always possible in the future.