├── .gitignore ├── Assets └── _Project │ └── Scripts │ ├── SerializeInterface.meta │ └── SerializeInterface │ ├── Editor.meta │ ├── Editor │ ├── InterfaceReferenceDrawer.cs │ ├── InterfaceReferenceDrawer.cs.meta │ ├── InterfaceReferenceUtil.cs │ ├── InterfaceReferenceUtil.cs.meta │ ├── RequireInterfaceDrawer.cs │ └── RequireInterfaceDrawer.cs.meta │ ├── InterfaceReference.cs │ ├── InterfaceReference.cs.meta │ ├── RequireInterfaceAttribute.cs │ └── RequireInterfaceAttribute.cs.meta ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uildFullScreen/ 10 | /[Bb]uilds/ 11 | /[Ll]ogs/ 12 | /[Uu]ser[Ss]ettings/ 13 | /CCDBuildData/ 14 | 15 | # Ignore everything under Assets except the _Project folder 16 | Assets/* 17 | !Assets/_Project/ 18 | 19 | # MemoryCaptures can get excessive in size. 20 | # They also could contain extremely sensitive data 21 | /[Mm]emoryCaptures/ 22 | 23 | # Recordings can get excessive in size 24 | /[Rr]ecordings/ 25 | 26 | # Uncomment this line if you wish to ignore the asset store tools plugin 27 | # /[Aa]ssets/AssetStoreTools* 28 | 29 | # Autogenerated Jetbrains Rider plugin 30 | /[Aa]ssets/Plugins/Editor/JetBrains* 31 | 32 | # Visual Studio cache directory 33 | .vs/ 34 | 35 | # Gradle cache directory 36 | .gradle/ 37 | 38 | # Autogenerated VS/MD/Consulo solution and project files 39 | ExportedObj/ 40 | .consulo/ 41 | *.csproj 42 | *.unityproj 43 | *.sln 44 | *.suo 45 | *.tmp 46 | *.user 47 | *.userprefs 48 | *.pidb 49 | *.booproj 50 | *.svd 51 | *.pdb 52 | *.mdb 53 | *.opendb 54 | *.VC.db 55 | 56 | # Unity3D generated meta files 57 | *.pidb.meta 58 | *.pdb.meta 59 | *.mdb.meta 60 | 61 | # Unity3D generated file on crash reports 62 | sysinfo.txt 63 | 64 | # Builds 65 | *.apk 66 | *.aab 67 | *.unitypackage 68 | *.app 69 | 70 | # Crashlytics generated file 71 | crashlytics-build.properties 72 | 73 | # Packed Addressables 74 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* 75 | 76 | # Temporary auto-generated Android Assets 77 | /[Aa]ssets/[Ss]treamingAssets/aa.meta 78 | /[Aa]ssets/[Ss]treamingAssets/aa/* 79 | 80 | # Custom 81 | Assets/SceneDependencyCache* 82 | Assets/NetCodeGenerated* 83 | .idea/ 84 | .DS_Store 85 | RiderScriptEditorPersistedState.asset 86 | Packages/packages-lock.json 87 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/SerializeInterface.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a19b1cabecc81f445aa531fdd9f1ef67 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/SerializeInterface/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: db9645eb16b04560a2008e1ed11802ca 3 | timeCreated: 1730524208 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/SerializeInterface/Editor/InterfaceReferenceDrawer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using Object = UnityEngine.Object; 8 | 9 | [CustomPropertyDrawer(typeof(InterfaceReference<>))] 10 | [CustomPropertyDrawer(typeof(InterfaceReference<,>))] 11 | public class InterfaceReferenceDrawer : PropertyDrawer { 12 | const string UnderlyingValueFieldName = "underlyingValue"; 13 | 14 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { 15 | var underlyingProperty = property.FindPropertyRelative(UnderlyingValueFieldName); 16 | var args = GetArguments(fieldInfo); 17 | 18 | EditorGUI.BeginProperty(position, label, property); 19 | 20 | var assignedObject = EditorGUI.ObjectField(position, label, underlyingProperty.objectReferenceValue, args.ObjectType, true); 21 | 22 | if (assignedObject != null) { 23 | Object component = null; 24 | 25 | if (assignedObject is GameObject gameObject) { 26 | component = gameObject.GetComponent(args.InterfaceType); 27 | } else if (args.InterfaceType.IsAssignableFrom(assignedObject.GetType())) { 28 | component = assignedObject; 29 | } 30 | 31 | if (component != null) { 32 | ValidateAndAssignObject(underlyingProperty, component, component.name, args.InterfaceType.Name); 33 | } else { 34 | Debug.LogWarning($"Assigned object does not implement required interface '{args.InterfaceType.Name}'."); 35 | underlyingProperty.objectReferenceValue = null; 36 | } 37 | } else { 38 | underlyingProperty.objectReferenceValue = null; 39 | } 40 | 41 | 42 | EditorGUI.EndProperty(); 43 | InterfaceReferenceUtil.OnGUI(position, underlyingProperty, label, args); 44 | } 45 | 46 | static InterfaceArgs GetArguments(FieldInfo fieldInfo) { 47 | Type objectType = null, interfaceType = null; 48 | Type fieldType = fieldInfo.FieldType; 49 | 50 | bool TryGetTypesFromInterfaceReference(Type type, out Type objType, out Type intfType) { 51 | objType = intfType = null; 52 | 53 | if (type?.IsGenericType != true) return false; 54 | 55 | var genericType = type.GetGenericTypeDefinition(); 56 | if (genericType == typeof(InterfaceReference<>)) type = type.BaseType; 57 | 58 | if (type?.GetGenericTypeDefinition() == typeof(InterfaceReference<,>)) { 59 | var types = type.GetGenericArguments(); 60 | intfType = types[0]; 61 | objType = types[1]; 62 | return true; 63 | } 64 | 65 | return false; 66 | } 67 | 68 | void GetTypesFromList(Type type, out Type objType, out Type intfType) { 69 | objType = intfType = null; 70 | 71 | var listInterface = type.GetInterfaces() 72 | .FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IList<>)); 73 | 74 | if (listInterface != null) { 75 | var elementType = listInterface.GetGenericArguments()[0]; 76 | TryGetTypesFromInterfaceReference(elementType, out objType, out intfType); 77 | } 78 | } 79 | 80 | if (!TryGetTypesFromInterfaceReference(fieldType, out objectType, out interfaceType)) { 81 | GetTypesFromList(fieldType, out objectType, out interfaceType); 82 | } 83 | 84 | return new InterfaceArgs(objectType, interfaceType); 85 | } 86 | 87 | static void ValidateAndAssignObject(SerializedProperty property, Object targetObject, string componentNameOrType, string interfaceName = null) { 88 | if (targetObject != null) { 89 | property.objectReferenceValue = targetObject; 90 | } else { 91 | var message = interfaceName != null 92 | ? $"GameObject '{componentNameOrType}'" 93 | : "assigned object"; 94 | 95 | Debug.LogWarning( 96 | $"The {message} does not have a component that implements '{interfaceName}'." 97 | ); 98 | property.objectReferenceValue = null; 99 | } 100 | } 101 | } 102 | 103 | public struct InterfaceArgs { 104 | public readonly Type ObjectType; 105 | public readonly Type InterfaceType; 106 | 107 | public InterfaceArgs(Type objectType, Type interfaceType) { 108 | Debug.Assert(typeof(Object).IsAssignableFrom(objectType), $"{nameof(objectType)} needs to be of Type {typeof(Object)}."); 109 | Debug.Assert(interfaceType.IsInterface, $"{nameof(interfaceType)} needs to be an interface."); 110 | 111 | ObjectType = objectType; 112 | InterfaceType = interfaceType; 113 | } 114 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/SerializeInterface/Editor/InterfaceReferenceDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3e21f6b7c3d34c41a2f371dc7caa8042 3 | timeCreated: 1730531766 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/SerializeInterface/Editor/InterfaceReferenceUtil.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | public class InterfaceReferenceUtil { 5 | static GUIStyle labelStyle; 6 | 7 | public static void OnGUI(Rect position, SerializedProperty property, GUIContent label, InterfaceArgs args) { 8 | InitializeStyleIfNeeded(); 9 | 10 | var controlID = GUIUtility.GetControlID(FocusType.Passive) - 1; 11 | var isHovering = position.Contains(Event.current.mousePosition); 12 | var displayString = property.objectReferenceValue == null || isHovering ? $"({args.InterfaceType.Name})" : "*"; 13 | DrawInterfaceNameLabel(position, displayString, controlID); 14 | } 15 | 16 | static void DrawInterfaceNameLabel(Rect position, string displayString, int controlID) { 17 | if (Event.current.type == EventType.Repaint) { 18 | const int additionalLeftWidth = 3; 19 | const int verticalIndent = 1; 20 | 21 | var content = EditorGUIUtility.TrTextContent(displayString); 22 | var size = labelStyle.CalcSize(content); 23 | var labelPos = position; 24 | 25 | labelPos.width = size.x + additionalLeftWidth; 26 | labelPos.x += position.width - labelPos.width - 18; 27 | labelPos.height -= verticalIndent * 2; 28 | labelPos.y += verticalIndent; 29 | labelStyle.Draw(labelPos, EditorGUIUtility.TrTextContent(displayString), controlID, DragAndDrop.activeControlID == controlID, false); 30 | } 31 | } 32 | 33 | static void InitializeStyleIfNeeded() { 34 | if (labelStyle != null) return; 35 | 36 | var style = new GUIStyle(EditorStyles.label) { 37 | font = EditorStyles.objectField.font, 38 | fontSize = EditorStyles.objectField.fontSize, 39 | fontStyle = EditorStyles.objectField.fontStyle, 40 | alignment = TextAnchor.MiddleRight, 41 | padding = new RectOffset(0, 2, 0, 0) 42 | }; 43 | labelStyle = style; 44 | } 45 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/SerializeInterface/Editor/InterfaceReferenceUtil.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 85e1cf936e904f0b855beefcf9651ca8 3 | timeCreated: 1730535854 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/SerializeInterface/Editor/RequireInterfaceDrawer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using Object = UnityEngine.Object; 6 | 7 | [CustomPropertyDrawer(typeof(RequireInterfaceAttribute))] 8 | public class RequireInterfaceDrawer : PropertyDrawer { 9 | RequireInterfaceAttribute RequireInterfaceAttribute => (RequireInterfaceAttribute)attribute; 10 | 11 | public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { 12 | Type requiredInterfaceType = RequireInterfaceAttribute.InterfaceType; 13 | EditorGUI.BeginProperty(position, label, property); 14 | 15 | if (property.isArray && property.propertyType == SerializedPropertyType.Generic) { 16 | DrawArrayField(position, property, label, requiredInterfaceType); 17 | } else { 18 | DrawInterfaceObjectField(position, property, label, requiredInterfaceType); 19 | } 20 | 21 | EditorGUI.EndProperty(); 22 | var args = new InterfaceArgs(GetTypeOrElementType(fieldInfo.FieldType), requiredInterfaceType); 23 | InterfaceReferenceUtil.OnGUI(position, property, label, args); 24 | } 25 | 26 | void DrawArrayField(Rect position, SerializedProperty property, GUIContent label, Type interfaceType) { 27 | property.arraySize = EditorGUI.IntField(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight), 28 | label.text + " Size", property.arraySize); 29 | 30 | float yOffset = EditorGUIUtility.singleLineHeight; 31 | for (int i = 0; i < property.arraySize; i++) { 32 | var element = property.GetArrayElementAtIndex(i); 33 | var elementRect = new Rect(position.x, position.y + yOffset, position.width, EditorGUIUtility.singleLineHeight); 34 | DrawInterfaceObjectField(elementRect, element, new GUIContent($"Element {i}"), interfaceType); 35 | yOffset += EditorGUIUtility.singleLineHeight; 36 | } 37 | } 38 | 39 | void DrawInterfaceObjectField(Rect position, SerializedProperty property, GUIContent label, Type interfaceType) { 40 | var oldReference = property.objectReferenceValue; 41 | Type baseType = GetAssignableBaseType(fieldInfo.FieldType, interfaceType); 42 | var newReference = EditorGUI.ObjectField(position, label, oldReference, baseType, true); 43 | 44 | if (newReference != null && newReference != oldReference) { 45 | ValidateAndAssignObject(property, newReference, interfaceType); 46 | } else if (newReference == null) { 47 | property.objectReferenceValue = null; 48 | } 49 | } 50 | 51 | Type GetAssignableBaseType(Type fieldType, Type interfaceType) { 52 | Type elementType = fieldType.IsArray ? fieldType.GetElementType() : 53 | fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>) 54 | ? fieldType.GetGenericArguments()[0] 55 | : fieldType; 56 | 57 | if (interfaceType.IsAssignableFrom(elementType)) return elementType; 58 | 59 | if (typeof(ScriptableObject).IsAssignableFrom(elementType)) return typeof(ScriptableObject); 60 | if (typeof(MonoBehaviour).IsAssignableFrom(elementType)) return typeof(MonoBehaviour); 61 | 62 | return typeof(Object); 63 | } 64 | 65 | void ValidateAndAssignObject(SerializedProperty property, Object newReference, Type interfaceType) { 66 | if (newReference is GameObject gameObject) { 67 | var component = gameObject.GetComponent(interfaceType); 68 | if (component != null) { 69 | property.objectReferenceValue = component; 70 | return; 71 | } 72 | } else if (interfaceType.IsAssignableFrom(newReference.GetType())) { 73 | property.objectReferenceValue = newReference; 74 | return; 75 | } 76 | 77 | Debug.LogWarning($"The assigned object does not implement '{interfaceType.Name}'."); 78 | property.objectReferenceValue = null; 79 | } 80 | 81 | Type GetTypeOrElementType(Type type) { 82 | if (type.IsArray) return type.GetElementType(); 83 | if (type.IsGenericType) return type.GetGenericArguments()[0]; 84 | return type; 85 | } 86 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/SerializeInterface/Editor/RequireInterfaceDrawer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: dc3d9f75c9934724ae835d811fdb57fd 3 | timeCreated: 1730537152 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/SerializeInterface/InterfaceReference.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using Object = UnityEngine.Object; 4 | 5 | [Serializable] 6 | public class InterfaceReference where TObject : Object where TInterface : class { 7 | [SerializeField, HideInInspector] TObject underlyingValue; 8 | 9 | public TInterface Value { 10 | get => underlyingValue switch { 11 | null => null, 12 | TInterface @interface => @interface, 13 | _ => throw new InvalidOperationException($"{underlyingValue} needs to implement interface {nameof(TInterface)}.") 14 | }; 15 | set => underlyingValue = value switch { 16 | null => null, 17 | TObject newValue => newValue, 18 | _ => throw new ArgumentException($"{value} needs to be of type {typeof(TObject)}.", string.Empty) 19 | }; 20 | } 21 | 22 | public TObject UnderlyingValue { 23 | get => underlyingValue; 24 | set => underlyingValue = value; 25 | } 26 | 27 | public InterfaceReference() { } 28 | 29 | public InterfaceReference(TObject target) => underlyingValue = target; 30 | 31 | public InterfaceReference(TInterface @interface) => underlyingValue = @interface as TObject; 32 | 33 | public static implicit operator TInterface(InterfaceReference obj) => obj.Value; 34 | } 35 | 36 | [Serializable] 37 | public class InterfaceReference : InterfaceReference where TInterface : class { } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/SerializeInterface/InterfaceReference.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8addd759cca147e3924f0617524f38b1 3 | timeCreated: 1730524219 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/SerializeInterface/RequireInterfaceAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | [AttributeUsage(AttributeTargets.Field)] 5 | public class RequireInterfaceAttribute : PropertyAttribute { 6 | public readonly Type InterfaceType; 7 | 8 | public RequireInterfaceAttribute(Type interfaceType) { 9 | Debug.Assert(interfaceType.IsInterface, $"{nameof(interfaceType)} needs to be an interface."); 10 | InterfaceType = interfaceType; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/SerializeInterface/RequireInterfaceAttribute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 96db24d86bcd4cf9b01487628507fe5f 3 | timeCreated: 1730523937 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity Seriazable Interfaces 2 | ![SerializedInterface](https://github.com/user-attachments/assets/0006465a-afda-4517-878b-2ce57e933c51) 3 | 4 | Serializable Interfaces in Unity give you the ability to drag, drop, and serialize objects that implement a specific interface in Inspector fields, enabling powerful, type-safe assignments. 5 | 6 | Using a custom attribute, you can restrict assignments to components implementing a particular interface. Or use an open generic type that supports serialization of references, enforcing both base types and interface types. This setup leverages custom editor tooling and reflection for seamless Inspector integration, enhanced validation, and intuitive drag-and-drop functionality. 7 | 8 | 9 | Don't want to serialze yourself but just want the inline hints and drag/drop Inspector framework? Also compatible with the free version of Odin Serializer. 10 | 11 | ## Features 12 | 13 | - **Serializable Interfaces**: Serialize objects that implement a specific interface in Unity Inspector fields. 14 | - **Require Interface Attribute**: Restrict assignments to components implementing a particular interface. 15 | - **Drag & Drop**: Drag and drop objects that implement a specific interface in Inspector fields. 16 | - **Hints**: Inline hints to show the interface type for any field. 17 | 18 | ## YouTube 19 | 20 | [**Watch the full video on YouTube**](https://youtu.be/xcGPr04Mgm4) 21 | 22 | You can also check out my [YouTube channel](https://www.youtube.com/@git-amend?sub_confirmation=1) for more Unity content. 23 | --------------------------------------------------------------------------------