Unity檢視面板重構(OnInspectorGUI重寫)

前言

使用GF框架時,有沒有發現很神奇的情況,繼承任何模塊的輔助器基類腳本(Helper)都會被檢視面板自動識別,這裏以GF框架爲例講述一下如何做到自動識別腳本的。

1.自動識別腳本

不知道GF框架是何物的,也不影響這篇文章的觀看,這裏先講述一下具體效果,按照框架模塊中的本地化模塊爲例,分析GF框架是如何更新檢視面板下的輔助器枚舉,首先看到以下截圖:

Localization Helper下的枚舉就是自動識別的,創建出的腳本繼承了DefaultLocalizationHelper,並且腳本的路徑在Asset下,它這裏選項就自動會添加剛剛創建的腳本,amazing!!!怎麼做到的這個功能的,也太神奇了。 

爲什麼在下的Unity就做不到(難道是長得不夠帥???),GF框架確可以自動識別,Unity應該學乖了,可以自動去開發遊戲了。來看看GF框架到底做了什麼妖?功夫不負有心人,當場抓獲以下腳本,具體代碼如下:

using UnityEditor;
using UnityGameFramework.Runtime;

namespace UnityGameFramework.Editor
{
    [CustomEditor(typeof(LocalizationComponent))]
    internal sealed class LocalizationComponentInspector : GameFrameworkInspector
    {
        private SerializedProperty m_EnableLoadDictionaryUpdateEvent = null;
        private SerializedProperty m_EnableLoadDictionaryDependencyAssetEvent = null;

        private HelperInfo<LocalizationHelperBase> m_LocalizationHelperInfo = new HelperInfo<LocalizationHelperBase>("Localization");

        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            serializedObject.Update();

            LocalizationComponent t = (LocalizationComponent)target;

            EditorGUI.BeginDisabledGroup(EditorApplication.isPlayingOrWillChangePlaymode);
            {
                EditorGUILayout.PropertyField(m_EnableLoadDictionaryUpdateEvent);
                EditorGUILayout.PropertyField(m_EnableLoadDictionaryDependencyAssetEvent);
                m_LocalizationHelperInfo.Draw();
            }
            EditorGUI.EndDisabledGroup();

            if (EditorApplication.isPlaying && IsPrefabInHierarchy(t.gameObject))
            {
                EditorGUILayout.LabelField("Language", t.Language.ToString());
                EditorGUILayout.LabelField("System Language", t.SystemLanguage.ToString());
                EditorGUILayout.LabelField("Dictionary Count", t.DictionaryCount.ToString());
            }

            serializedObject.ApplyModifiedProperties();

            Repaint();
        }

        protected override void OnCompileComplete()
        {
            base.OnCompileComplete();

            RefreshTypeNames();
        }

        private void OnEnable()
        {
            m_EnableLoadDictionaryUpdateEvent = serializedObject.FindProperty("m_EnableLoadDictionaryUpdateEvent");
            m_EnableLoadDictionaryDependencyAssetEvent = serializedObject.FindProperty("m_EnableLoadDictionaryDependencyAssetEvent");

            m_LocalizationHelperInfo.Init(serializedObject);

            RefreshTypeNames();
        }

        private void RefreshTypeNames()
        {
            m_LocalizationHelperInfo.Refresh();
            serializedObject.ApplyModifiedProperties();
        }
    }
}

GameFrameworkInspector是繼承了UnityEditor.Editor,封裝了編譯開始和完成的事件,部分代碼段如下:

        private bool m_IsCompiling = false;

        /// <summary>
        /// 繪製事件。
        /// </summary>
        public override void OnInspectorGUI()
        {
            if (m_IsCompiling && !EditorApplication.isCompiling)
            {
                m_IsCompiling = false;
                OnCompileComplete();  //虛函數沒有任何實現
            }
            else if (!m_IsCompiling && EditorApplication.isCompiling)
            {
                m_IsCompiling = true;
                OnCompileStart();     //虛函數沒有任何實現 
            }
        }

首次接觸Unity的Editor模塊編程,可能會看不懂上面的代碼,所以先整理出一下表格,描述經常使用接口的具體功能,表格如下:

EditorGUILayout.LabelField CustomEditor指定的GameObject腳本的檢查器面板下顯示標籤
EditorGUILayout.PropertyField 製作用於顯示SerializedProperty屬性字段的方式,如果FindProperty是布爾型就顯示勾選,字符串型就顯示文本輸入框。
EditorGUILayout.Popup

彈出選擇菜單

EditorApplication.isPlayingOrWillChangePlaymode

是否正在顯示或即將切換到檢查器面板顯示。

EditorApplication.isPlaying 編譯器已經啓動正在運行時返回true。
BeginDisabledGroup,EndDisabledGroup 它提供了一種更安全的範圍劃分機制,當條件爲true時會觸發執行,這裏使用是一種優化的方案,當查看到腳本纔會進行繪畫。
Editor.Repaint

重繪顯示在這個編輯器的任何檢視面板,一般用於面板屬性有更新變動時。

serializedObject.ApplyModifiedProperties 應用修改的屬性。
serializedObject.FindProperty CustomEditor指定的GameObject腳本中獲取對象以在檢查器中顯示。
serializedObject.Update 更新序列化對象的表示形式。

代碼含義是先獲取(FindProperty)LocalizationComponent需要設置的屬性,然後顯示獲取到的屬性,額外顯示了當前使用的語言、系統的語言、語言字典的數量。應用屬性的變化之後進行重畫,就這樣一直循環刷新,這裏有HelperInfo腳本就是用來顯示輔助器腳本的,進入看看它到底怎麼實現了自動識別腳本的,具體代碼如下:

using GameFramework;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

namespace UnityGameFramework.Editor
{
    internal sealed class HelperInfo<T> where T : MonoBehaviour
    {
        private const string CustomOptionName = "<Custom>";

        private readonly string m_Name;

        private SerializedProperty m_HelperTypeName;
        private SerializedProperty m_CustomHelper;
        private string[] m_HelperTypeNames;
        private int m_HelperTypeNameIndex;

        public HelperInfo(string name)
        {
            m_Name = name;

            m_HelperTypeName = null;
            m_CustomHelper = null;
            m_HelperTypeNames = null;
            m_HelperTypeNameIndex = 0;
        }

        public void Init(SerializedObject serializedObject)
        {
            m_HelperTypeName = serializedObject.FindProperty(Utility.Text.Format("m_{0}HelperTypeName", m_Name));
            m_CustomHelper = serializedObject.FindProperty(Utility.Text.Format("m_Custom{0}Helper", m_Name));
        }

        public void Draw()
        {
            string displayName = FieldNameForDisplay(m_Name);
            int selectedIndex = EditorGUILayout.Popup(Utility.Text.Format("{0} Helper", displayName), m_HelperTypeNameIndex, m_HelperTypeNames);
            if (selectedIndex != m_HelperTypeNameIndex)
            {
                m_HelperTypeNameIndex = selectedIndex;
                m_HelperTypeName.stringValue = (selectedIndex <= 0 ? null : m_HelperTypeNames[selectedIndex]);
            }

            if (m_HelperTypeNameIndex <= 0)
            {
                EditorGUILayout.PropertyField(m_CustomHelper);
                if (m_CustomHelper.objectReferenceValue == null)
                {
                    EditorGUILayout.HelpBox(Utility.Text.Format("You must set Custom {0} Helper.", displayName), MessageType.Error);
                }
            }
        }

        public void Refresh()
        {
            List<string> helperTypeNameList = new List<string>
            {
                CustomOptionName
            };

            helperTypeNameList.AddRange(Type.GetTypeNames(typeof(T)));
            m_HelperTypeNames = helperTypeNameList.ToArray();

            m_HelperTypeNameIndex = 0;
            if (!string.IsNullOrEmpty(m_HelperTypeName.stringValue))
            {
                m_HelperTypeNameIndex = helperTypeNameList.IndexOf(m_HelperTypeName.stringValue);
                if (m_HelperTypeNameIndex <= 0)
                {
                    m_HelperTypeNameIndex = 0;
                    m_HelperTypeName.stringValue = null;
                }
            }
        }

        private string FieldNameForDisplay(string fieldName)
        {
            if (string.IsNullOrEmpty(fieldName))
            {
                return string.Empty;
            }

            string str = Regex.Replace(fieldName, @"^m_", string.Empty);
            str = Regex.Replace(str, @"((?<=[a-z])[A-Z]|[A-Z](?=[a-z]))", @" $1").TrimStart();
            return str;
        }
    }
}

看到這裏就知道其原由,腳本是通過Type.GetTypeNames去獲取解決方案下所有繼承於輔助器基類的腳本,然後彈出選擇菜單進行名字選定(EditorGUILayout.Popup),框架啓動時通過反射將創建出本地化輔助器實例,如此一來就實現自定義和擴展框架功能了,是不是感覺屌炸天了。

 2.花裏胡哨的檢視(Inspector)界面

看到Unity自帶的組件檢視界面是如此花裏胡哨的(比如Button,Material這些花裏胡哨檢視界面),用時並且想模仿出類似的檢視界面,有這個想法的話就已經成功一半了,畢竟只要想模仿纔是邁出成功的第一步,首先看一下按鈕組件的檢視界面長啥樣,雖然沒有喫過豬肉起碼看過豬跑,爲了和模擬出來的界面進行比較,還是把自帶按鈕的檢視界面給各位放出來看看。

標準按鈕組件的檢視界面就是長成這樣的,接下來就模擬一下按鈕組件的檢視界面樣式,經過筆者一段時間猛如虎的操作,再展示模擬出的按鈕檢視界面效果之前,各位看官注意拿好手機或抱好電腦屏幕,具體圖片如下:

雌兔腳撲朔,雄兔眼迷離。可能會說狗賊別拿Photoshop以後的效果來唬弄,這裏是分享知識的地方,怎麼就被玷污了,這百分百是p出來的。對不起各位看官!這個確實靠重寫OnInspectorGUI函數得到的效果。

接下來就展示一下TButtonInspector的源代碼,悟空的分身術到底是如何實現的,具體代碼如下:

using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(TButton))]
internal sealed class TButtonInspector :UnityEditor.Editor
{
    SerializedProperty OnClick = null;
    SerializedProperty Interactable = null;
    public enum Transition
    {
        None,
        ColorTint,
        SpriteSwap,
        Animation
    }
    private GameObject graphic;
    private Transition transition = Transition.ColorTint;
    public override void OnInspectorGUI()
    {
        EditorGUI.BeginDisabledGroup(EditorApplication.isPlayingOrWillChangePlaymode);
        {
            EditorGUILayout.PropertyField(Interactable);         
            EditorGUILayout.EnumPopup("Transition", transition);
            EditorGUILayout.BeginHorizontal();
            EditorGUILayout.BeginVertical(GUILayout.Width(6));
            EditorGUILayout.Space();
            EditorGUILayout.EndVertical();
            EditorGUILayout.BeginVertical();
            EditorGUILayout.ObjectField("Target graphic", graphic, typeof(GameObject), false);
            if (graphic == null)
                EditorGUILayout.HelpBox("You must have Target graphic", MessageType.Warning);
            EditorGUILayout.ColorField("Normal Color",Color.white);
            EditorGUILayout.ColorField("Highlighted Color", Color.white);
            EditorGUILayout.ColorField("Pressed Color", Color.gray);
            EditorGUILayout.ColorField("Disabled Color", Color.gray);
            EditorGUILayout.Slider("Color Multiplier",1,1,10);
            EditorGUILayout.FloatField("Fade Duration", 0.1f);
            EditorGUILayout.EnumPopup("Navigation", transition);

            EditorGUILayout.BeginHorizontal();
            EditorGUILayout.BeginVertical(GUILayout.Width(180));
            EditorGUILayout.Space();
            EditorGUILayout.EndVertical();
            EditorGUILayout.BeginVertical();
            if (GUILayout.Button("Visualize"))
            {
                Debug.Log("檢測到點擊了");
            }
            EditorGUILayout.EndVertical();
            EditorGUILayout.EndHorizontal();

            EditorGUILayout.EndVertical();
            EditorGUILayout.EndHorizontal();
            EditorGUILayout.Space();
            EditorGUILayout.Space();
            EditorGUILayout.PropertyField(OnClick);
        }
        EditorGUI.EndDisabledGroup();
        serializedObject.ApplyModifiedProperties();
    }

    private void OnEnable()
    {
        Interactable = serializedObject.FindProperty("Interactable");
        OnClick = serializedObject.FindProperty("onClick");
    }
}

然後TButton的代碼如下:

using UnityEngine;
using UnityEngine.Events;

[DisallowMultipleComponent]
public class TButton : MonoBehaviour
{
    [SerializeField]
    private bool Interactable = true;
    [SerializeField]
    private OnClick onClick;
}

[Serializable]
public class OnClick : UnityEvent { }

想不到吧!最後就是腳本圖標的替換,Assets下命名一個Gizmos文件夾,把圖片放到此文件夾裏,然後把圖片命名成腳本名+空格+Icon,Unity會自動去幫你替換腳本圖標, 以上的腳本只是模仿檢視界面,沒有任何實際的功能,俗話說的好花瓶雖然好看,但是一點用沒有。

3.彩蛋(Unity的UGUI源代碼)

花瓶好看卻是毫無實際功能,怎麼辦呢?接下來我就要給大家一份厚禮了,記得關注投幣餵食三連,不對不對,禁止投食...

Unity官方下載UGUI源代碼鏈接是:https://bitbucket.org/Unity-Technologies/ui/downloads/?tab=tags

網速屬實太慢的話,給大家上傳到csdn了:https://download.csdn.net/download/m0_37920739/12229964

工程已經下載好了,迫不及待開始部署UIGUI到Unity裏進行學習吧,先查看下載過來的壓縮包有那些東西,可以看到以下的文件夾,具體截圖如下:

只需要把UnityEngine.UI放到Unity下即可,然後把UnityEditor.UI、UnityEngine.UI-Editor放到Assets/Editor路徑。記住這些文件夾下所有和代碼不相關的東西都可以刪掉,還需要移除掉Editor\Data\UnityExtensions\Unity\GUISystem文件夾,然後就是創建UnityEditor.UI和UnityEngine.UI的Assembly Denfinition,查看打印什麼錯誤給它們添加上缺失的引用,具體的引用添加如下圖:

Unity2018以上有一個坑爹的地方就是Packages裏的有一個TestMeshPro引用了UnityEngine.UI,但是這個包的所有文件是不讓修改的,無法給它添加上引用,如圖所示:

直接通過Window下的Package Manager選項移除掉這個包,具體界面如下:

之後就可以調試UGUI模塊了,如果各位有什麼比較好的想法需要添加集成到Unity的UGUI模塊裏,這時可以去替換GUISystem文件夾下所有文件,就是自行定製UGUI模塊了 ,具體截圖如下:

需要注意的是,Unity的mdb而不是pdb,所以還需要一個工具將pdb轉成mdb,所有pdb都需要轉換,Unity有自帶的轉換工具交pdb2mdb.exe,通過命令行執行這個程序,具體執行界面如下:

 先寫上pdb2mdb.exe然後將dll直接拖動到命令行下,路徑就會自動出來。

pdb2mdb下載鏈接:https://download.csdn.net/download/m0_37920739/12230644

UGUI部署好的開源工程鏈接:https://download.csdn.net/download/m0_37920739/12230641

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章