Unity3d : UnityEditor 上的 Drag and drop 寫法.

Unity3d : UnityEditor 上的 Drag and drop 寫法.

借工作之便解決了先前想不到的 Drag and drop 的判斷.

  • 利用 Event.Current 上的 EventType.DragUpdated, DragPerform 等抓取 Drag 狀態
  • 使用 GUILayoutUtility.GetLastRect() 取得先前的 list element 的繪畫大小並覆蓋.
  • 分拆出 DragAndDrop.objectReferences (選取的 Asset list) 並放入 nest 結構的 struct 裡面 (a.k.a: NamedPrefab)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Test
{
    [CreateAssetMenu]
    public class PrefabLoader : ScriptableObject
    {
        [System.Serializable]
        public struct NamedPrefab
        {
            public GameObject prefab;
            public string name;
        }
        public List<NamedPrefab> m_PrefabList;

        private Dictionary<string, GameObject> m_PrefabDic = null;
        public Dictionary<string, GameObject> PrefabDic
        {
            get
            {
                if (m_PrefabDic == null)
                {
                    m_PrefabDic = new Dictionary<string, GameObject>(m_PrefabList.Count);
                    for (int i = 0; i < m_PrefabList.Count; ++i)
                    {
                        // Noted: assume not require duplicate checking.
                        m_PrefabDic.Add(m_PrefabList[i].name, m_PrefabList[i].prefab);
                    }
                }
                return m_PrefabDic;
            }
        }
    }

#if UNITY_EDITOR

    [CustomEditor(typeof(PrefabLoader), true)]
    public class PrefabLoaderInspector : Editor
    {
        SerializedProperty scriptProp;
        SerializedProperty prefabListProp;
        private void OnEnable()
        {
            scriptProp = serializedObject.FindProperty("m_Script");
            prefabListProp = serializedObject.FindProperty(nameof(PrefabLoader.m_PrefabList));
        }

        public override void OnInspectorGUI()
        {
            serializedObject.UpdateIfRequiredOrScript();
            SerializedProperty iter = serializedObject.GetIterator();
            iter.NextVisible(true);
            EditorGUI.BeginDisabledGroup(true);
            EditorGUILayout.PropertyField(scriptProp, includeChildren: true);
            EditorGUI.EndDisabledGroup();

            EditorGUI.BeginChangeCheck(); // ---- change check [optional]

            do
            {
                if (scriptProp != null && iter.propertyPath == scriptProp.propertyPath)
                {
                    // do nothing.
                }
                else
                {
                    OnDrawProperty(iter);
                }
            }
            while (iter.NextVisible(false));

            if (EditorGUI.EndChangeCheck()) // ---- change check [optional]
                serializedObject.ApplyModifiedProperties();
        }

        private void OnDrawProperty(SerializedProperty property)
        {
            if (property.propertyPath == prefabListProp.propertyPath)
            {
                EditorGUILayout.PropertyField(property);
                DragArea(GUILayoutUtility.GetLastRect(), property, NamedPrefabEditorDrawer.CustomDraw);
            }
            else
            {
                EditorGUILayout.PropertyField(property);
            }
        }

        public delegate void InsertProperty(SerializedProperty listProperty, UnityEngine.Object interactObj);

        private void DragArea(Rect area, SerializedProperty property, InsertProperty drawCallback)
        {
            Event evt = Event.current;
            if (evt.type == EventType.Repaint &&
                DragAndDrop.visualMode == DragAndDropVisualMode.Copy)
                GUI.Box(area, "Drag and drop all Skin.", GUI.skin.window);
            switch (evt.type)
            {
                case EventType.DragUpdated:
                case EventType.DragPerform:
                    if (!area.Contains(evt.mousePosition))
                        return;
                    DragAndDrop.visualMode = DragAndDropVisualMode.Copy;

                    if (evt.type == EventType.DragPerform)
                    {
                        DragAndDrop.AcceptDrag();
                        if (property.isArray)
                        {
                            foreach (UnityEngine.Object obj in DragAndDrop.objectReferences)
                            {
                                if (!(obj is GameObject prefab))
                                    continue;
                                if (!string.IsNullOrEmpty(prefab.scene.path))
                                {
                                    Debug.LogError($"Scene object {prefab.name} not allow");
                                    continue;
                                }
                                int last = property.arraySize;
                                property.InsertArrayElementAtIndex(last);
                                drawCallback?.Invoke(property.GetArrayElementAtIndex(last), obj);
                                property.serializedObject.ApplyModifiedProperties();
                            }
                        }
                        else
                        {
                            // Debug.LogError("Haven't test");
                            var obj = DragAndDrop.objectReferences.GetValue(0) as UnityEngine.Object;
                            drawCallback?.Invoke(property, obj);
                        }
                    }
                    break;
                case EventType.MouseUp:
                    DragAndDrop.PrepareStartDrag();
                    break;
            }
        }
    }

    [CustomPropertyDrawer(typeof(PrefabLoader.NamedPrefab), true)]
    public class NamedPrefabEditorDrawer : PropertyDrawer
    {
        public static void CustomDraw(SerializedProperty property, UnityEngine.Object interactObj)
        {
            // Debug.Log(listProperty.propertyType);
            var nameProp = property.FindPropertyRelative(nameof(PrefabLoader.NamedPrefab.name));
            var prefabProp = property.FindPropertyRelative(nameof(PrefabLoader.NamedPrefab.prefab));
            nameProp.stringValue = interactObj.name;
            prefabProp.objectReferenceValue = interactObj;
            property.serializedObject.ApplyModifiedProperties();
        }

        public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
        {
            // return base.GetPropertyHeight(property, label);
            return EditorGUIUtility.singleLineHeight;
        }
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            EditorGUI.BeginProperty(position, label, property);
            // position = EditorGUI.PrefixLabel(position, label);
            Rect[] rect = { position, position };
            const float hSpace = 4f;
            rect[0].width = EditorGUIUtility.labelWidth - hSpace;
            rect[1].width = rect[1].width - rect[0].width;
            rect[1].x += rect[0].width + hSpace;
            SerializedProperty nameProp = property.FindPropertyRelative(nameof(PrefabLoader.NamedPrefab.name));
            SerializedProperty prefabProp = property.FindPropertyRelative(nameof(PrefabLoader.NamedPrefab.prefab));

            EditorGUI.BeginChangeCheck();
            string tmp = EditorGUI.TextField(rect[0], nameProp.stringValue);
            EditorGUI.PropertyField(rect[1], prefabProp, GUIContent.none, false);
            if (EditorGUI.EndChangeCheck())
            {
                if (string.IsNullOrEmpty(tmp) && prefabProp.objectReferenceValue != null)
                {
                    tmp = prefabProp.objectReferenceValue.name;
                }
                nameProp.stringValue = tmp;
                property.serializedObject.ApplyModifiedProperties();
            }
            EditorGUI.EndProperty();
        }
    }
#endif
}

2 Comments

  1. 阿峰

    請教一下為什麼要Call UpdateIfRequiredOrScript,我一直搞不懂這個部分,但很多教學都直接寫要Call這個方法,想請問他實際上做了什麼?

    1. 官方文檔就有提到 UpdateIfRequiredOrScript

      Update serialized object’s representation, only if the object has been modified since the last call to Update or if it is a script.
      In which case it is not safe to assume that SetDirty has been called. Return true if an Update was done.

      它就是把資料更新的 event 觸發一次,這樣確保 inspector收到event進行重繪.

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

*

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料