Search Unity

  1. We are migrating the Unity Forums to Unity Discussions by the end of July. Read our announcement for more information and let us know if you have any questions.
    Dismiss Notice
  2. Dismiss Notice

Question How can you decouple correctly if you'r not able to assing interfaces in the inspector?

Discussion in 'Scripting' started by calpolican, May 14, 2024.

  1. calpolican

    calpolican

    Joined:
    Feb 2, 2015
    Posts:
    427
    Hi, I'm trying to reduce the dependencies of subsystems in my code by the use of interfaces. Is this classic arrengement:
    costumer (uses interfaces instead of classes) => [interfaces] <= services(implement a given interface)
    However, I'm having a hard time doing this, since in Unity, most dependcies come from public fields assigned via the inspector. So, in order to use a system, I'd have to assign an interface in the inspector, however, as you might know, that's not possible in unity.
    Should I try to bypass this restriction and allow interfaces to be publicly assign, or should I use some other scheem?
     
  2. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,219
    Unity can not serialize reference to interface types. However I made this SerializableInterface class. Internally it simply stores a UnityEngine.Object reference but the property drawer will take care to only accept instances which implement the given interface. It also works with both, MonoBehaviours and ScriptableObjects.

    When you drag a gameobject onto the field and it has several components that implement this interface, you will get a context menu to select which one you want to assign. Though you can always just drag the actual component instance by dragging the header as usual.
     
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,430
    Unity can serialize plain C# types implementing an interface via SerializeReference, though doesn't have built in support for assigning said references without your own custom inspector work. Tools like Odin Inspector + Serializer can reference Unity objects via interfaces, with the caveat that it's unstable with prefabs.

    Otherwise various tools exist to allow you to do this. It's a problem the community has solved many times over.
     
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,219
    That's true, though it's worth to point out that it does not support serialization of UnityEngine.Object derived type references which was the main idea here I think. Just linking up components like usual, but don't have them directly depend on each other but have an interface as common ground.

    SerializeReference
     
    spiney199 likes this.
  5. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,372
    I went with bypassing the restriction personally. I just find it really useful in some cases to be able to assign any object that implements an interface, rather than being only able to drag in objects that derive from a specific base class.

    There are four main strategies, from what I've seen, that have been used to overcome the limitation:

    1. Serialization via Base Class
    1. Implement custom serialization logic capable of serializing interface types.
    2. Implement a custom editor that enables assigning values to the interface type fields via the Inspector.
    3. Have the client derive from a base class that handles the serialization via ISerializationCallbackReceiver.
    Code (CSharp):
    1. class Client : SerializedMonoBehaviour
    2. {
    3.     [OdinSerialize] IService service;
    4. }
    Used by Odin Inspector and Serializer.

    2. Serialization via Wrapper
    1. Implement a generic wrapper class capable of serializing interface type objects.
    2. Implement a custom property drawer for the wrapper that enables assigning values via the Inspector.
    3. Wrap each interface type field in the client with the wrapper class.
    Code (CSharp):
    1. class Client : MonoBehaviour
    2. {
    3.     [SerializeField] SerializableInterface<IService> service;
    4.  
    5.     IService Service => service.Value;
    6. }
    Used by SerializableInterface.

    3. Separate Initializer Composer
    1. Create a separate component responsible for resolving the interface type fields' values and injecting them to the client.
    2. The composer can internally use method #1 or #2 to enable assigning values via the Inspector and serializing them.
    Code (CSharp):
    1. class ClientInitializer : Initializer<Client, IService> { }
    2.  
    3. class Client : MonoBehaviour, IInitializable<IService>
    4. {
    5.     IService service;
    6.  
    7.     public void Init(IService service) => this.service = service;
    8. }
    Used by Init(args) (created by me), Zenject.

    4. Source Generators
    1. Create a source generator that adds code to partial classes that takes care of serializing interface type fields.
    2. Use a custom editor or property drawer to enable assigning values to the fields via the Inspector.
    Code (CSharp):
    1. partial class Client : MonoBehaviour
    2. {
    3.     [SerializeInterface] IService service;
    4.  
    Used by [SerializeInterface].


    In lieu of using actual interfaces, it's also possible to implement the facade pattern / adapter pattern to enable specifying different implementations that don't need to derive from any particular base class:
    Code (CSharp):
    1. abstract class Command : MonoBehaviour
    2. {
    3.     public abstract void Execute();
    4. }
    5.  
    6. sealed class ScriptableObjectCommandAdapter : Command
    7. {
    8.     [SerializeField] ScriptableObjectCommand command;
    9.  
    10.     public override void Execute() => command.Execute();
    11. }
    12.  
    13. abstract class PlainClassCommandAdapter<TCommand> : Command where TCommand : ICommand
    14. {
    15.     [SerializeField] TCommand command;
    16.  
    17.     public override void Execute() => command.Execute();
    18. }
     
    Last edited: May 15, 2024
    CodeSmile, Bunny83 and spiney199 like this.
  6. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    7,010
    Your original message included this statement:
    I just want to warn of creating many small assemblies. This can quickly skyrocket your domain reload times if you have several dozen if not hundreds of asmdefs. It will also make managing dependencies a pain.

    For example, if you find yourself adding the same ten "subsystem" dependencies to most other assemblies, that would indicate that those ten subsystems should be in a single assembly.
     
    Bunny83 and spiney199 like this.