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

Creating assignable C# interface fields in the inspector

Discussion in 'Scripting' started by Philip Nelson, Feb 13, 2013.

  1. Philip Nelson

    Philip Nelson

    Joined:
    May 28, 2012
    Posts:
    20
    I'm trying to make C# interfaces work as a field that can be assigned in the inspector, using a custom editor script. I found an old topic that showed how it might work, but the method given doesn't work now.

    Here's what I have (in Assets/Editor):

    Code (csharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3.  
    4. [CustomEditor(typeof(Button))]
    5.  
    6. public class ButtonActionInspector : UnityEditor.Editor
    7. {
    8.     public override void OnInspectorGUI()
    9.     {
    10.         var script = (Button)target;
    11.         var obj = EditorGUILayout.ObjectField("ButtonAction", (Object)script.ButtonAction, typeof(IButtonAction), true);
    12.  
    13.         script.ButtonAction = obj as IButtonAction;
    14.         DrawDefaultInspector();
    15.     }
    16. }
    The result is that I can indeed assign a script component of type IButtonAction to the Button target in the inspector. When I hit play, however, all ButtonAction fields on all Button components are reset to None, which of course breaks everything.

    I haven't figured a way around this. Any ideas? I'm running Unity 4.0.1f2.

    I should add I see this warning in the console for every assignment that disappears:

     
    Last edited: Feb 13, 2013
    kamran-bigdely likes this.
  2. PaulR

    PaulR

    Joined:
    Nov 14, 2012
    Posts:
    43
  3. Philip Nelson

    Philip Nelson

    Joined:
    May 28, 2012
    Posts:
    20
    Thanks! That certainly describes the problem. Adding the [Serializable] attribute to the classes isn't helping, though.

    One difference I notice is that my classes are already inheriting from MonoBehavior. None of the classes in the examples do.
     
  4. Atin Skrita

    Atin Skrita

    Joined:
    Feb 18, 2011
    Posts:
    94
    Because the interface is not serialized by Unity, none of its members will be serialized. I would try adding [SerializeField] to the fields that are 'behind' the interface's properties.

    Code (csharp):
    1.  
    2. class ButtonAction : IButtonAction
    3. {
    4.    //Needs to be serialized with [SerializeField]
    5.    [SerializeField]
    6.    private SomeType someField;
    7.  
    8.    //Implements IButtonAction
    9.    public SomeType SomeProperty
    10.    {
    11.        get{return someField;}
    12.    }
    13. }
    14.  
    I think that is your problem.

    ~Atin
     
  5. Philip Nelson

    Philip Nelson

    Joined:
    May 28, 2012
    Posts:
    20
    Thanks for the suggestion! Doesn't seem to be helping, though maybe I'm not implementing it properly. The interface has a method, not fields, by the way. Here's the interface itself:

    Code (csharp):
    1. public interface IButtonAction
    2. {
    3.     void ButtonAction(Button button);
    4. }
    Here's the Button script, which has the IButtonAction member I'm trying to assign in the inspector:

    Code (csharp):
    1. [Serializable]
    2. public class Button : MonoBehaviour
    3. {
    4.     public MeshRenderer TextRenderer;
    5.     public Color EnabledColor;
    6.     public Color DisabledColor;
    7.  
    8.     [SerializeField]
    9.     public IButtonAction ButtonAction;
    10.  
    11.     private void OnMouseDown()
    12.     {
    13.         OnActivate();
    14.     }
    15.  
    16.     public void OnActivate()
    17.     {
    18.         ButtonAction.ButtonAction(this);
    19.     }
    20.  
    21.     public void DisableButton()
    22.     {
    23.         TextRenderer.material.color = DisabledColor;
    24.     }
    25.  
    26.     public void EnableButton()
    27.     {
    28.         TextRenderer.material.color = EnabledColor;
    29.     }
    30. }
    And here's one of the classes implementing IButtonAction, that I'm trying to assign to the Button class in the inspector:

    Code (csharp):
    1. [Serializable]
    2. public class TileButtonAction : MonoBehaviour, IButtonAction
    3. {
    4.     public MapController MapController;
    5.  
    6.     public void ButtonAction(Button button)
    7.     {
    8.         MapController.MoveMap(new Vector3(transform.position.x * -1 + MapController.transform.position.x,
    9.                                         transform.position.y * -1 + MapController.transform.position.y));
    10.     }
    11. }
    I should add that both the Button and TileButtonAction classes are attached as components to the same object in the scene.
     
  6. Philip Nelson

    Philip Nelson

    Joined:
    May 28, 2012
    Posts:
    20
    FYI, for anyone who may have the same problem, I was able to work around it by using an abstract class that inherits from MonoBehavior:

    Code (csharp):
    1. using UnityEngine;
    2.  
    3. public abstract class ButtonAction : MonoBehaviour
    4. {
    5.     public abstract void Activate(Button button);
    6. }
    I was able to have a component with a ButtonAction field exposed in the inspector, and then assign various classes implementing the ButtonAction abstract class to that field, with no special editor class required and no trouble with serialization.
     
  7. Roland1234

    Roland1234

    Joined:
    Nov 21, 2012
    Posts:
    190
    I've just finished releasing an asset to the store that allows you to do what you wanted without relying on abstract classes.

    It uses a generic container with a custom property drawer that enables it to be assignable from the inspector and is serialized, all you have to do is subclass the container like so:
    Code (csharp):
    1.  
    2. [System.Serializable]
    3. public class ButtonAction : IUnifiedContainer<IButtonAction> { }
    4.  
    and then you can simply have:
    Code (csharp):
    1.  
    2. public class MyScript : MonoBehaviour
    3. {
    4.     public ButtonAction Button;
    5. }
    6.  
    Which will automatically be rendered/assignable from the default inspector without having to create a custom editor. You access the actual interface from the .Result property of the container, or wrap a property around it like so:
    Code (csharp):
    1.  
    2. public class MyScript : MonoBehaviour
    3. {
    4.     public IButtonAction Button
    5.     {
    6.         get { return ButtonField.Result }
    7.         set { ButtonField.Result = value; }
    8.     }
    9.  
    10.     [SerializeField]
    11.     private ButtonAction ButtonField;
    12. }
    13.  
    To have a field you can assign from the editor and access from code without having to deal with the container at all. More info here if anyone is interested.
     
  8. Marrt

    Marrt

    Joined:
    Feb 7, 2012
    Posts:
    613
    Old thread but this is how i worked around it: I created an abstract class that overwrites Monobehavior AND implements the interface i needed. So if i want to slot in a Monobehavior as the needed interface, i just derive from my overwritten class that implements the interface, this is the pattern:

    Code (CSharp):
    1. #region Serializable Interface Pattern
    2. [System.Serializable]
    3. public    abstract    class MyInterfaceLikeMonobehavior : MonoBehaviour, IMyInterface{
    4.     //abstract class implements the interface
    5.     abstract  public bool        GetMybool();
    6. }
    7.  
    8. public    interface IMyInterface{
    9.     bool    GetMybool();
    10. }
    11.  
    12. //Now you can assign Monobehaviours that derive from MyInterfaceLikeMonobehavior in inspector
    13. // This will be exposed in Inspector: public    MyInterfaceLikeMonobehavior    myMonoBehavior;
    14. // instead of
    15. //    'public class MyComponent : MonoBehaviour {...'
    16. //    use
    17. //    'public class MyComponent : MyInterfaceLikeMonobehavior {...'
    18. // and you can slot it into myMonoBehavior
    19.  
    20. #endregion Serializable Interface Pattern
    Only Downside: When you create classes that implement multiple interfaces in different permutations you will need to create a specific abstract overwriting class for each permutation

    Code (CSharp):
    1. #region Serializable Interface Pattern
    2.  
    3. [System.Serializable]
    4. public    abstract    class ContextTarget : MonoBehaviour, IContextTarget{
    5.     abstract  public ActivationType        GetActivationType();
    6.     abstract  public Confirmation        TryContextAction();
    7.  
    8.     public enum ActivationType        { KeyDown, KeyUp, Hold }
    9.     public enum Confirmation        { Failed, Interacted, Collected }
    10. }
    11.  
    12. public    interface IContextTarget{
    13.     ContextTarget.ActivationType    GetActivationType();
    14.     ContextTarget.Confirmation        TryContextAction();
    15. }
    16.  
    17. #endregion Serializable Interface Pattern
    Now, when i want a new Monobehavior to be slotable as IContextTarget:

    Code (CSharp):
    1. public class ActivationTarget : ContextTarget {
    2.  
    3.     public                ActivationType            contextActivationType = ActivationType.KeyDown;
    4.     public    override    ActivationType            GetActivationType(){    return contextActivationType;    }
    5.  
    6.     public    override    Confirmation        TryContextAction(){
    7.         return Confirmation.Interacted;
    8.     }
    9. }
     
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    Or you could use a PropertyDrawer.

    Basically create a PropertyAttribute that takes in a restriction type for the field you want. Create a PropertyDrawer for that which restricts the object field to something of that type.

    The down side is in the class you'll have to type your field to 'UnityEngine.Object' (or component if you only want to allow components). But you can set that private and wrap it with a property that casts for you.

    So:
    PropertyAttribute:
    Code (csharp):
    1.  
    2. public class TypeRestrictionAttribute : PropertyAttribute
    3. {
    4.  
    5.     public System.Type type;
    6.     public bool allowSceneObjects = true;
    7.  
    8.     public TypeRestrictionAttribute(System.Type tp)
    9.     {
    10.         if (tp == null) throw new System.ArgumentNullException("tp");
    11.         this.type = tp;
    12.     }
    13.  
    14. }
    15.  
    PropertyDrawer:
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using UnityEditor;
    4. using System.Collections.Generic;
    5.  
    6. [CustomPropertyDrawer(typeof(TypeRestrictionAttribute))]
    7. public class TypeRestrictionPropertyDrawer : PropertyDrawer
    8. {
    9.  
    10.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    11.     {
    12.         //validate inputs - could draw a 'fail' message if not validated correctly
    13.         if (property.propertyType != SerializedPropertyType.ObjectReference) return;
    14.  
    15.         var attrib = this.attribute as TypeRestrictionAttribute;
    16.         if (attrib == null) return;
    17.        
    18.         //do draw
    19.         EditorGUI.BeginChangeCheck();
    20.         UnityEngine.Object obj = EditorGUI.ObjectField(position, label, property.objectReferenceValue, typeof(UnityEngine.Object), attrib.allowSceneObjects);
    21.         if(EditorGUI.EndChangeCheck())
    22.         {
    23.             if(obj != null)
    24.             {
    25.                 var tp = obj.GetType();
    26.                 if (!attrib.type.IsAssignableFrom(tp))
    27.                 {
    28.                     if(obj is GameObject)
    29.                     {
    30.                         obj = (obj as GameObject).GetComponent(attrib.type);
    31.                     }
    32.                     else if(obj is Component)
    33.                     {
    34.                         obj = (obj as Component).gameObject.GetComponent(attrib.type);
    35.                     }
    36.                     else
    37.                     {
    38.                         obj = null;
    39.                     }
    40.                 }
    41.             }
    42.             property.objectReferenceValue = obj;
    43.         }
    44.     }
    45.  
    46. }
    47.  
    Then usage:
    Code (csharp):
    1.  
    2. public class zTest01 : MonoBehaviour {
    3.  
    4.     [SerializeField]
    5.     [TypeRestriction(typeof(IExampleInterface))]
    6.     private UnityEngine.Object _obj;
    7.    
    8.     public IExampleInterface Obj
    9.     {
    10.         get { return _obj as IExampleInterface; }
    11.         set { _obj = value as UnityEngine.Object; }
    12.     }
    13.  
    14. }
    15.  
     
    melsov likes this.
  10. koirat

    koirat

    Joined:
    Jul 7, 2012
    Posts:
    2,068
    I'm going to necro a little and ask about safety of using this interfaces in inspector.

    Long time ago I was thinking about starting to use some interfaces in inspector (usually to simulate multiple inheritance), but was always to scared of breaking my project in near future.
    Also there is a question why unity is not supporting it by default, is there a possible backlash ?

    So for example lets assume I decided to use method above, and later I change the interface type for some other, will my obj still be stored inside "obj" variable, even when it will not implement this new interface ?
     
  11. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    Yes, pre-existing serialized entries will still point at the object that no longer implements that interface. Because the actual serialized field is really only concerned about if it's a UnityEngine.Object. Of course when you access the property, since it uses the 'as' operator, it'll end up returning null even though the underlying private field is pointing at the mismatched object.

    Also, take into consideration this same problem arises if you typed it as a concrete class. For example if you have a field accepting 'Renderer', and then you change it to say 'Transform' you end up in a similar situation. The serialized data still points at the Renderer. And in the inspector you'll see this:
    upload_2022-5-23_19-11-34.png

    This is just incidental to the entire scenario because the serialization system really only has a guid underneath that it looks up the target with.

    Back at the interface example I posted though, as long as you never actually access the private field, and instead always access via the property. You're safe. It'll just come back as null. There's no crazy exception that'll crash the system. It just returns null since the target is not the type expected.

    Yes, this is in itself an issue. But it's an expected issue. You can't go willy nilly changing serialized field types without some consequence. As demonstrated that even the built in unity types have similar flaws.

    ...

    As to why Unity hasn't added support out right. Well it's hard to say exactly why.

    But a big part I'm willing to bet is that there is no guarantee that the target object inherits from UnityEngine.Object. Only those objects which inherit from that are serialized by reference to the object rather than as a value (I'm not getting into SerializeReference at this point which doesn't support UnityEngine.Object at all). This creates a strange duality about the field since it technically can be set to objects that exist in both the set of types inherited from UnityEngine.Object and those not. And there's no simple C# syntax that can restrict it otherwise.

    In the end... in C# how you would make that restriction is well... precisely what I've shown above.

    With that said, I'm willing to bet the people at Unity have had this discussion to some extent and the biggest sticking point is just determining the best way to uncover such functionality for its userbase. Unity tends to like very simple designs that are easy to understand, and coming up with one isn't exactly straightforward without some boilerplate (like I have here). And if there's boilerplate, why not just have the user do it themselves. Especially since those who are going to want to leverage such a design are also skilled enough to be able to handle said boilerplate on their own.

    But I will not be surprised if in the coming years we get this feature at some point. It happens all the time. The community develops their own workarounds and workflows and then Unity adopts them not much later. I remember I had my own custom yield instruction, my own version of UnityEvent, and more before Unity came along with their own. And that's awesome they do. And I'm not surprised by the timeline for them either... developing features like this, even something that seems as mundane as the topic of this thread, doesn't usually get done overnight in the setting that is Unity.

    Or rather... I hope it doesn't. None of the places I've ever worked would.

    And I get the sneaking suspicion Unity USED to do it that way (as many young startups do), and they've since learned their lesson. Hence the backtracking and hemming and hawing of pre-existing aspects of the API (I'm looking at you == null).
     
    koirat and Bunny83 like this.