在 Unity 的多选下拉菜单中显示 ScriptableObjects

Showing ScriptableObjects in multi selection dropdown menu in Unity

我应该如何创建 PropertyAttributePropertyDrawer 以在 Inspector 的下拉菜单中显示 ScriptableObject 以进行多选?

我发布了 a repository on Github 来解决这个问题。它用于检查器下拉菜单中的多个 selecting。

在 Github 链接中,您可以访问发布页面中的示例文件夹和 unitypackage,但如果您不想转到链接或链接出现任何问题,您可以按照以下说明操作:

ScriptableObjectMultiSelectDropdown:

ScriptableObjectMultiSelectDropdown 是 Unity Inspector 的一个属性。 它用于显示在您的项目中创建的 ScriptableObjects,在下拉菜单中以及 select Inspector 中的多个。

代码:

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;
        }
    }
}

将这个放在编辑器文件夹中:

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;
            }
        }
    }
}

用法示例:

  1. 创建ScriptableObject class你想创建的指定对象
using UnityEngine;

[CreateAssetMenu(menuName = "Create Block")]
public class Block : ScriptableObject
{
    // Some fields
}
  1. 在项目中创建 ScriptableObjects。

  1. 通过设置指定的 ScriptableObject 派生类型 class 和可选分组(默认分组为 None)来使用 ScriptableObjectMultiSelectDropdown 属性,就像在 MonoBeahviour 或 [=19= 中这样] 派生 classes.

单行为:

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;
}

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;
}