Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Reference interfaces in Editor! (workaround)

Discussion in 'Editor Workflows' started by Gamebient, Oct 12, 2022.

  1. Gamebient

    Gamebient

    Joined:
    Mar 8, 2019
    Posts:
    15
    I was again looking for some weird ways to speed up my workflow in unity.
    Most of us know that we are not able to reference interface implementations in the editor that easily. Nevertheless I found a solution that may not be the best out there but only needs two classes of less than 100 lines of code to work. All assets that inherit from UnityEngine.Object are supported.

    That said, couldn't it be possible to make Interface references serializable internally??

    How does it work?
    We use an object field to be able to assign any object but actually filter it with the interface we want.
    In the inspector we can then drag and drop any UnityEngine.Object that implements the given interface (I haven't got the object picker working yet). This means the window containing this field needs to stay open e.g. select GameObject > click three dots > select "Properties...".
    Open Properties Drawn.png
    Then just navigate to the Object/Component you want to add and drag it to the slot

    Drop Drawn.png
    And that's it. You can now access it through script. Only null checks are needed to prevent errors, no casts to other types!

    The Code needed
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [System.Serializable]
    4. public class InterfaceReference<TInterface>
    5.     where TInterface : class
    6. {
    7.     [SerializeField] Object m_Target;
    8.     public TInterface Target => m_Target as TInterface;
    9. }
    Code (CSharp):
    1. using System.Collections;
    2. using UnityEditor;
    3. using UnityEngine;
    4.  
    5. [CustomPropertyDrawer(typeof(InterfaceReference<>), true)]
    6. public class InterfaceReferencePropertyDrawer : PropertyDrawer
    7. {
    8.     SerializedProperty property_Target;
    9.  
    10.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    11.     {
    12.         EditorGUI.BeginProperty(position, label, property);
    13.         property_Target = property.FindPropertyRelative("m_Target");
    14.         Rect rect = new(position.x, position.y, position.width, EditorGUI.GetPropertyHeight(property_Target));
    15.         EditorGUI.ObjectField(rect, property_Target, GetInterfaceType(), label);
    16.         EditorGUI.EndProperty();
    17.     }
    18.    
    19.  
    20.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    21.     {
    22.         return base.GetPropertyHeight(property, label);
    23.     }
    24.  
    25.     System.Type GetInterfaceType()
    26.     {
    27.         System.Type type = fieldInfo.FieldType;
    28.         System.Type[] typeArguments = type.GenericTypeArguments;
    29.         if (typeof(IEnumerable).IsAssignableFrom(type))
    30.         {
    31.             typeArguments = fieldInfo.FieldType.GenericTypeArguments[0].GenericTypeArguments;
    32.         }
    33.         if (typeArguments == null || typeArguments.Length == 0)
    34.             return null;
    35.         return typeArguments[0];
    36.     }
    37. }

    Example
    Code (CSharp):
    1. public interface IInteractable
    2. {
    3.     void Interact();
    4. }
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3.  
    4. public class Interactor : MonoBehaviour
    5. {
    6.     [SerializeField] List<InterfaceReference<IInteractable>> m_Interactables;
    7.  
    8.     private void Awake()
    9.     {
    10.         foreach (var interactable in m_Interactables)
    11.         {
    12.             if (interactable == null)
    13.                 continue;
    14.             interactable.Target.Interact();
    15.         }
    16.     }
    17. }
     
    CaseyHofland likes this.
  2. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    EDIT: New version, view below.

    Thank you, this was exactly what I needed!

    I've rewritten this to be a little more sleek using attributes. It also uses the new UIElements API.

    (Bonus tip: whenever you're dealing with something in Unity where you have all the serialization values you need and only need a drawer, you can do it using attributes. This is how I made a drawer for TimeSpan that you can use like
    [TimeSpan] private long _timeSpan;
    )

    Use like so:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Animations;
    3.  
    4. // IConstraints are things like AimConstraint, ParentConstraint, etc. They are included with Unity.
    5.     public class MyIConstraintSelector: MonoBehaviour
    6.     {
    7.         [SerializeField, OfType(typeof(IConstraint))] private Object _constraint;
    8.  
    9.         // Example: Array that specifies components / scene objects.
    10.         [SerializeField, OfType(typeof(IConstraint))] public Component[] _constraints;
    11.  
    12.         // Use this in code.
    13.         public IConstraint constraint
    14.         {
    15.             get => _constraint as IConstraint;
    16.             set => _constraint = value as Object;
    17.         }
    18.     }
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4. namespace Heatgrid
    5. {
    6.     public class OfTypeAttribute : PropertyAttribute
    7.     {
    8.         public Type type;
    9.  
    10.         public OfTypeAttribute(Type type)
    11.         {
    12.             this.type = type;
    13.         }
    14.     }
    15. }
    16.  
    Note: a very cool addition to this would be to support multiple types using
    params Type[] types
    but this poses problems for the drawer, see the note there.

    Code (CSharp):
    1. using System.Linq;
    2. using UnityEditor;
    3. using UnityEditor.UIElements;
    4. using UnityEngine;
    5. using UnityEngine.UIElements;
    6. namespace Heatgrid.Editor
    7. {
    8.     [CustomPropertyDrawer(typeof(OfTypeAttribute))]
    9.     public class OfTypeDrawer : PropertyDrawer
    10.     {
    11.         public override VisualElement CreatePropertyGUI(SerializedProperty property)
    12.         {
    13.             if (property.propertyType != SerializedPropertyType.ObjectReference
    14.                 && property.propertyType != SerializedPropertyType.ExposedReference)
    15.             {
    16.                 throw new System.ArgumentException("This attribute is not supported on properties of this property type.", nameof(property.propertyType));
    17.             }
    18.             var ofType = attribute as OfTypeAttribute;
    19.             var objectField = new ObjectField(property.displayName);
    20.             objectField.AddToClassList(ObjectField.alignedFieldUssClassName);
    21.             objectField.BindProperty(property);
    22.             objectField.objectType = ofType.type;
    23.             objectField.RegisterValueChangedCallback(changed =>
    24.             {
    25.                 Component component;
    26.                 if (IsValid(changed.newValue))
    27.                 {
    28.                     return;
    29.                 }
    30.                 else if (changed.newValue is GameObject gameObject
    31.                     && (component = gameObject.GetComponents<Component>().FirstOrDefault(component => IsValid(component))))
    32.                 {
    33.                     objectField.SetValueWithoutNotify(component);
    34.                     return;
    35.                 }
    36.                 else if (changed.newValue)
    37.                 {
    38.                     objectField.SetValueWithoutNotify(null);
    39.                 }
    40.                 bool IsValid(Object obj) => obj && ofType.type.IsAssignableFrom(obj.GetType());
    41.             });
    42.             return objectField;
    43.         }
    44.     }
    45. }
    Note:
    It would be cool to support multiple types in the OfTypeAttribute that can be added to the
    IsValid
    method using
    ofType.types.All(type => ...)
    , however the objectField.objectType poses problematic as it allows only one type. To support this, a method would need to be found to override the objectField selector, but for now that's overkill.

    Note:
    Depending on your editor version, it might throw this error a million-and-one times when you remove the MonoBehaviour you use this on.
    NullReferenceException: SerializedObject of SerializedProperty has been Disposed.

    From what I read it should only be on older versions and it causes no harm, just trigger an editor reset with a script change and it disappears. Not great but I really can't be bothered to babysit Unity all the time.
     
    Last edited: Dec 29, 2023
  3. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    New version:
    - multiple type support!!!
    - drag and drop that actually works!
    - object selector won't show up empty! (still not specifically targeting interfaces, but I can only do so much)


    The one caveat is that I use an internal Unity function. I have a safe way to accessing internal Unity code, but it takes a lot of steps to set up and requires the use of assembly definitions. If that solution is not for you, you will need to use reflection to access this method:
    FieldInfo UnityEditor.ScriptAttributeUtility.GetFieldInfoFromProperty(SerializedProperty property, out Type type);

    That's left as an exercise to the reader though.

    Alternatively, you can look online for a method that returns a SerializedProperty's Type, though there is no guarantee these work in all circumstances, like wrappers, arrays, or aliases.

    Alternatively, it will be added to Cubusky.Core (eventually) which handles the internal accessing for you.

    Use like so:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.UI;
    3.  
    4.     // Test out with UI Components e.g. Dropdown, Button, Slider.
    5.     public class MyUISelector: MonoBehaviour
    6.     {
    7.         // This will accept a Dropdown and a Button, but not a Slider.
    8.         // The object selector will respect the Selectable type.
    9.         [SerializeField, OfType(typeof(IPointerClickHandler), typeof(ISubmitHandler))] private Selectable _selectable;
    10.  
    11.         // This works like you would expect too.
    12.         [SerializeField, OfType(typeof(Component))] private Object _IAmDense;
    13.     }
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3.  
    4.     /// <summary>
    5.     /// Specifies types that an Object needs to be of. Can be used to create an Object selector that allows interfaces.
    6.     /// </summary>
    7.     public class OfTypeAttribute : PropertyAttribute
    8.     {
    9.         public Type[] types;
    10.  
    11.         public OfTypeAttribute(Type type)
    12.         {
    13.             this.types = new Type[] { type };
    14.         }
    15.  
    16.         public OfTypeAttribute(params Type[] types)
    17.         {
    18.             this.types = types;
    19.         }
    20.     }

    Code (CSharp):
    1. using System.Linq;
    2. using UnityEditor;
    3. using UnityEditor.UIElements;
    4. using UnityEngine;
    5. using UnityEngine.UIElements;
    6.  
    7.     [CustomPropertyDrawer(typeof(OfTypeAttribute))]
    8.     public class OfTypeDrawer : PropertyDrawer
    9.     {
    10.         public override VisualElement CreatePropertyGUI(SerializedProperty property)
    11.         {
    12.             // Check if the property type is of an object reference.
    13.             if (property.propertyType != SerializedPropertyType.ObjectReference
    14.                 && property.propertyType != SerializedPropertyType.ExposedReference)
    15.             {
    16.                 throw new System.ArgumentException("This attribute is not supported on properties of this property type.", nameof(property.propertyType));
    17.             }
    18.  
    19.             // Set up the type variables.
    20.             InternalEditorBridge.GetFieldInfoFromProperty(property, out var type); // INTERNAL UNITY FUNCTION
    21.             var ofType = attribute as OfTypeAttribute;
    22.  
    23.             // Set up the object field.
    24.             var objectField = new ObjectField(property.displayName);
    25.             objectField.AddToClassList(ObjectField.alignedFieldUssClassName);
    26.             objectField.BindProperty(property);
    27.             objectField.objectType = type;
    28.  
    29.             // Disable dropping if not assignable from drag and drop.
    30.             objectField.RegisterCallback<DragUpdatedEvent>(dragUpdated =>
    31.             {
    32.                 if (!DragAndDrop.objectReferences.Any(obj
    33.                     => obj is GameObject gameObject ? FirstValidOrDefault(gameObject) : IsValid(obj)))
    34.                 {
    35.                     dragUpdated.PreventDefault();
    36.                 }
    37.             });
    38.  
    39.             // Assign the appropriate value.
    40.             objectField.RegisterValueChangedCallback(changed =>
    41.             {
    42.                 if (IsValid(changed.newValue))
    43.                 {
    44.                     return;
    45.                 }
    46.                 else if (changed.newValue is GameObject gameObject
    47.                     || changed.newValue is Component component && (gameObject = component.gameObject))
    48.                 {
    49.                     objectField.value = FirstValidOrDefault(gameObject);
    50.                 }
    51.                 else
    52.                 {
    53.                     objectField.value = null;
    54.                 }
    55.             });
    56.  
    57.             return objectField;
    58.  
    59.             // Helper methods.
    60.             bool IsValid(Object obj) => !obj || type.IsAssignableFrom(obj.GetType()) && ofType.types.All(type => type.IsAssignableFrom(obj.GetType()));
    61.             Component FirstValidOrDefault(GameObject gameObject) => gameObject.GetComponents<Component>().FirstOrDefault(IsValid);
    62.         }
    63.     }
     
    Last edited: Dec 30, 2023
  4. KrisGungrounds

    KrisGungrounds

    Joined:
    Aug 20, 2019
    Posts:
    15
    This is just amazing, great work everyone!

    @CaseyHofland - I tried to include your code in my project but noticed that it keeps reloading/refreshing, making the Unity Editor slow. Does this happen for you as well? Does it need some specific additional setup?

    Also, the reference field in the inspector wasn't properly drawn, it was a bit longer than the rest. Small thing, but wondering is it the case with your version as well?

    Version 1.1.0.
     
  5. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    Cubusky.Core does not hook into any UnityEditor delegates or slow down the Editor in any way, I’m very mindful of that.

    Say you have this code:
    Code (CSharp):
    1. public class MyMonoBehaviour : MonoBehaviour
    2. {
    3.     [OfType(typeof(IMyInterface))] public Object myInterface;
    4. }
    This by itself does nothing passively, it hooks into dropdown or hover events.

    However, trying to select an object can be very slow, as it will search every UnityEngine.Object in your project.

    Unfortunately I haven’t figured out how to filter for interfaces in the Object selector. If anyone has any suggestions I’d love to hear them, it’s bringing down an otherwise sweet implementation.
     
    KrisGungrounds likes this.
  6. KrisGungrounds

    KrisGungrounds

    Joined:
    Aug 20, 2019
    Posts:
    15
    Alright, it seems the slowness was related to something else. I also adjusted the inspector so it looks nice.

    I was testing this yesterday on Windows, but today I was working on my Macbook, and I pulled the changes.
    This is the error I am getting even though both projects are using the same Unity version and the same cubusky package.

    Any ideas why? Am I missing some additional package?

    1. Assets/com.cubusky.core-1.1.0/Runtime/InternalBridge/UIElements/InternalEngineBridge.cs(16,94): error CS0122: 'TextInputBaseField<TValueType>.TextInputBase' is inaccessible due to its protection level
    2. [4:44 PM]
      Assets/com.cubusky.core-1.1.0/Runtime/InternalBridge/UIElements/InternalEngineBridge.cs(21,103): error CS0122: 'ExpressionEvaluator.Expression' is inaccessible due to its protection level
     
  7. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    I notice that you have this installed in the Assets folder. I design my libraries as Unity packages, and they are meant to be placed inside the Unity's Packages folder.

    If you do want to keep it in the Assets folder (although I strongly recommend against it), what probably happened was that it didn't install its dependency on the 2D Common Package. I sometimes use it to access internal Unity code through the InternalEngineBridge.
     
    Last edited: Feb 24, 2024
  8. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    I just released version 1.2.0! Pick it up at Cubusky.Core!

    My suggestion is that you delete the copy you have in your assets folder and install this via the installation instructions.

    I suspect that you installed it this way because you wanted to make changes to the package. In that case I suggest to instead Fork Cubusky.Core and clone that fork locally to the packages folder in your Unity project. If your Unity project is itself a .git project, add Cubusky.Core as a submodule. If it is your first time with submodules it may take some setting up but the payoff is well worth it, it is by far the cleanest way to have git projects in git projects.
     
    KrisGungrounds likes this.
  9. KrisGungrounds

    KrisGungrounds

    Joined:
    Aug 20, 2019
    Posts:
    15
    Thanks, that was it!

    The reason why I didn't install it through a Unity package system is that I have a code framework in a separate git repository, and that framework is cloned for other projects. The issue is the way Unity packages work is that they don't add Cubusky.Core to other projects.

    I just tried 1.2, and I see that the field indentation in inspector is not correct, I updated the code that fixes it

    Code (CSharp):
    1.  
    2. using System.Linq;
    3. using UnityEditor;
    4. using UnityEngine;
    5. namespace Cubusky.Editor
    6. {
    7.     [CustomPropertyDrawer(typeof(OfTypeAttribute))]
    8.     public class OfTypeDrawer : PropertyDrawer
    9.     {
    10.         public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    11.         {
    12.             // Verify the property type is supported.
    13.             if (property.propertyType != SerializedPropertyType.ObjectReference)
    14.             {
    15.                 EditorGUI.LabelField(position, label.text, "Use OfType with ObjectReference properties only.");
    16.                 return;
    17.             }
    18.             OfTypeAttribute ofType = attribute as OfTypeAttribute;
    19.             EditorGUI.BeginProperty(position, label, property);
    20.             // Calculate the object field position and size
    21.             Rect objectFieldPosition = position;
    22.             objectFieldPosition.height = EditorGUIUtility.singleLineHeight;
    23.             // Draw the object field and get the selected object
    24.             Object objectValue = EditorGUI.ObjectField(objectFieldPosition, label, property.objectReferenceValue, fieldInfo.FieldType, true);
    25.             // Validate the assigned object
    26.             if (objectValue != null && !ofType.types.Any(t => t.IsAssignableFrom(objectValue.GetType())))
    27.             {
    28.                 property.objectReferenceValue = null;
    29.                 // Adjust the position for the help box below the object field
    30.                 Rect helpBoxPosition = position;
    31.                 helpBoxPosition.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
    32.                 helpBoxPosition.height = EditorGUIUtility.singleLineHeight * 2; // Adjust height as needed
    33.                 EditorGUI.HelpBox(helpBoxPosition, $"This field only accepts types: {string.Join(", ", ofType.types.Select(t => t.Name))}", MessageType.Error);
    34.             }
    35.             else
    36.             {
    37.                 property.objectReferenceValue = objectValue;
    38.             }
    39.             EditorGUI.EndProperty();
    40.         }
    41.     }
    42. }
    43.  
    44.  
     
    Last edited: Feb 24, 2024
  10. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    610
    Can you please attach a picture of the incorrect indentation? I’m not getting this issue. I use the new UIElements namespaces for UI creation- I know there is a setting that forces the editor to use IMGUI and if that setting is on perhaps it creates this issue, but I’ll have to check.
     
  11. KrisGungrounds

    KrisGungrounds

    Joined:
    Aug 20, 2019
    Posts:
    15
    @CaseyHofland - sorry didnt get back to you, but it was just a bit of off, and with the changes to the code I showed above, it was fixed.

    We noticed one thing, wondering is it related. It feels like all the time there is something working in the background.
    We tried to profile the editor, but nothing came out, mostly used by Application.idle.
    This happens only when using Cubusky.Core.