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

Showing ScriptableObjects in dropdown menu and select between items in Unity


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


Solution

  • I've published a repository on Github which solve this problem. It is for selecting between them 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:

    ScriptableObject Dropdown:

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

    ScriptableObject Dropdown

    Code:

    ScriptableObjectDropdownAttribute.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 ScriptableObjectDropdown
    {
        /// <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
        /// {
        ///     [ScriptableObjectDropdown] public Block targetBlock;
        ///     
        ///     // or
        ///     
        ///     [ScriptableObjectDropdown(grouping = ScriptableObjectGrouping.ByFolder)] public Block targetBlock;
        /// }
        /// 
        /// // or
        /// 
        /// [CreateAssetMenu(menuName = "Create Block Manager Settings")]
        /// public class BlockManagerSetting : ScriptableObject
        /// {
        ///     [ScriptableObjectDropdown] public Block targetBlock;
        ///     
        ///     // or
        ///     
        ///     [ScriptableObjectDropdown(grouping = ScriptableObjectGrouping.ByFolder)] public Block targetBlock;
        /// }
        /// ]]></code>
        /// </example>
        [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
        public class ScriptableObjectDropdownAttribute : PropertyAttribute
        {
            public ScriptableObjectGrouping grouping = ScriptableObjectGrouping.None;
    
            public ScriptableObjectDropdownAttribute() { }
        }
    }
    

    Put this one in Editor folder:

    ScriptableObjectDropdownDrawer.cs:

    // Copyright (c) ATHellboy (Alireza Tarahomi) Limited. All rights reserved.
    // Licensed under the MIT license. See LICENSE file in the project root.
    
    using UnityEngine;
    using UnityEditor;
    using System.Reflection;
    using System;
    using System.Collections.Generic;
    
    namespace ScriptableObjectDropdown.Editor
    {
        // TODO: Mixed value (-) for selecting multi objects
        [CustomPropertyDrawer(typeof(ScriptableObjectDropdownAttribute))]
        public class ScriptableObjectDropdownDrawer : PropertyDrawer
        {
            private static List<ScriptableObject> _scriptableObjects = new List<ScriptableObject>();
            private static ScriptableObject _selectedScriptableObject;
            private static readonly int _controlHint = typeof(ScriptableObjectDropdownAttribute).GetHashCode();
            private static GUIContent _popupContent = new GUIContent();
            private static int _selectedControlID;
            private static readonly GenericMenu.MenuFunction2 _onSelectedScriptableObject = OnSelectedScriptableObject;
            private static bool isChanged;
    
            static ScriptableObjectDropdownDrawer()
            {
                EditorApplication.projectChanged += ClearCache;
            }
    
            public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
            {
                if (_scriptableObjects.Count == 0)
                {
                    GetScriptableObjects(property);
                }
    
                Draw(position, label, property, attribute as ScriptableObjectDropdownAttribute);
            }
    
            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.IsSubclassOf(typeof(ScriptableObject)) && propertyType != typeof(ScriptableObject))
                {
                    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 ScriptableObject[] GetScriptableObjects(SerializedProperty property)
            {
                Type propertyType = GetPropertyType(property);
                string[] guids = AssetDatabase.FindAssets(String.Format("t:{0}", propertyType));
                for (int i = 0; i < guids.Length; i++)
                {
                    _scriptableObjects.Add(AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[i]), propertyType) as ScriptableObject);
                }
    
                return _scriptableObjects.ToArray();
            }
    
            private void Draw(Rect position, GUIContent label,
                SerializedProperty property, ScriptableObjectDropdownAttribute attribute)
            {
                if (label != null && label != GUIContent.none)
                    position = EditorGUI.PrefixLabel(position, label);
    
                if (ValidateProperty(property))
                {
                    if (_scriptableObjects.Count != 0)
                    {
                        UpdateScriptableObjectSelectionControl(position, label, property, attribute);
                    }
                    else
                    {
                        EditorGUI.LabelField(position, "There is no this type asset in the project");
                    }
                }
                else
                {
                    EditorGUI.LabelField(position, "Use it with non-array ScriptableObject or derived class of ScriptableObject");
                }
            }
    
            private static void UpdateScriptableObjectSelectionControl(Rect position, GUIContent label,
                SerializedProperty property, ScriptableObjectDropdownAttribute attribute)
            {
                ScriptableObject output = DrawScriptableObjectSelectionControl(position, label, property.objectReferenceValue as ScriptableObject, property, attribute);
    
                if (isChanged)
                {
                    isChanged = false;
                    property.objectReferenceValue = output;
                }
            }
    
            private static ScriptableObject DrawScriptableObjectSelectionControl(Rect position, GUIContent label,
                ScriptableObject scriptableObject, SerializedProperty property, ScriptableObjectDropdownAttribute 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 (_selectedControlID == controlID)
                            {
                                if (scriptableObject != _selectedScriptableObject)
                                {
                                    scriptableObject = _selectedScriptableObject;
                                    isChanged = true;
                                }
    
                                _selectedControlID = 0;
                                _selectedScriptableObject = 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 (scriptableObject == null)
                        {
                            _popupContent.text = "Nothing";
                        }
                        else
                        {
                            _popupContent.text = scriptableObject.name;
                        }
                        EditorStyles.popup.Draw(position, _popupContent, controlID);
                        break;
                }
    
                if (_scriptableObjects.Count != 0 && triggerDropDown)
                {
                    _selectedControlID = controlID;
                    _selectedScriptableObject = scriptableObject;
    
                    DisplayDropDown(position, scriptableObject, attribute.grouping);
                }
    
                return scriptableObject;
            }
    
            private static void DisplayDropDown(Rect position, ScriptableObject selectedScriptableObject, ScriptableObjectGrouping grouping)
            {
                var menu = new GenericMenu();
    
                menu.AddItem(new GUIContent("Nothing"), selectedScriptableObject == null, _onSelectedScriptableObject, null);
                menu.AddSeparator("");
    
                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, scriptableObject == selectedScriptableObject, _onSelectedScriptableObject, scriptableObject);
                }
    
                menu.DropDown(position);
            }
    
            private static void OnSelectedScriptableObject(object userData)
            {
                _selectedScriptableObject = userData as 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 ScriptableObjectDropdown attribute by setting optional grouping (Default grouping is None) like this in MonoBeahviour or ScriptableObject derived classes.

    MonoBehavior:

    using ScriptableObjectDropdown;
    using UnityEngine;
    
    public class BlockManager : MonoBehaviour
    {
        // Without grouping (default is None)
        [ScriptableObjectDropdown] public Block firstTargetBlock;
        // By grouping
        [ScriptableObjectDropdown(grouping = ScriptableObjectGrouping.ByFolder)] public Block secondTargetBlock;
    }
    

    MonoBehaviour Default Grouping

    MonoBehaviour ByFolder Grouping

    ScriptableObject:

    using UnityEngine;
    using ScriptableObjectDropdown;
    
    [CreateAssetMenu(menuName ="Create Block Manager Settings")]
    public class BlockManagerSettings : ScriptableObject
    {
        // Without grouping (default is None)
        [ScriptableObjectDropdown] public Block firstTargetBlock;
        // By grouping
        [ScriptableObjectDropdown(grouping = ScriptableObjectGrouping.ByFolderFlat)] public Block secondTargetBlock;
    }
    

    ScriptableObject Default Grouping

    ScriptableObject ByFolderFlat Grouping