10
\$\begingroup\$

I've written some code that allows Unity3D's inspector to display fields that conform to an interface. Unity has some quirks about their inspector, so as a preface they are listed here:

  • If you add a [Serializable] attribute to a class, Unity's Inspector will attempt to show all public fields inside that class.
  • Any class extending Monobehaviour automatically has the [Serializable] attribute
  • Unity's inspector will attempt to display any private field with the [SerializeField] attribute.
  • Unity's inspector will not attempt to display generic types or interfaces, with the exception of List<T>, which is hard-coded.
  • Unity's inspector will not attempt to display properties. A common workaround is to have a private backing field for your property with [SerializeField] attached. Setters won't be called on the value set in the inspector. It's typically only set pre-compilation time, although a developer can modify values in the inspector during runtime. Currently it is acceptable to me to only take the initial value, although if anybody has a simple and efficient way to update successfully, I'd be happy to hear it.
  • Unity has a PropertyDrawer class you can extend to control how a type is displayed in the inspector. The PropertyDrawer for an interface or generic type will be ignored.

The code

UnityInterfaceHelper.cs

[Serializable]
public class UnityInterfaceHelperBase
{
    [Tooltip("The component that is of the type required.")]
    [SerializeField]
    public Component target;
}

[Serializable]
public class UnityInterfaceHelper<TInterface> where TInterface : class
{
    public TInterface TargetAsInterface
    {
        get
        {
            if (targetAsInterface == null)
            {
                targetAsInterface = target as TInterface;
            }
            return targetAsInterface;
        }

        set
        {
            if (targetAsInterface != value)
            {
                targetAsInterface = value;
                if (value as Component != null)
                {
                    target = targetAsInterface as Component;
                }
            }
        }
    }

    [Tooltip("The component that is of the type required.")]
    [SerializeField]
    private Component target;

    private TInterface targetAsInterface;

    public static implicit operator UnityInterfaceHelper<TInterface>(UnityInterfaceHelperBase b)
    {
        return new UnityInterfaceHelper<TInterface>()
        {
            target = b.target
        };
    }

    public static implicit operator UnityInterfaceHelperBase(UnityInterfaceHelper<TInterface> b)
    {
        return new UnityInterfaceHelperBase()
            {
                target = b.target
            };
    }
}

UnityInterfaceHelperPropertyDrawer.cs

[CustomPropertyDrawer(typeof(UnityInterfaceHelperBase), true)]
public class UnityInterfaceHelperPropertyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        label = EditorGUI.BeginProperty(position, label, property);

            position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
            EditorGUI.PropertyField(position, property.FindPropertyRelative("target"), GUIContent.none);

        EditorGUI.EndProperty();
    }
}

Typical usage

[SerializeField]
private UnityInterfaceHelperBase itemComparable;

public IComparable ItemComparable
{
    get { return ((UnityInterfaceHelper<IComparable>)itemComparable).TargetAsInterface; }
    set { ((UnityInterfaceHelper<IComparable>)landedComparable).TargetAsInterface = value; }
}

public void CompareItems()
{
    if(ItemComparable.CompareTo("Hello") == 0)
    {
        Debug.Log("Hello world!");
    }
}

I'm looking to reduce the amount of code I have to repeat for every property, it's already down quite a way, but I'm hoping to make it as simple as possible, but any other comments on the code are welcome too.

\$\endgroup\$
3
  • \$\begingroup\$ If there's no other idea about this, maybe do some code generation, possibly via T4? \$\endgroup\$
    – ferada
    Commented Oct 11, 2014 at 0:23
  • \$\begingroup\$ Check out the Full Inspector for Unity by Sient. No need to re-invent the wheel. I believe there is a trial version as well. The source code might also be available if you purchase it. jacobdufault.github.io/fullinspector \$\endgroup\$
    – Steve
    Commented Nov 4, 2014 at 16:53
  • \$\begingroup\$ I like it, but it has a number of issues that I think are dealbreakers: 1) Having to make all classes inherit from their own Monobehaviour subclass, that's a lot of renaming, 2) Having to use a custom serialization toolkit to get non-Moonobehaviour types serialized. I do like their extra features on top, but they're not what I'm looking for (and not free either). \$\endgroup\$
    – Nick Udell
    Commented Nov 4, 2014 at 17:01

1 Answer 1

3
+50
\$\begingroup\$

Possible bug:

If you get the value, targetAsInterface is initialized. One can then set the value to something that is not a Component. Like null.

If you then get the value again, you'll get the old value of target recasted to TInterface. Seems to me that this violates what get and set are supposed to do.

\$\endgroup\$
1
  • \$\begingroup\$ Looks like you're correct. Perhaps I need to implement PropertyChanged notifications or something similar. \$\endgroup\$
    – Nick Udell
    Commented Oct 17, 2014 at 8:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.