Search code examples
unity-game-enginedrop-down-menudropdownunity-editor

Showing ScriptableObjects in multi selection dropdown menu in Unity


How should I create PropertyAttribute and PropertyDrawer to show ScriptableObjects in dropdown menu in Inspector for multi selecting ?


Solution

  • I've published a repository on Github which solve this problem. It is for multi selecting in dropdown menu in Inspector.

    In Github links you have access to example folder and unitypackage in release page but if you don't want to go to the links or any problem happens to the links, you can follow this instruction:

    ScriptableObjectMultiSelectDropdown:

    ScriptableObjectMultiSelectDropdown is an attribute for the Unity Inspector. It is used for showing ScriptableObjects which are created in your project, in dropdown menu and select multiple of them in Inspector.

    ScriptableObject Multi Select Dropdown

    Code:

    ScriptableObjectReference.cs:

    // Copyright (c) ATHellboy (Alireza Tarahomi) Limited. All rights reserved.
    // Licensed under the MIT license. See LICENSE file in the project root.
    
    using System;
    using UnityEngine;
    
    namespace ScriptableObjectMultiSelectDropdown
    {
        /// <summary>
        /// Because you can't make a PropertyDrawer for arrays or generic lists themselves,
        /// I had to create parent class as an abstract layer.
        /// </summary>
        [Serializable]
        public class ScriptableObjectReference
        {
            public ScriptableObject[] values;
        }
    }
    

    ScriptableObjectMultiSelectDropdownAttribute.cs:

    // Copyright (c) ATHellboy (Alireza Tarahomi) Limited. All rights reserved.
    // Licensed under the MIT license. See LICENSE file in the project root.
    
    using System;
    using UnityEngine;
    
    namespace ScriptableObjectMultiSelectDropdown
    {
        /// <summary>
        /// Indicates how selectable scriptableObjects should be collated in drop-down menu.
        /// </summary>
        public enum ScriptableObjectGrouping
        {
            /// <summary>
            /// No grouping, just show type names in a list; for instance, "MainFolder > NestedFolder > SpecialScriptableObject".
            /// </summary>
            None,
            /// <summary>
            /// Group classes by namespace and show foldout menus for nested namespaces; for
            /// instance, "MainFolder >> NestedFolder >> SpecialScriptableObject".
            /// </summary>
            ByFolder,
            /// <summary>
            /// Group scriptableObjects by folder; for instance, "MainFolder > NestedFolder >> SpecialScriptableObject".
            /// </summary>
            ByFolderFlat
        }
    
        /// <example>
        /// <para>Usage Examples</para>
        /// <code language="csharp"><![CDATA[
        /// using UnityEngine;
        /// using ScriptableObjectDropdown;
        /// 
        /// [CreateAssetMenu(menuName = "Create Block")]
        /// public class Block : ScriptableObject
        /// {
        ///     // Some fields
        /// }
        /// 
        /// public class BlockManager : MonoBehaviour
        /// {
        ///     [ScriptableObjectMultiSelectDropdown(typeof(Block))]
        ///     public ScriptableObjectReference firstTargetBlocks;
        ///     
        ///     // or
        ///     
        ///     [ScriptableObjectMultiSelectDropdown(typeof(Block), grouping = ScriptableObjectGrouping.ByFolder)]
        ///     public ScriptableObjectReference secondTargetBlocks;
        /// }
        /// 
        /// // or
        /// 
        /// [CreateAssetMenu(menuName = "Create Block Manager Settings")]
        /// public class BlockManagerSetting : ScriptableObject
        /// {
        ///     [ScriptableObjectMultiSelectDropdown(typeof(Block))]
        ///     public ScriptableObjectReference firstTargetBlocks;
        ///     
        ///     // or
        ///     
        ///     [ScriptableObjectMultiSelectDropdown(typeof(Block), grouping = ScriptableObjectGrouping.ByFolderFlat)]
        ///     public ScriptableObjectReference secondTargetBlocks;
        /// }
        /// ]]></code>
        /// </example>
        [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
        public class ScriptableObjectMultiSelectDropdownAttribute : PropertyAttribute
        {
            public ScriptableObjectGrouping grouping = ScriptableObjectGrouping.None;
    
            private Type _baseType;
            public Type BaseType
            {
                get { return _baseType; }
                private set { _baseType = value; }
            }
    
            public ScriptableObjectMultiSelectDropdownAttribute(Type baseType)
            {
                _baseType = baseType;
            }
        }
    }
    

    Put this one in Editor folder:

    ScriptableObjectMultiSelectionDropdownDrawer.cs:

    // Copyright (c) ATHellboy (Alireza Tarahomi) Limited. All rights reserved.
    // Licensed under the MIT license. See LICENSE file in the project root.
    
    using System;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEditor;
    using System.Reflection;
    using System.Linq;
    
    namespace ScriptableObjectMultiSelectDropdown.Editor
    {
        // TODO: Mixed value (-) for selecting multi objects
        [CustomPropertyDrawer(typeof(ScriptableObjectReference))]
        [CustomPropertyDrawer(typeof(ScriptableObjectMultiSelectDropdownAttribute))]
        public class ScriptableObjectMultiSelectionDropdownDrawer : PropertyDrawer
        {
            private static ScriptableObjectMultiSelectDropdownAttribute _attribute;
            private static List<ScriptableObject> _scriptableObjects = new List<ScriptableObject>();
            private static List<ScriptableObject> _selectedScriptableObjects = new List<ScriptableObject>();
            private static readonly int _controlHint = typeof(ScriptableObjectMultiSelectDropdownAttribute).GetHashCode();
            private static GUIContent _popupContent = new GUIContent();
            private static int _selectionControlID;
            private static readonly GenericMenu.MenuFunction2 _onSelectedScriptableObject = OnSelectedScriptableObject;
            private static bool isChanged;
    
            static ScriptableObjectMultiSelectionDropdownDrawer()
            {
                EditorApplication.projectChanged += ClearCache;
            }
    
            public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
            {
                ScriptableObjectMultiSelectDropdownAttribute castedAttribute = attribute as ScriptableObjectMultiSelectDropdownAttribute;
    
                if (_scriptableObjects.Count == 0)
                {
                    GetScriptableObjects(castedAttribute);
                }
    
                Draw(position, label, property, castedAttribute);
            }
    
            public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
            {
                return EditorStyles.popup.CalcHeight(GUIContent.none, 0);
            }
    
            /// <summary>
            /// How you can get type of field which it uses PropertyAttribute
            /// </summary>
            private static Type GetPropertyType(SerializedProperty property)
            {
                Type parentType = property.serializedObject.targetObject.GetType();
                FieldInfo fieldInfo = parentType.GetField(property.propertyPath);
                if (fieldInfo != null)
                {
                    return fieldInfo.FieldType;
                }
                return null;
            }
    
            private static bool ValidateProperty(SerializedProperty property)
            {
                Type propertyType = GetPropertyType(property);
                if (propertyType == null)
                {
                    return false;
                }
                if (propertyType != typeof(ScriptableObjectReference))
                {
                    return false;
                }
                return true;
            }
    
            /// <summary>
            /// When new ScriptableObject added to the project
            /// </summary>
            private static void ClearCache()
            {
                _scriptableObjects.Clear();
            }
    
            /// <summary>
            /// Gets ScriptableObjects just when it is a first time or new ScriptableObject added to the project
            /// </summary>
            private static void GetScriptableObjects(ScriptableObjectMultiSelectDropdownAttribute attribute)
            {
                string[] guids = AssetDatabase.FindAssets(String.Format("t:{0}", attribute.BaseType));
                for (int i = 0; i < guids.Length; i++)
                {
                    _scriptableObjects.Add(AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[i]), attribute.BaseType) as ScriptableObject);
                }
            }
    
            /// <summary>
            /// Checks if the ScriptableObject is selected or not by checking if the list contains it.
            /// </summary>
            private static bool ResolveSelectedScriptableObject(ScriptableObject scriptableObject)
            {
                if (_selectedScriptableObjects == null)
                {
                    return false;
                }
                return _selectedScriptableObjects.Contains(scriptableObject);
            }
    
            private static void Draw(Rect position, GUIContent label,
                SerializedProperty property, ScriptableObjectMultiSelectDropdownAttribute attribute)
            {
                if (label != null && label != GUIContent.none)
                    position = EditorGUI.PrefixLabel(position, label);
    
                if (ValidateProperty(property))
                {
                    if (_scriptableObjects.Count != 0)
                    {
                        UpdateScriptableObjectSelectionControl(position, label, property.FindPropertyRelative("values"), attribute);
                    }
                    else
                    {
                        EditorGUI.LabelField(position, "There is no this type asset in the project");
                    }
                }
                else
                {
                    EditorGUI.LabelField(position, "Use it with ScriptableObjectReference");
                }
            }
    
            /// <summary>
            /// Iterats through the property for finding selected ScriptableObjects
            /// </summary>
            private static ScriptableObject[] Read(SerializedProperty property)
            {
                List<ScriptableObject> selectedScriptableObjects = new List<ScriptableObject>();
                SerializedProperty iterator = property.Copy();
                SerializedProperty end = iterator.GetEndProperty();
                while (!SerializedProperty.EqualContents(iterator, end) && iterator.Next(true))
                {
                    if (iterator.propertyType == SerializedPropertyType.ObjectReference)
                    {
                        selectedScriptableObjects.Add(iterator.objectReferenceValue as ScriptableObject);
                    }
                }
    
                return selectedScriptableObjects.ToArray();
            }
    
            /// <summary>
            /// Iterats through the property for storing selected ScriptableObjects
            /// </summary>
            private static void Write(SerializedProperty property, ScriptableObject[] scriptableObjects)
            {
                // Faster way
                // var w = new System.Diagnostics.Stopwatch();
                // w.Start();
                int i = 0;
                SerializedProperty iterator = property.Copy();
                iterator.arraySize = scriptableObjects.Length;
                SerializedProperty end = iterator.GetEndProperty();
                while (!SerializedProperty.EqualContents(iterator, end) && iterator.Next(true))
                {
                    if (iterator.propertyType == SerializedPropertyType.ObjectReference)
                    {
                        iterator.objectReferenceValue = scriptableObjects[i];
                        i++;
                    }
                }
                // w.Stop();
                // long milliseconds = w.ElapsedMilliseconds;
                // Debug.Log(w.Elapsed.TotalMilliseconds + " ms");
    
                // Another way
                // property.arraySize = scriptableObjects.Length;
                // for (int i = 0; i < property.arraySize; i++)
                // {
                //     property.GetArrayElementAtIndex(i).objectReferenceValue = scriptableObjects[i];
                // }
            }
    
            private static void UpdateScriptableObjectSelectionControl(Rect position, GUIContent label,
                SerializedProperty property, ScriptableObjectMultiSelectDropdownAttribute attribute)
            {
                ScriptableObject[] output = DrawScriptableObjectSelectionControl(position, label, Read(property), property, attribute);
                if (isChanged)
                {
                    isChanged = false;
                    Write(property, output);
                }
            }
    
            private static ScriptableObject[] DrawScriptableObjectSelectionControl(Rect position, GUIContent label,
                ScriptableObject[] scriptableObjects, SerializedProperty property, ScriptableObjectMultiSelectDropdownAttribute attribute)
            {
                bool triggerDropDown = false;
                int controlID = GUIUtility.GetControlID(_controlHint, FocusType.Keyboard, position);
    
                switch (Event.current.GetTypeForControl(controlID))
                {
                    case EventType.ExecuteCommand:
                        if (Event.current.commandName == "ScriptableObjectReferenceUpdated")
                        {
                            if (_selectionControlID == controlID)
                            {
                                if (scriptableObjects != _selectedScriptableObjects.ToArray())
                                {
                                    scriptableObjects = _selectedScriptableObjects.ToArray();
                                    isChanged = true;
                                }
    
                                _selectionControlID = 0;
                                _selectedScriptableObjects = null;
                            }
                        }
                        break;
    
                    case EventType.MouseDown:
                        if (GUI.enabled && position.Contains(Event.current.mousePosition))
                        {
                            GUIUtility.keyboardControl = controlID;
                            triggerDropDown = true;
                            Event.current.Use();
                        }
                        break;
    
                    case EventType.KeyDown:
                        if (GUI.enabled && GUIUtility.keyboardControl == controlID)
                        {
                            if (Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.Space)
                            {
                                triggerDropDown = true;
                                Event.current.Use();
                            }
                        }
                        break;
    
                    case EventType.Repaint:
                        if (scriptableObjects.Length == 0)
                        {
                            _popupContent.text = "Nothing";
                        }
                        else if (scriptableObjects.Length == _scriptableObjects.Count)
                        {
                            _popupContent.text = "Everything";
                        }
                        else if (scriptableObjects.Length >= 2)
                        {
                            _popupContent.text = "Mixed ...";
                        }
                        else
                        {
                            _popupContent.text = scriptableObjects[0].name;
                        }
    
                        EditorStyles.popup.Draw(position, _popupContent, controlID);
                        break;
                }
    
                if (triggerDropDown)
                {
                    _selectionControlID = controlID;
                    _selectedScriptableObjects = scriptableObjects.ToList();
    
                    DisplayDropDown(position, scriptableObjects, attribute.grouping);
                }
    
                return scriptableObjects;
            }
    
            private static void DisplayDropDown(Rect position, ScriptableObject[] selectedScriptableObject, ScriptableObjectGrouping grouping)
            {
                var menu = new GenericMenu();
    
                menu.AddItem(new GUIContent("Nothing"), selectedScriptableObject.Length == 0, _onSelectedScriptableObject, null);
                menu.AddItem(new GUIContent("Everything"),
                    (_scriptableObjects.Count != 0 && selectedScriptableObject.Length == _scriptableObjects.Count),
                    _onSelectedScriptableObject, _scriptableObjects.ToArray());
    
                for (int i = 0; i < _scriptableObjects.Count; ++i)
                {
                    var scriptableObject = _scriptableObjects[i];
    
                    string menuLabel = MakeDropDownGroup(scriptableObject, grouping);
                    if (string.IsNullOrEmpty(menuLabel))
                        continue;
    
                    var content = new GUIContent(menuLabel);
                    menu.AddItem(content, ResolveSelectedScriptableObject(scriptableObject), _onSelectedScriptableObject, scriptableObject);
                }
    
                menu.DropDown(position);
            }
    
            private static void OnSelectedScriptableObject(object userData)
            {
                if (userData == null)
                {
                    _selectedScriptableObjects.Clear();
                }
                else if (userData.GetType().IsArray)
                {
                    _selectedScriptableObjects = (userData as ScriptableObject[]).ToList();
                }
                else
                {
                    ScriptableObject scriptableObject = userData as ScriptableObject;
                    if (!ResolveSelectedScriptableObject(scriptableObject))
                    {
                        _selectedScriptableObjects.Add(scriptableObject);
                    }
                    else
                    {
                        _selectedScriptableObjects.Remove(scriptableObject);
                    }
                }
    
                var scriptableObjectReferenceUpdatedEvent = EditorGUIUtility.CommandEvent("ScriptableObjectReferenceUpdated");
                EditorWindow.focusedWindow.SendEvent(scriptableObjectReferenceUpdatedEvent);
            }
    
            private static string FindScriptableObjectFolderPath(ScriptableObject scriptableObject)
            {
                string path = AssetDatabase.GetAssetPath(scriptableObject);
                path = path.Replace("Assets/", "");
                path = path.Replace(".asset", "");
    
                return path;
            }
    
            private static string MakeDropDownGroup(ScriptableObject scriptableObject, ScriptableObjectGrouping grouping)
            {
                string path = FindScriptableObjectFolderPath(scriptableObject);
    
                switch (grouping)
                {
                    default:
                    case ScriptableObjectGrouping.None:
                        path = path.Replace("/", " > ");
                        return path;
    
                    case ScriptableObjectGrouping.ByFolder:
                        return path;
    
                    case ScriptableObjectGrouping.ByFolderFlat:
                        int last = path.LastIndexOf('/');
                        string part1 = path.Substring(0, last);
                        string part2 = path.Substring(last);
                        path = part1.Replace("/", " > ") + part2;
                        return path;
                }
            }
        }
    }
    

    Usage Example:

    1. Create ScriptableObject class which you want to create specified objects by that.
    using UnityEngine;
    
    [CreateAssetMenu(menuName = "Create Block")]
    public class Block : ScriptableObject
    {
        // Some fields
    }
    
    1. Create ScriptableObjects in the project.

    Resources

    1. Use ScriptableObjectMultiSelectDropdown attribute by setting type of specified ScriptableObject derived class and optional grouping (Default grouping is None) like this in MonoBeahviour or ScriptableObject derived classes.

    MonoBehavior:

    using ScriptableObjectMultiSelectDropdown;
    using UnityEngine;
    
    public class BlockManager : MonoBehaviour
    {
        // Without grouping (default is None)
        [ScriptableObjectMultiSelectDropdown(typeof(Block))]
        public ScriptableObjectReference firstTargetBlocks;
        // By grouping
        [ScriptableObjectMultiSelectDropdown(typeof(Block), grouping = ScriptableObjectGrouping.ByFolder)]
        public ScriptableObjectReference secondTargetBlocks;
    }
    

    MonoBehaviour Default Grouping

    MonoBehaviour ByFolder Grouping

    ScriptableObject:

    using UnityEngine;
    using ScriptableObjectMultiSelectDropdown;
    
    [CreateAssetMenu(menuName = "Create Block Manager Settings")]
    public class BlockManagerSettings : ScriptableObject
    {
        // Without grouping (default is None)
        [ScriptableObjectMultiSelectDropdown(typeof(Block))]
        public ScriptableObjectReference firstTargetBlocks;
        // By grouping
        [ScriptableObjectMultiSelectDropdown(typeof(Block), grouping = ScriptableObjectGrouping.ByFolderFlat)]
        public ScriptableObjectReference secondTargetBlocks;
    }
    

    ScriptableObject Default Grouping

    ScriptableObject ByFolderFlat Grouping