Serialized interface fields

Yep, that is the better approach at the moment and is the one I am using, but not ideal in my opinion since:

  1. we need to declare 2 fields and then cast (this is annoying and noisy) :frowning:
  2. we can drag and drop any Object in the serialized field (which can fail the cast if the wrong one is attached)
3 Likes

I agree, the ideal would be to have this feature native to Unity without this additional work.

1 Like

About two-fields solution:

  1. problem - allow drag wrong-typed references
    solution: clear reference in OnBeforeSerialize and in property setters if wrong-typed

  2. problem - need cast value somewhere
    solution: cast value in OnAfterDeserialize and in property setter whe assign

  3. problem - UnityEngine.Object references canā€™t be saved to same field as C# plain class.
    solution: use SerializeReference with interface and create plain class-container, including field with reference and inherited from interface

Basic Code

using System;
using UnityEngine;

[Serializable]
public class SerializedInterfaceReference<T1, T2> : ISerializationCallbackReceiver where T1 : UnityEngine.Object
{
    [SerializeField] T1 _classField;

    public T1 ClassField
    {
        get => _classField;
        set
        {
            if (value is T2 field)
            {
                InterfaceValue = field;
                _classField = value;
            }
            else if (value == default)
            {
                InterfaceValue = default;
                _classField = value;
            }
            else
            {
                throw new InvalidCastException($"Can't assign {value.GetType()} to {typeof(T2)}");
            }
        }
    }

    T2 _interfaceValue;

    public T2 InterfaceValue
    {
        get => _interfaceValue;
        set
        {
            _interfaceValue = value;
            ClassField = value as T1;
        }
    }

    public void OnBeforeSerialize()
    {
        if (ClassField != null && ClassField is not T2)
        {
            ClassField = null;
        }
    }

    public void OnAfterDeserialize()
    {
        InterfaceValue = ClassField is T2 field ? field : default;
    }
    public static implicit operator T1(SerializedInterfaceReference<T1, T2> d) => d._classField;
}

Usage Example.

  1. We can drag ā€˜OtherScriptableObjectā€™-value to ā€˜_referenceExampleā€™, cause saved ReferenceExample.ClassField type is ScriptableObject, but it would be nullified in OnBeforeSerialize.

  2. _referenceExample.Value is available after deserialization and no need to execute cast somewhere

  3. we can store C# plain class ( ClassValueExample ) and Container (ReferenceExample) with reference to ScriptableObject (CorrectScriptableObject) at same IInterface serialized field

8762335--1187926--SerializedInterfaceReference.gif

public interface IInterface
{
    void Execute();
}

[Serializable]
public class ReferenceExample : SerializedInterfaceReference<ScriptableObject, IInterface>, IInterface
{
    void IInterface.Execute() => InterfaceValue.Execute();
}

[Serializable]
public class ClassValueExample : IInterface
{
    public float floatField = 5;

    void IInterface.Execute() { }
}

[CreateAssetMenu]
public class OtherScriptableObject : ScriptableObject
{
    [SerializeReference] public IInterface _referenceExample;

    void ExecuteAnywhere()
    {
        IInterface value = _referenceExample;
        ScriptableObject scriptableObject = _referenceExample as ReferenceExample;
    }
}

[CreateAssetMenu]
public class CorrectScriptableObject : ScriptableObject, IInterface
{
    void IInterface.Execute() { }
}
2 Likes

+1 need this feature

+1 would make life so much easier

also check this Serialize Interfaces! | Utilities Tools | Unity Asset Store

2 Likes

Curious that this is (still?) an issue given how generally mature Unity is. Seems like it would be a fairly fundamental use case for object properties.

Iā€™m curious how developers that donā€™t find this an issue design around it. Would love to leverage some basic OO best practices but having to cram everything into an inheritance hierarchy is kinda a nonstarter.

1 Like

Itā€™s probably never going to happen. Unityā€™s serialisation is basic because it needs to be fast. SerializeReference is already slower than standard serialisation, and anything more complex would probably be slower yet.

There are a number of assets out there that add this functionality should you require it.

Otherwise you can still absolutely use interfaces, you will just have to change up how you use them in the context of Unity. Most often I use an interface + base class arrangement. More or less gets the same result.

Also I just noticed this in the original post:

This is false. SerializeReference serialises a reference to a section of managed serialised data. You can absolutely serialise reference-based hierarchies within the same UnityEngine.Object.

1 Like

Definitely appreciate the performance concerns. Mostly just wanting to add my voice to those hoping this gets a first-class solution at some point. Iā€™m holding out hope that Unityā€™s still open to some more considerableā€”even potentially breakingā€”changes if thatā€™s what it takes to keep it feeling modern.

Iā€™d happily pay a reasonable performance tax to be able to elect into a little more serialization flexibility. Maybe extend SerializeReference, maybe another attribute.

Iā€™m a big fan of Odin, in the Editor it is indispensable.

Yet, all the ā€œYouā€™re on your own.ā€ messages you have to click to use what seems like should be basic features along with the nested prefab scariness make it a bit more a minefield at runtime than Iā€™m comfortable with. As we get closer to release Iā€™m looking to remove our runtime dependency on it. Interface references to MonoBehaviours would get me most of the way there.

You mention ā€œa number of assetsā€. Is there one thatā€™s generally regarded at the best solution to this? I have yet to find one that doesnā€™t include boilerplate in my behaviours. Thanks!

Looks like youā€™re already using the best option:

You only see the ā€˜youā€™re on your ownā€™ messages in relation to prefabs, which have their entirely own serialisation format that Odin (as of yet) canā€™t emulate well enough to call it a supported featured. The devs have said the might be on the verge of solving this, however.

Otherwise in non-prefab components and scriptable objects, itā€™s been 100% reliable in my experience. That said, I only use Odin serialisation where absolutely necessary, as - even as fast as it is - it has a performance overhead as well.

@spiney199 Why do you think it will be slower for interfaces than concrete types?

Anything that isnā€™t in line with Unityā€™s super basic serialisation is going to be slower. Thereā€™s no getting around that.

Case in point: SerializeReference is slower than standard serialisation (as noted in the docs).

Ran yesterday for the first time into this problem. Just wanted to have a list of components which have one common method but inherit from different classes. In code I can nicely define an Interface for this usecase but thereā€™s no way to then assign the components to a List in the editor.

Ended up making sure that thereā€™s only one of the relevant components per game object and then used a List together with GetComponent.

Works but itā€™s a hassle that adds unnecesary GOs.

2 Likes

Welcome to the team :smile:

+1 Iā€™m too dumb for custom property drawers

Do not punish yourself, if it was a matter of creating custom property drawers, we would already have resolved it and shared it with everybody. I mean, there are plenty of ideas, but every one of them has downsides, mainly related to doing way more stuff than simply declaring, initializing, and using the serialized field as we would expect :frowning:

Canā€™t wait until this is properly implemented in Unity. For now, I personnally use SerializableInterface (which I contributed on ;)) which solve a lot of issues. One being that it properly checks null references for the interface, something you wouldnā€™t get by simply using an interface.

What do I mean about that? Letā€™s say you have this code:

public interface IMyInterface
{
    void Greet();
}

public class MyBehaviour : MonoBehaviour
{
    public IMyInterface mySerializableInterface;

    private void Awake()
    {
        if (mySerializableInterface != null)
            mySerializableInterface.Greet();
    }
}

If your mySerializableInterface is a C# class, then it will behave correctly. But letā€™s say it points to a MonoBehaviour with the IMyInterface interface and the object gets destroyed in the scene. Then, because itā€™s an interface and not an Object, the not-null check will not use Unityā€™s operator-override for == and will do a normal ReferenceEquals(mySerializableInterface, null) operation, which bypasses Unityā€™s lifetime. That means that the not-null check will return true (meaning it is not null in the native layer), but the mySerializableInterface.Greet(); will throw an NullReferenceException, since mySerializableInterface is null in the managed layer. SerializableInterface offers a simple method to check that:

public class MyBehaviour : MonoBehaviour
{
    public SerializableInterface<IMyInterface> mySerializableInterface;

    private void Awake()
    {
        if (mySerializableInterface.IsDefined(out IMyInterface value))
            value.Greet();
    }
}

The package also lets you points to plain C# classes, ScriptableObjects, prefabs and components in the scene.

4 Likes

It works great so far. That may be a great tool!

  1. How ā€œsafeā€ is to implement this in a project in terms of something breaking up when new unity version releases?
  2. Also on what platforms was it tested and it works?
  3. Additionally - can be performance an issue here?

Thanks!

EDIT:

I tested on:

  1. iOS
  2. Android
  3. tvOS
  4. macOS

it works great.

1 Like

Hey! Glad to hear it works great for you!

1. Pretty safe. On the runtime level, itā€™s standard C#, so nothing special there. As for the Editor level, it uses the Legacy GUI, which might get deprecated one day, although not soon. Iā€™m currently looking at changing the code to use UI Toolkit, but the whole thing changes too much to be the reliable solution for now.

2. Weā€™ve been using SerializableInterface in multiple projects since last year, mostly in VR (Android), with IL2CPP backend, without any issues. Since IL2CPP is the most restrictive backend, it should work very well in Mono. (we do use it in Mono when we test our application, cuz itā€™s faster to build). We also tested it in WebGL. Again, thereā€™s nothing special about the code, it just wraps an interface, thereā€™s no reflection or weird code there, so it should work on all platforms.

  1. Didnā€™t test yet. Would be nice to do so if anyone is a benchmark genius. But yes it does - of course - since itā€™s not just a simple reference to an interface, but a wrapper class. The trade-off though is ABSOLUTLY worth it. Weā€™ve been doing the best architecture design since weā€™ve introduced it in our projects. I mean, just about everything is reusable now. Itā€™s insane. But! Since you asked, these are the two considerations weā€™ve had in terms of performance:

3.1 Serialization:
It does have some serialization impacts, although, itā€™s not a lot. Basically, SerializableInterface has 3 fields, which all of them needs to be serialized (in the scene, prefab or in a ScriptableObjectā€¦). So while you only deal with an interface, SerializableInterface needs to have a Mode (enum), a UnityReference (Object) and a RawObjectReference (object). This can slightly increase the size of your serialized Object, but it ensures a proper serialization of Unity Objects and raw C# objects (for the null check, for instance).

3.2 Allocation:
While an interface field can contain a struct and be allocated on the stack or directly in an object on the heap, SerializableInterface is a class, so it will be declared on the heap and subject to GC. This shouldnā€™t be a huge deal in most cases though. In any case, changing SerializableInterface to a struct wasnā€™t a practical choice for us since we inherit from it.

Hope it answers your questions. :slight_smile:

3 Likes

If someone else is looking for the implementation of this function, I can share my version. I found the basis for it in one of the many threads in which the same topic was discussed and I finished it up to working condition.
SerializeInterfaceAttribute

using UnityEngine;

public class SerializeInterfaceAttribute : PropertyAttribute
{
    public System.Type SerializedType { get; private set; }

    public SerializeInterfaceAttribute(System.Type type)
    {
        SerializedType = type;
    }
}

SerializeInterfaceDrawer

using UnityEngine;
using UnityEditor;
using System;
using Object = UnityEngine.Object;

[CustomPropertyDrawer(typeof(SerializeInterfaceAttribute))]
public class SerializeInterfaceDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        SerializeInterfaceAttribute serializedInterface = attribute as SerializeInterfaceAttribute;
        Type serializedType = serializedInterface.SerializedType;

        if (IsValid(property, serializedType))
        {
            label.tooltip = "Serialize " + serializedInterface.SerializedType.Name + " interface";
            CheckProperty(property, serializedType);

            if (position.Contains(Event.current.mousePosition) == true)
            {
                if (DragAndDrop.objectReferences.Length > 0)
                {
                    if (TryGetInterfaceFromObject(DragAndDrop.objectReferences[0], serializedType) == null)
                        DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
                }
            }
        }

        label.text += $" ({serializedType.Name})";
        EditorGUI.PropertyField(position, property, label);
    }

    private Object TryGetInterfaceFromObject(Object targetObject, Type serializedType)
    {
        if (serializedType.IsInstanceOfType(targetObject) == false)
        {
            if (targetObject is Component)
                return (targetObject as Component).GetComponent(serializedType);
            else if (targetObject is GameObject)
                return (targetObject as GameObject).GetComponent(serializedType);
        }

        return targetObject;
    }

    private bool IsValid(SerializedProperty property, Type targetType)
    {
        return targetType.IsInterface && property.propertyType == SerializedPropertyType.ObjectReference;
    }

    private void CheckProperty(SerializedProperty property, Type targetType)
    {
        if (property.objectReferenceValue == null)
            return;

        property.objectReferenceValue = TryGetInterfaceFromObject(property.objectReferenceValue, targetType);
    }
}

The only condition for using this attribute is that the field must be of the UnityEngine.Object type, so that it is possible to use Drag and Drop to set components and GameObjects into it.

[SerializeField, SerializeInterface(typeof(IMyInterface))] private UnityEngine.Object _interfaceField;

But this problem can be easily smoothed out by creating a duplicate property for a field already with the desired interface type.

private IMyInterface InterfaceProperty => _interfaceField as IMyInterface;

If you drag a GameObject into this field, on which there are several components implementing the desired interface, then the very first one in the hierarchy will be selected.

And also, bugs known to me: when several components are selected at the same time, which have the same fields with a serializable interface, they are all overwritten with the value of the first selected component. So itā€™s better not to do that. I havenā€™t figured out how to fix it, maybe someone here will tell me.

2 Likes