- Home /
Unity won't even let me draw my interface property drawer? WTF?!?! Y U No support interfaces????
I was hoping I'd get to write a property drawer that would allow me to assign interface fields from the inspector.
SerializeInterfaceAttribute.cs
using UnityEngine;
public class SerializeInterfaceAttribute : PropertyAttribute
{
private System.Type type;
public System.Type Type { get { return type; } }
public SerializeInterfaceAttribute(System.Type type)
{
this.type = type;
}
}
SerializeInterfaceDrawer.cs
using UnityEngine;
using UnityEditor;
using System.Linq;
[CustomPropertyDrawer(typeof(SerializeInterfaceAttribute))]
public class SerializeInterfaceDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// get the type
var sid = attribute as SerializeInterfaceAttribute;
System.Type iType = sid.Type;
// find all the implementors
string[] dlls = Utils.GetProjectDlls(d => !d.Contains("Editor"));
System.Type[] validTypes = iType.GetAllConcreteChildren(dlls);
// make a MB object field
EditorGUI.BeginChangeCheck();
MonoBehaviour field = EditorGUI.ObjectField(position, iType.ToString(), property.objectReferenceValue, typeof(MonoBehaviour), true) as MonoBehaviour;
if (EditorGUI.EndChangeCheck()) {
// if a proper type is not assigned throw an exception
if (!field.GetType().IsSubclassOf(iType)) {
throw new UnityException("Invalid type assignment. Type must be of " + iType);
}
property.objectReferenceValue = field;
}
}
}
Utils.cs
public static class Utils
{
public static string[] GetProjectDlls(Func<string, bool> predicate)
{
string[] dlls = Directory.GetFiles("Library\\ScriptAssemblies", "*.dll");
List<string> validDlls = new List<string>();
foreach(var dll in dlls)
if (predicate(dll))
validDlls.Add(dll);
return validDlls.ToArray();
}
}
OtherExtensions.cs
public static class OtherExtensions
{
public static Type[] GetAllConcreteChildren(this Type type, Assembly asm = null)
{
if (asm == null) asm = Assembly.GetExecutingAssembly();
var types = asm.GetTypes();
return types.Where(t => t.IsSubclassOf(type) && !t.IsAbstract).ToArray();
}
public static Type[] GetAllConcreteChildren(this Type type, string[] dlls)
{
List<Type> allTypes = new List<Type>();
foreach (var dll in dlls) {
var types = type.GetAllConcreteChildren(Assembly.LoadFile(dll));
foreach (var t in types) {
allTypes.Add(t);
}
}
return allTypes.ToArray();
}
}
Usage:
(better to be placed in separate files)
public interface IMyInterface { }
public class A : MonoBehaviour, IMyInterface { }
public class B : MonoBehaviour, IMyInterface { }
public class TestMB : MonoBehaviour
{
[SerializeInterface(typeof(IMyInterface))
IMyInterface iField;
}
What should happen, is that you'll get a MB object field for iField
, if you assign a MB that is not of type IMyInterface
, an exception will get thrown, otherwise the value will be assigned.
But guess what?!
The OnGUI
of the drawer ISN'T EVEN EXECUTING!
No debug logs, no breakpoints, it just won't budge!
If you notice, I could pass any type, not just an interface, so if we modify our code a bit:
public abstract class Abstract : MonoBehaviour { }
public class A : Abstract { }
public class B : Abstract { }
public class TestMB : MonoBehaviour
{
[SerializeInterface(typeof(Abstract))
Abstract iField;
}
It will actually work as expected.
I have no words to express, other than the Unity serialization is retarded!
I made this attempt cause I saw this which kind of encouraged me to try to make my own way to get an interface field visible somehow...
I wonder how he did it.
Anyway, question is, is there possibly any way to just, get the drawer OnGUI
to execute? and why isn't it getting executed? And, if that won't work, how is it possible to do what I want, in a modular, reusable and plug-n-play manner?
Thanks for any help.
Answer by Roland1234 · Dec 26, 2013 at 06:59 PM
Hi vexe, I got your e-mail - thanks for linking to my thread, I appreciate it.
Taking a quick look at your code and narrowing it down to your specific question regarding why your property drawer's OnGUI method isn't even getting executed I can tell you that, as you've likely deduced yourself already, Unity simply does not serialize interfaces or fields of an interface type (even explicitly decorating the field with the [SerializeField] attribute will do nothing). Unity will serialize primitives, certain Unity-defined derived types (like Component, MonoBehaviour, ScriptableObject, etc), as well as classes decorated with the [Serializable] attribute, and maybe some others types I'm not aware of, but that's basically it. And if Unity doesn't serialize something, then as far as the editor is concerned it almost doesn't exist - property drawers will not get invoked for it and even if you could somehow manage it, any assignment made to a non-serialized field will be lost between sessions and entering/exiting play mode.
However, we do have enough to come up with a generic solution to expose assignable (and serialized) "interface fields" in the inspector if you are willing to put in some work and dance within some Unity-enforced constraints. I'll describe to you the approach I've taken in my asset, maybe it'll help you or someone else figure something better out in the future (although I honestly think this is as good as it gets the way Unity currently does things):
Basically, what I have is an abstract generic container class which contains a Component (which MonoBehaviours end up deriving from) field and a property of the generic type which will cast the Component to the generic type when accessed (casting only once and remembering the result during runtime). The reason for making it abstract is to require a class to derive from it since raw generics are not serialized by Unity (I know, right?) but if you explicitly resolve the generic types in a deriving class decorated with the [Serializable] attribute, then Unity will indeed serialize it. So that gives us the core functionality.
Now that we have something Unity can serialize we can write a property drawer for it that will get invoked and pretty much do whatever we want to display and assign it - just remember that Unity will only "remember" the Component field. That's the key here - we technically can't serialize an actual interface field, but if the thing implementing it is serializable, well we can serialize that and cast it to our interface when we need to via the generic property. I've also added the logic necessary to be able to assign anything that implements the interface in code, as not being able to kinda defeats the whole point of an interface, but this is limited to runtime assignment in code only. I've found this to be perfectly acceptable as the only thing I'm likely to even assign in the editor would be a MonoBehaviour anyways (how would you drag and drop anything else in the editor, right?).
So that's the basic idea, there's quite a bit more work necessary (and some that's just nice to make it easier to use - like a custom selection list showing all implementing MonoBehaviours in the scene) but it mostly presents itself logically once you have the core concept. I wouldn't mind helping out if you have more specific questions, but I would of course encourage people to simply buy the asset (it's only $5) which would give you access to the code itself.
Good coding to you!
Thanks a lot for this great reply! - I actually figured out that Unity will completely ignore anything that it doesn't serialize, when I also attempted to write a property drawer for auto-properties, same issue there.
I just wonder, if Unity could serialize an abstract class, then how come it can't serialize an interface? These two are pretty much the same:
public abstract class Character
{
public abstract string Name { get; set; }
public abstract int Id { get; }
}
public interface ICharacter
{
string Name { get; set; }
int Id { get; set; }
}
Oh sorry, I forgot, they're not the same. The interface is prefixed with "I" - And as we know, prefixing things with 'i' makes them 'cool' (but useless... like... some... consumer products out there...) With a capital "I", it's now super cool! psshh...
I read that interfaces can't be serialized cause they represent "behavior" and not "state" and also cause the confusion that comes when you deserialize, how can you tell, which type you'll deserialize to when you deserialize? - Well, same questions are asked when it comes to abstract classes.
The thing is, we're not actually serializing an abstract class/interface, how can we serialize something abstract? we can't (just like we can't instantiate an interface or an abstract class). But we're actually serializing the objects whose type implement this interface/abstract class. I just can't see how it is hard to deserialize the object, back to its state prior serializtion regardless of whether it's implementing an interface, or inherting an abstract class. From my humble knowledge, and by simply thinking about it, I don't see any reason why can't they just serialize interfaces. (Btw UD$$anonymous$$ has that feature out of the box... but the workflow there is just ahh... let's just stay here and talk about interface frustration ins$$anonymous$$d of UD$$anonymous$$, makes me feel much better...)
A question about your asset: I've read about it a little bit, but couldn't find something to answer my question, can I do stuff like this:
public interface I$$anonymous$$yInterface { }
public class A : $$anonymous$$onoBehahiour, I$$anonymous$$yInterface { }
public class B : $$anonymous$$onoBehaviour, I$$anonymous$$yInterface { }
public class Test$$anonymous$$B : $$anonymous$$onoBehaviour
{
[Interface$$anonymous$$agic]
I$$anonymous$$yInterface field;
}
I know you should be inheriting from some generic class, but you get what I'm asking for... does your asset provide 'pure' interface candy like that? i.e. 1- I could just start treating interfaces like abstract classes in terms of serializtion. 2- Interfaces will maintain their essense and beauty (the ability to do multiple inheritance, etc)
Thanks again for taking the time. Appreciate it.
even if you could somehow manage it, any assignment made to a non-serialized field will be lost between sessions and entering/exiting play mode.
I understand that. But I was hoping I'd pass the OnGUI
getting executed point, from which maybe I could get it to serialize/deserialize in a custom way via a custom serializer, somehow...
Actually, I couldn't wait for you to comment back so I went ahead and bought it, and within it, lies my answers!
Well done! I must say I'm impressed! I highly recommend it to everyone struggling with interfaces!
Gives me exactly what I was looking for. It is the solution to the interfaces problem.
Super cool custom editor.
VERY educational.
I have a few comments though:
It would be nice if you added more documentation/commenting on your methods, even if the method does something internal. I always try my best to make my code - and whatever it is I'm trying to do very clear to the reader, whom I assume is a beginner.
A bit more ease of use? The main reason I thought of mentioning this point, is how you create a list of containers. In your example, there's just too much to do in order to get it working.
_interfacesDelegate
, lambda expression, etc. All of which is pretty scary to regular consumers, it might not be to you, but it is to them. (Again, assume ALL your users are amatures, newbs, assume they know nothing. That way you write the most easy-to-use framework ever)Perhaps, add more notes or give an example, as to how one would reuse your property drawer registration feature, if it's possible?
$$anonymous$$ake a thorough video, explaining in simple non-technical words, the usage, the terms, etc. Especially the word "container". Words speak louder and clearer than text.
I'll add more suggestions if I come across something while I'm using the asset.
Thanks and well done.
Thank you very much for the review and feedback, I'm really happy to see someone has found it helpful! If you have more suggestions by all means let me know in the IUnified thread; it may take me a while to implement, but I'll be sure to at least take note and respond.