- Home /
How do I expose a list of interfaces in the inspector?
This shouldn't be that hard. After looking through many post of often outdated and/or opinionated discussions about the topic of exposing interfaces in the inspector, I haven't gotten anywhere...
I am looking to have a list of interfaces and be able to assign objects to the list from the inspector. I am open to using a custom editor to solve this. Is this possible or should I just be creating a list of GameObjects and checking each one implements the interface?
The code so far:
public class InstructionSet : MonoBehaviour
{
public List<ICompletableInstruction> Instructions;
public bool IsComplete
{
get
{
return Instructions.All(i => i.IsComplete == true);
}
}
}
public interface ICompletableInstruction
{
bool IsComplete { get; }
bool CheckCompletion();
}
Thanks for any help.
Answer by Xarbrough · Mar 13, 2018 at 12:44 PM
I'll try to give a high-level recap:
Interfaces are currently not serializable by Unity and therefore don't show in the inspector. It is not likely for Unity to add support soon (maybe ever).
In many cases, you can use a abstract MonoBehaviour base class to essentially provide the same functionality, unless you require a class to implement several interfaces (more than one base class).
Unity's GetComponent calls find Interfaces, so it's a valid strategy to only serialize references to GameObjects und check for the existence of a specific interface, e.g. an IDamageable interface which is queried OnTriggerEnter.
Interfaces do not have any connection to the object they are implemented on, this is way Unity doesn't serialize them. We can create this connection ourselves, as mentioned, by serializing the reference to an actual object, that Unity can handle (everything that inherits from UnityEngine.Object, mainly GameObject, MonoBehaviour and ScriptableObject) and then checking if the interface is still implemented.
There are multiple open-source and commercial solutions online. I can't recommend any, but I assume they all work very similarly. For plain C# classes you could also think about using a reflection-based approach by storing a string representation of the class and interface and then using the Activator class from the .NET framework to create instances when you need them. Existing solutions probably use one of the two approaches. The main differences come in how the editor/inspector is designed. In any case, interfaces are not actually serialized (even though some products claim), instead, other related data like GameObjects and strings are serialized, but the custom editor provides a mechanism to prevent user error when assigning values, etc.
You can write a simple usable version by creating your own ObjectField which checks for the presence of an interface on the object before allowing to save it.
Here is a very rough example:
using UnityEditor;
using UnityEngine;
public class StoreInterfaceReference : MonoBehaviour, MyInterface
{
[TypeConstraint(typeof(MyInterface))]
public GameObject myReference;
public void Do()
{
}
}
public interface MyInterface
{
void Do();
}
public class TypeConstraintAttribute : PropertyAttribute
{
private System.Type type;
public TypeConstraintAttribute(System.Type type)
{
this.type = type;
}
public System.Type Type
{
get { return type; }
}
}
[CustomPropertyDrawer(typeof(TypeConstraintAttribute))]
public class TypeConstraintDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.ObjectReference)
{
// Show error
// Also check that the user only uses the attribute on a GameObject or Component
// because we need to call GetComponent
}
var constraint = attribute as TypeConstraintAttribute;
if (DragAndDrop.objectReferences.Length > 0)
{
var draggedObject = DragAndDrop.objectReferences[0] as GameObject;
// Prevent dragging of an object that doesn't contain the interface type.
if (draggedObject == null || (draggedObject != null && draggedObject.GetComponent(constraint.Type) == null))
DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
}
// If a value was set through other means (e.g. ObjectPicker)
if(property.objectReferenceValue != null)
{
// Check if the interface is present.
GameObject go = property.objectReferenceValue as GameObject;
if (go != null && go.GetComponent(constraint.Type) == null)
{
// Clean out invalid references.
property.objectReferenceValue = null;
}
}
property.objectReferenceValue = EditorGUI.ObjectField(position, label, property.objectReferenceValue, typeof(GameObject), true);
}
}
You can see, it's not too difficult to create a convenient system which let's designers only drag interface references to GameObject fields, but it can get quite complex, if you want the system to work in every situation, there are quite a few edge-cases in my experience.
Additionally, instead of using a GameObject and GetComponent, you can also create a wrapper type and custom editor to automatically call GetComponent and maybe cache it for you.
Answer by JasonC_ · Oct 27, 2019 at 11:37 PM
So, I was trying to do the same thing and found this post, and Xarbrough's answer (thanks) made it clear it's not possible.
So for anybody else trying something similar, I was able to come up with at least a convenient approach to the strategy of maintaining a list of generic items and checking if they implement the interface later.
For example, if you have the OP's example interface:
public interface ICompletableInstruction {
bool IsComplete { get; }
bool CheckCompletion ();
void DoSomething (); // I added this for an example.
}
You can create a generic list (the challenge is choosing an appropriate base type, that's your call):
public List<Component> Instructions;
And then you can do this if you want to e.g. call DoSomething
on every member of the list:
Instructions.ForEach(c => (c as ICompletableInstruction)?.DoSomething());
And that will do the interface check on the fly and skip over items that don't implement that interface or are just null
. See C# null-conditional operator for more info.
For the equivalent of the OP's example, you can use the C# null-coalescing operator:
return Instructions.All(o => ((o as ICompletableInstruction)?.IsComplete ?? true) == true);
Where you can choose ?? true
if you want incompatible objects to evaluate to true there, or ?? false
if you want them to be false, your call.
This is my first post here and gosh darn is the formatting on this forum busted or what... they really ought to space out double line-breaks better...
Answer by dh2000 · Jan 23, 2020 at 10:23 AM
A small improvement to Xarbrough's solution. OnGUI must also check whether mouse is over its object field, otherwise it will break assigning references to other fields.
[CustomPropertyDrawer(typeof(TypeConstraintAttribute))]
public class TypeConstraintDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.ObjectReference)
{
// Also check that the user only uses the attribute on a GameObject or Component
// because we need to call GetComponent
Debug.LogError(string.Format("{0} - {1}: This drawer must be used only on Object types",
property.serializedObject.targetObject.GetType().ToString(), property.displayName));
return;
}
var constraint = attribute as TypeConstraintAttribute;
Event evt = Event.current;
if (DragAndDrop.objectReferences.Length > 0 && position.Contains(evt.mousePosition))
{
var draggedObject = DragAndDrop.objectReferences[0] as GameObject;
// Prevent dragging of an object that doesn't contain the interface type.
if (draggedObject == null || (draggedObject != null && draggedObject.GetComponent(constraint.Type) == null))
{
DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
if (evt.type == EventType.DragExited)
Debug.LogError(string.Format("Object assigned to '{0}' must implement interface '{1}'", property.name, constraint.Type));
}
}
property.objectReferenceValue = EditorGUI.ObjectField(position, label, property.objectReferenceValue, typeof(GameObject), true);
//If a value was set through other means(e.g.ObjectPicker)
if (property.objectReferenceValue != null)
{
// Check if the interface is present.
Component go = property.objectReferenceValue as Component;
if (go != null && go.GetComponent(constraint.Type) == null)
{
// Clean out invalid references.
property.objectReferenceValue = null;
Debug.LogError(string.Format("Object assigned to '{0}' must implement interface '{1}'", property.name, constraint.Type));
}
}
}
}
Answer by danny_unity495 · Feb 23, 2020 at 03:55 PM
Here’s a working solution using the above method on GitHub. Includes Demo vid and example. Super lightweight- just add 2 files to your project.
Your answer
![](https://koobas.hobune.stream/wayback/20220612154509im_/https://answers.unity.com/themes/thub/images/avi.jpg)
Follow this Question
Related Questions
Can not you change the default GUI style of EditorGUI in OnInspectorGUI? 1 Answer
Custom Display for system.object Editor 1 Answer
How can i get SerializedProperty from UnityEvent which in List. Sorry for my Eng. 2 Answers
Is it possible to choose which custom editor load for specific script? 0 Answers