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…”.
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
InterfaceReference.cs
using UnityEngine;
[System.Serializable]
public class InterfaceReference<TInterface>
where TInterface : class
{
[SerializeField] Object m_Target;
public TInterface Target => m_Target as TInterface;
}
InterfaceReferencePropertyDrawer.cs
using System.Collections;
using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(InterfaceReference<>), true)]
public class InterfaceReferencePropertyDrawer : PropertyDrawer
{
SerializedProperty property_Target;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
property_Target = property.FindPropertyRelative("m_Target");
Rect rect = new(position.x, position.y, position.width, EditorGUI.GetPropertyHeight(property_Target));
EditorGUI.ObjectField(rect, property_Target, GetInterfaceType(), label);
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return base.GetPropertyHeight(property, label);
}
System.Type GetInterfaceType()
{
System.Type type = fieldInfo.FieldType;
System.Type[] typeArguments = type.GenericTypeArguments;
if (typeof(IEnumerable).IsAssignableFrom(type))
{
typeArguments = fieldInfo.FieldType.GenericTypeArguments[0].GenericTypeArguments;
}
if (typeArguments == null || typeArguments.Length == 0)
return null;
return typeArguments[0];
}
}
Example
IInteractable.cs
public interface IInteractable
{
void Interact();
}
Interactor.cs
using System.Collections.Generic;
using UnityEngine;
public class Interactor : MonoBehaviour
{
[SerializeField] List<InterfaceReference<IInteractable>> m_Interactables;
private void Awake()
{
foreach (var interactable in m_Interactables)
{
if (interactable == null)
continue;
interactable.Target.Interact();
}
}
}
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:
using UnityEngine;
using UnityEngine.Animations;
// IConstraints are things like AimConstraint, ParentConstraint, etc. They are included with Unity.
public class MyIConstraintSelector: MonoBehaviour
{
[SerializeField, OfType(typeof(IConstraint))] private Object _constraint;
// Example: Array that specifies components / scene objects.
[SerializeField, OfType(typeof(IConstraint))] public Component[] _constraints;
// Use this in code.
public IConstraint constraint
{
get => _constraint as IConstraint;
set => _constraint = value as Object;
}
}
OfTypeAttribute
using System;
using UnityEngine;
namespace Heatgrid
{
public class OfTypeAttribute : PropertyAttribute
{
public Type type;
public OfTypeAttribute(Type type)
{
this.type = type;
}
}
}
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.
OfTypeDrawer
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace Heatgrid.Editor
{
[CustomPropertyDrawer(typeof(OfTypeAttribute))]
public class OfTypeDrawer : PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
if (property.propertyType != SerializedPropertyType.ObjectReference
&& property.propertyType != SerializedPropertyType.ExposedReference)
{
throw new System.ArgumentException("This attribute is not supported on properties of this property type.", nameof(property.propertyType));
}
var ofType = attribute as OfTypeAttribute;
var objectField = new ObjectField(property.displayName);
objectField.AddToClassList(ObjectField.alignedFieldUssClassName);
objectField.BindProperty(property);
objectField.objectType = ofType.type;
objectField.RegisterValueChangedCallback(changed =>
{
Component component;
if (IsValid(changed.newValue))
{
return;
}
else if (changed.newValue is GameObject gameObject
&& (component = gameObject.GetComponents<Component>().FirstOrDefault(component => IsValid(component))))
{
objectField.SetValueWithoutNotify(component);
return;
}
else if (changed.newValue)
{
objectField.SetValueWithoutNotify(null);
}
bool IsValid(Object obj) => obj && ofType.type.IsAssignableFrom(obj.GetType());
});
return objectField;
}
}
}
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.
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:
using UnityEngine;
using UnityEngine.UI;
// Test out with UI Components e.g. Dropdown, Button, Slider.
public class MyUISelector: MonoBehaviour
{
// This will accept a Dropdown and a Button, but not a Slider.
// The object selector will respect the Selectable type.
[SerializeField, OfType(typeof(IPointerClickHandler), typeof(ISubmitHandler))] private Selectable _selectable;
// This works like you would expect too.
[SerializeField, OfType(typeof(Component))] private Object _IAmDense;
}
OfTypeAttribute
using System;
using UnityEngine;
/// <summary>
/// Specifies types that an Object needs to be of. Can be used to create an Object selector that allows interfaces.
/// </summary>
public class OfTypeAttribute : PropertyAttribute
{
public Type[] types;
public OfTypeAttribute(Type type)
{
this.types = new Type[] { type };
}
public OfTypeAttribute(params Type[] types)
{
this.types = types;
}
}
OfTypeDrawer
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
[CustomPropertyDrawer(typeof(OfTypeAttribute))]
public class OfTypeDrawer : PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
// Check if the property type is of an object reference.
if (property.propertyType != SerializedPropertyType.ObjectReference
&& property.propertyType != SerializedPropertyType.ExposedReference)
{
throw new System.ArgumentException("This attribute is not supported on properties of this property type.", nameof(property.propertyType));
}
// Set up the type variables.
InternalEditorBridge.GetFieldInfoFromProperty(property, out var type); // INTERNAL UNITY FUNCTION
var ofType = attribute as OfTypeAttribute;
// Set up the object field.
var objectField = new ObjectField(property.displayName);
objectField.AddToClassList(ObjectField.alignedFieldUssClassName);
objectField.BindProperty(property);
objectField.objectType = type;
// Disable dropping if not assignable from drag and drop.
objectField.RegisterCallback<DragUpdatedEvent>(dragUpdated =>
{
if (!DragAndDrop.objectReferences.Any(obj
=> obj is GameObject gameObject ? FirstValidOrDefault(gameObject) : IsValid(obj)))
{
dragUpdated.PreventDefault();
}
});
// Assign the appropriate value.
objectField.RegisterValueChangedCallback(changed =>
{
if (IsValid(changed.newValue))
{
return;
}
else if (changed.newValue is GameObject gameObject
|| changed.newValue is Component component && (gameObject = component.gameObject))
{
objectField.value = FirstValidOrDefault(gameObject);
}
else
{
objectField.value = null;
}
});
return objectField;
// Helper methods.
bool IsValid(Object obj) => !obj || type.IsAssignableFrom(obj.GetType()) && ofType.types.All(type => type.IsAssignableFrom(obj.GetType()));
Component FirstValidOrDefault(GameObject gameObject) => gameObject.GetComponents<Component>().FirstOrDefault(IsValid);
}
}
@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?
[quote=KrisGungrounds, post: 9660572, member: 3570536]
This is just amazing, great work everyone!
@ - 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.
[/quote]
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:
public class MyMonoBehaviour : MonoBehaviour
{
[OfType(typeof(IMyInterface))] public Object myInterface;
}
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.
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?
Assets/com.cubusky.core-1.1.0/Runtime/InternalBridge/UIElements/InternalEngineBridge.cs(16,94): error CS0122: ‘TextInputBaseField.TextInputBase’ is inaccessible due to its protection level
[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
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.
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.
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
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace Cubusky.Editor
{
[CustomPropertyDrawer(typeof(OfTypeAttribute))]
public class OfTypeDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// Verify the property type is supported.
if (property.propertyType != SerializedPropertyType.ObjectReference)
{
EditorGUI.LabelField(position, label.text, "Use OfType with ObjectReference properties only.");
return;
}
OfTypeAttribute ofType = attribute as OfTypeAttribute;
EditorGUI.BeginProperty(position, label, property);
// Calculate the object field position and size
Rect objectFieldPosition = position;
objectFieldPosition.height = EditorGUIUtility.singleLineHeight;
// Draw the object field and get the selected object
Object objectValue = EditorGUI.ObjectField(objectFieldPosition, label, property.objectReferenceValue, fieldInfo.FieldType, true);
// Validate the assigned object
if (objectValue != null && !ofType.types.Any(t => t.IsAssignableFrom(objectValue.GetType())))
{
property.objectReferenceValue = null;
// Adjust the position for the help box below the object field
Rect helpBoxPosition = position;
helpBoxPosition.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
helpBoxPosition.height = EditorGUIUtility.singleLineHeight * 2; // Adjust height as needed
EditorGUI.HelpBox(helpBoxPosition, $"This field only accepts types: {string.Join(", ", ofType.types.Select(t => t.Name))}", MessageType.Error);
}
else
{
property.objectReferenceValue = objectValue;
}
EditorGUI.EndProperty();
}
}
}
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.
Hi Kris, as far as I’m aware it should work on Unity 6.
There should be no issue porting to Switch or PS5. The great thing about this script is that it does all the hard work in-editor. At runtime it just uses Unity’s SerializeReference, so unless that has issues on those consoles you should be fine
Oh wow, that looks amazing indeed. One idea that comes to mind is that it could be used for building a complex hierarchical tree of types that also have some specific methods attached to them.
BTW if I want to try this out, what would be the best way? I tried to pull it through package manager, but it wouldn’t allow it. And, if I copy/paste it, it’s not working, I guess it requires additional packages.
There’s installation instructions in the readme. It links to Unity’s page for how to install git packages. Make sure you install the git client, git lfs is not needed.
After that go through the “Procedure”-part step by step.
BTW I am wondering would this be possible to do - Component<T>:Monobehaviour where the user could attach this Component and define the T through inspector rather than making a new class that specifies the T with ComponentT:Component.
Hi @KrisGungrounds , I am unsure what you are trying to accomplish, but either way I can’t help you with specific implementations. Those will always come down to the nature of your project. We can discuss a consultancy fee and project for me to tackle if you like, if so send me a private message (Unity Discussions has those).
You may be inspired by these design patterns though:
Also: composition > inheritance. Interfaces and extension methods may be a better solution for adding generic functionality than abstraction.
Unity doesn’t support generic components or scriptable objects without having a concrete type. Its an inherent limitation of the engine and the way Unity maps your monobehaviour component and scriptable object instances to their respective MonoScript.
On topic with the thread, if you have anything that lets you edit [SerializeReference] fields via the inspector, there’s a simple pattern that can let you do this. Namely, you just make a serializable plain C# type implementing your interface which acts as a medium for referencing an asset.
In code:
public interface IMyInterface
{
void SomeMethod();
}
public sealed class MyInterfaceAsset : ScriptableObject, IMyInterface
{
public void SomeMethod() { }
}
[System.Serialzable]
public sealed class MyInterfaceAssetReference : IMyInterface
{
[SerializeField]
private MyInterfaceAsset _asset;
public void SomeMethod()
{
if (_asset != null)
{
_asset.SomeMethod();
}
}
}
// usage
[SerializeReference]
private IMyInterface _myInterface = new MyInterfaceAssetReference();
This way you get to keep full type safety at the code level.
Not applicable in all cases, but works well in data-driven cases so you can intermingle scriptable objects with plain C# types within the same system.