【Unity编辑器扩展】UI变量代码自动生成工具(编辑器扩展干货/大幅提高效率)

 工具主要功能:通过扩展编辑器菜单,在不写一行代码的情况下,快速方便灵活的配置生成UI变量。

 工具使用演示

模式一:通过SerializedField填充,初始化变量

模式二:通过GetComponent初始化变量 

 移除变量:

2022.9.1更新:

新增了变量绑定模式设置:

模式一:先点Generate Script自动生成[SerializeField]变量,然后点击Bind Properties将组件赋值给[SerializeField]变量;  优点是可以在Inspector面板上显示出每一个变量值,缺点是由于某种bug,直接生成完[SerialzedField]变量代码后,即使做了等待脚本编译和资源导入全部完成后再对[SerializeField]变量赋值,依然会出现"Type mismatch"的情况。所以只能把生成代码和绑定变量拆成两个按钮步骤才能正常。

模式二:不用SerializeField,直接生成通过GetComponent从已绑定GameObject上获取组件的代码。优点是步骤简单,面板显示简洁,不需要保存多个序列化对象。缺点是在界面逻辑实例化时会有调用GetComponent的微微微微微小的开销。

1. 在Prefab UI界面中右键点击节点,指定变量为private/protected/public,然后弹出可选择的变量类型菜单。

 2. 添加绑定后,数据在Inspector面板显示,方便修改变量相关类型等;支持添加变量和数组变量;默认变量名为节点名,变量名重复时自动修改变量名,可自行编辑变量名;支持数组添加/移除;拖动数组元素可调整索引;

 3. 选择变量类型。根据绑定节点筛选出交集组件,展示在下拉列表里以便选择变量类型

 4. 在Hierarchy面板直观显示已绑定的组件信息:

 5. 绑定完UI变量后点击生成代码按钮一键生成对应UIForm的partial类绑定脚本;如果当前UIForm逻辑脚本类没有添加partial修饰符,程序会自动提示添加。UI绑定代码独立生成partial脚本是为了避免与UIForm界面逻辑脚本冲突,比如VS中存在为保存的代码,这时直接操作UIForm类会为保存部分丢失的可能。

功能代码实现:

1. 定义SerializeFieldData类用于记录变量数据,在UI逻辑的基类添加_fields用于存放变量数据列表

[Serializable]
public class SerializeFieldData
{
    public string VarName;      //变量名
    public GameObject[] Targets;//关联的GameObject
    public string VarType;      //变量类型FullName,带有名字空间
    public bool Foldout;    //列表展开
    public string VarPrefix;//变量private/protect/public
    public string VarSampleType;//变量类型不带名字空间
    public SerializeFieldData(string varName, GameObject[] targets = null)
    {
        VarName = varName;
        Targets = targets ?? new GameObject[1];
        Foldout = true;
    }
    public T GetComponent<T>(int idx) where T : Component
    {
        return Targets[idx].GetComponent<T>();
    }
    public T[] GetComponents<T>() where T : Component
    {
        T[] result = new T[Targets.Length];
        for (int i = 0; i < Targets.Length; i++)
        {
            result[i] = Targets[i].GetComponent<T>();
        }
        return result;
    }
}

public class UIFormBase : UIFormLogic
{
    [HideInInspector][SerializeField] SerializeFieldData[] _fields = new SerializeFieldData[0];
...
}

2. 自定义UIFormEditor类,扩展编辑器:

[CustomEditor(typeof(UIFormBase), true)] 传入true使编辑器扩展对所有继承自UIFormBase的类都能拥有编辑器扩展部分出的功能。

[CustomEditor(typeof(UIFormBase), true)]
public class UIFormBaseEditor : Editor
{
...
}

通过注册 EditorApplication.hierarchyWindowItemOnGUI = delegate (int id, Rect rect){...},可以自定义Hierarchy面板的UI显示。如,在已经添加到变量的节点后面用绿色文字显示出变量信息

private static GUIStyle normalStyle;
    private static GUIStyle selectedStyle;
    [InitializeOnLoadMethod]
    static void InitEditor()
    {

        normalStyle = new GUIStyle();
        normalStyle.normal.textColor = Color.white;

        selectedStyle = new GUIStyle();
        selectedStyle.normal.textColor = Color.green;
        Selection.selectionChanged = () =>
        {
            addToFieldToggle = false;
            removeToFieldToggle = false;
        };

        EditorApplication.hierarchyWindowItemOnGUI = delegate (int id, Rect rect)
        {
            OpenSelectComponentMenuListener(rect);
            var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
            if (prefabStage == null)
            {
                return;
            }
            var uiForm = prefabStage.prefabContentsRoot.GetComponent<UIFormBase>();
            if (uiForm == null)
            {
                return;
            }
            var curDrawNode = EditorUtility.InstanceIDToObject(id) as GameObject;
            if (curDrawNode == null)
            {
                return;
            }
            var fields = uiForm.GetFieldsProperties();
            SerializeFieldData drawItem = null;
            foreach (var item in fields)
            {
                if (item == null) continue;
                if (ArrayUtility.Contains(item.Targets, curDrawNode))
                {
                    drawItem = item;
                    break;
                }
            }
            if (drawItem != null)
            {
                rect.x = rect.xMax - Mathf.Min(300, rect.size.x * 0.35f);
                GUI.Label(rect, string.Format("{0} {1} {2}", drawItem.VarPrefix, drawItem.VarSampleType, drawItem.VarName), selectedStyle);
            }

        };
    }

3. 自定义右键菜单:

通过[MenuItem("GameObject/Add UIForm Variable/private", false, priority = 0)],设置路径为GameObject/就出现在Hierarchy的右键菜单里。

static int varPrefixIndex = -1;
    static bool mShowSelectTypeMenu;//是否显示变量类型选择菜单
    [MenuItem("GameObject/Add UIForm Variable/private", false, priority = 0)]
    private static void AddPrivateVariable2UIForm()
    {
        varPrefixIndex = 0;
        mShowSelectTypeMenu = true;
    }

4. 变量类型选择菜单框:

在EditorApplication.hierarchyWindowItemOnGUI回调中调用了OpenSelectComponentMenuListener(rect)方法,然后获取选中节点的所有组件的交集,显示在菜单中:

private static void OpenSelectComponentMenuListener(Rect rect)
    {
        if (mShowSelectTypeMenu)
        {
            int idx = -1;
            var strArr = GetPopupContents(GetTargetsFromSelectedNodes(Selection.gameObjects));
            var contents = new GUIContent[strArr.Length];
            for (int i = 0; i < strArr.Length; i++)
            {
                contents[i] = new GUIContent(strArr[i]);
            }
            rect.width = 200;
            rect.height = MathF.Max(100, contents.Length * rect.height);
            EditorUtility.DisplayCustomMenu(rect, contents, idx, (userData, contents, selected) =>
            {
                AddToFields(varPrefixArr[varPrefixIndex], contents[selected]);
            }, null);
            mShowSelectTypeMenu = false;
        }
    }

5. 根据选择的变量信息创建SerializeFieldData并存入UI基类的_fields变量列表;

通过PrefabStageUtility.GetCurrentPrefabStage().prefabContentsRoot可以拿到已打开Prefab的根节点。

private static void AddToFields(string varPrefix, string varType)
    {
        if (addToFieldToggle)
        {
            return;
        }
        if (Selection.count <= 0) return;
        var uiForm = GetPrefabRootComponent<UIFormBase>();
        if (uiForm == null)
        {
            Debug.LogWarning("UIForm Script is not exist.");
            return;
        }
        var targets = GetTargetsFromSelectedNodes(Selection.gameObjects);

        var fieldsProperties = uiForm.GetFieldsProperties();
        if (fieldsProperties == null) fieldsProperties = new SerializeFieldData[0];
        Undo.RecordObject(uiForm, uiForm.name);
        SerializeFieldData field = new SerializeFieldData(GenerateFieldName(fieldsProperties, targets), targets);
        field.VarType = varType;
        field.VarSampleType = GetSampleType(field.VarType).Name;
        field.VarPrefix = varPrefix;

        ArrayUtility.Add(ref fieldsProperties, field);
        uiForm.ModifyFieldsProperties(fieldsProperties);
        EditorUtility.SetDirty(uiForm);
        addToFieldToggle = true;
        removeToFieldToggle = false;
    }

6. Inspector面板扩展:

先了解一些比较常用的编辑器扩展API:

1.垂直布局: EditorGUILayout.BeginVertical();EditorGUILayout.EndVertical(); 需成对存在;

2.水平布局:EditorGUILayout.BeginHorizontal(); EditorGUILayout.EndHorizontal();需成对存在;

3.代码编译中:EditorApplication.isCompiling

4.资源导入中:EditorApplication.isUpdating

5.警示框:EditorGUILayout.HelpBox("Wiatting for compiling or updating...", MessageType.Warning);

 6.UI禁止交互; 

EditorGUI.BeginDisabledGroup(EditorApplication.isCompiling || EditorApplication.isUpdating);

EditorGUI.EndDisabledGroup();

成对存在,可控制被包裹的UI是否可交互。如上,当编译和导入资源过程让UI不可交互;

7.可折叠按钮:

boolValue = EditorGUILayout.BeginFoldoutHeaderGroup(boolValue, “Title”);

if(boolValue){
    可折叠的UI放到这里
}

 8. 下拉菜单

①EditorGUILayout.Popup:intValue为当前选择的菜单项索引

intValue = EditorGUILayout.Popup(intValue, varPrefixArr, GUILayout.MaxWidth(fieldPrefixWidth));

 ②EditorGUILayout.DropdownButton: 下拉按钮,优点是可以在下拉按钮点击后再加载下拉数据并显示,减少不必要性能消耗。点击下拉按钮后再创建GenericMenu(下拉列表):

if (EditorGUILayout.DropdownButton(new GUIContent(varPrefixProperty.stringValue), FocusType.Passive, GUILayout.MaxWidth(fieldPrefixWidth)))
            {
                GenericMenu popMenu = new GenericMenu();
                foreach (var varPrefix in varPrefixArr)
                {
                    popMenu.AddItem(new GUIContent(varPrefix), varPrefix.CompareTo(varPrefixProperty.stringValue) == 0, selectObj =>
                    {
                        varPrefixProperty.stringValue = selectObj.ToString();
                        serializedObject.ApplyModifiedProperties();
                    }, varPrefix);
                }
                popMenu.ShowAsContext();
            }

9. 可编辑文本框:

stringValue = GUILayout.TextField(stringValue, GUILayout.MinWidth(fieldItemWidth));

10. 按钮:

if (GUILayout.Button("+", GUILayout.Width(smallButtonWidth)))
{
    //按钮被点击
}

 11. 对齐:GUILayout.FlexibleSpace();GUILayout.Space(10);

GUILayout.FlexibleSpace(); 动态间距大小;GUILayout.Space(10)固定间距,空出10个像素

让Label右对齐:

EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField("Label");
EditorGUILayout.EndHorizontal();

 让Label左对齐:

EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Label");
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();

让Label居中对齐:

EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField("Label");
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
public override void OnInspectorGUI()
    {
        CheckAndInitFields();
        serializedObject.Update();
        EditorGUILayout.BeginVertical();

        bool disableAct = EditorApplication.isCompiling || EditorApplication.isUpdating || EditorApplication.isPlaying;
        if (disableAct)
        {
            EditorGUILayout.HelpBox("Wiatting for compiling or updating...", MessageType.Warning);
        }
        EditorGUILayout.BeginHorizontal();
        EditorGUI.BeginDisabledGroup(disableAct);

        if (GUILayout.Button("1. Generate Script")) //生成脚本
        {
            GenerateUIFormVariables(uiForm, serializedObject);
        }

        if (useSerializeMode && GUILayout.Button("2. Bind Properties")) //绑定变量
        {
            SerializeFieldProperties(serializedObject, uiForm.GetFieldsProperties());
        }
        if (GUILayout.Button("Open Script"))
        {
            var monoScript = MonoScript.FromMonoBehaviour(uiForm);
            string scriptFile = AssetDatabase.GetAssetPath(monoScript);
            InternalEditorUtility.OpenFileAtLineExternal(scriptFile, 0);
        }
        if (GUILayout.Button("Open Bind Script"))
        {
            var uiFormClassName = uiForm.GetType().Name;
            string scriptFile = UtilityBuiltin.ResPath.GetCombinePath(ConstEditor.UISerializeFieldDir, Utility.Text.Format("{0}.Variables.cs", uiFormClassName));
            InternalEditorUtility.OpenFileAtLineExternal(scriptFile, 0);
        }
        if (GUILayout.Button("Clear", GUILayout.MaxWidth(55)))
        {
            mFields.ClearArray();
        }
        EditorGUILayout.EndHorizontal();
        EditorGUILayout.BeginHorizontal();
        EditorGUI.BeginChangeCheck();

        useSerializeMode = EditorGUILayout.ToggleLeft("SerializeMode", useSerializeMode, GUILayout.MaxWidth(115));
        if (EditorGUI.EndChangeCheck())
        {
            UnityEditor.EditorPrefs.SetBool("SerializeMode", useSerializeMode);
        }
        GUILayout.FlexibleSpace();
        if (GUILayout.Button(EditorGUIUtility.TrIconContent("_Help", "使用说明"), GUIStyle.none))
        {
            EditorUtility.DisplayDialog("使用说明", "1.打开UI界面预制体.\n2.右键节点'[Add/Remove] UI Variable'添加/移除变量.\n3.在Inspector面板点击Generate Script生成代码.", "OK");
            GUIUtility.ExitGUI();
        }
        EditorGUILayout.EndHorizontal();
        for (int i = 0; i < mFields.arraySize; i++)
        {
            EditorGUILayout.BeginHorizontal();
            GUILayout.Label(i.ToString(), GUILayout.Width(30f));
            var item = mFields.GetArrayElementAtIndex(i);
            var varNameProperty = item.FindPropertyRelative("VarName");
            var varTypeProperty = item.FindPropertyRelative("VarType");
            var targetsProperty = item.FindPropertyRelative("Targets");
            var unfoldProperty = item.FindPropertyRelative("Foldout");
            var varPrefixProperty = item.FindPropertyRelative("VarPrefix");
            var varSampleType = item.FindPropertyRelative("VarSampleType");

            int targetsCount = targetsProperty != null ? targetsProperty.arraySize : 0;
            string targetsTitle = $"Size {targetsCount}";

            unfoldProperty.boolValue = EditorGUILayout.BeginFoldoutHeaderGroup(unfoldProperty.boolValue, targetsTitle);
            if (EditorGUILayout.DropdownButton(new GUIContent(varPrefixProperty.stringValue), FocusType.Passive, GUILayout.MaxWidth(fieldPrefixWidth)))
            {
                GenericMenu popMenu = new GenericMenu();
                foreach (var varPrefix in varPrefixArr)
                {
                    popMenu.AddItem(new GUIContent(varPrefix), varPrefix.CompareTo(varPrefixProperty.stringValue) == 0, selectObj =>
                    {
                        varPrefixProperty.stringValue = selectObj.ToString();
                        serializedObject.ApplyModifiedProperties();
                    }, varPrefix);
                }
                popMenu.ShowAsContext();
            }
            if (EditorGUILayout.DropdownButton(new GUIContent(varTypeProperty.stringValue), FocusType.Passive, GUILayout.MaxWidth(fieldTypeWidth)))
            {
                GenericMenu popMenu = new GenericMenu();
                var popContens = GetPopupContents(targetsProperty);
                foreach (var tpName in popContens)
                {
                    popMenu.AddItem(new GUIContent(tpName), tpName.CompareTo(varTypeProperty.stringValue) == 0, selectObj =>
                    {
                        varTypeProperty.stringValue = selectObj.ToString();
                        varSampleType.stringValue = GetSampleType(varTypeProperty.stringValue).Name;
                        serializedObject.ApplyModifiedProperties();
                    }, tpName);
                }
                popMenu.ShowAsContext();
            }

            varNameProperty.stringValue = GUILayout.TextField(varNameProperty.stringValue, GUILayout.MinWidth(fieldItemWidth));

            if (GUILayout.Button("+", GUILayout.Width(smallButtonWidth)))
            {
                InsertField(i + 1);
            }
            if (GUILayout.Button("-", GUILayout.Width(smallButtonWidth)))
            {
                RemoveField(i);
            }
            EditorGUILayout.EndHorizontal();
            if (i < mFields.arraySize && mFields.GetArrayElementAtIndex(i).FindPropertyRelative("Foldout").boolValue)
            {
                item = mFields.GetArrayElementAtIndex(i);
                targetsProperty = item.FindPropertyRelative("Targets");

                mCurFieldIdx = i;
                if (mReorderableList[i] == null)
                {
                    mReorderableList[i] = new ReorderableList(serializedObject, targetsProperty, true, false, true, true);
                    mReorderableList[i].drawElementCallback = DrawTargets;
                }
                else
                {
                    mReorderableList[i].serializedProperty = targetsProperty;
                }
                mReorderableList[i].DoLayoutList();
            }
            EditorGUILayout.EndFoldoutHeaderGroup();
        }

        if (serializedObject.hasModifiedProperties)
        {
            serializedObject.ApplyModifiedProperties();
            serializedObject.Update();
        }

        EditorGUI.EndDisabledGroup();
        EditorGUILayout.EndVertical();
        base.OnInspectorGUI();
    }

12. 获取GameObject上挂的脚本文件所在目录:

var monoScript = MonoScript.FromMonoBehaviour(monoBehaviorInstance);
string scriptPath = AssetDatabase.GetAssetPath(monoScript);//.cs脚本文件路径

13. 显示对话框:

EditorUtility.DisplayDialog("标题", "内容", "确定按钮",“取消按钮”);

14. 打开文件夹:EditorUtility.RevealInFinder(path);

选择文件:EditorUtility.OpenFilePanel()

选择文件夹:EditorUtility.OpenFolderPanel()

15. 可调数组顺序/可增删的列表ReorderableList用法:

mReorderableList[i] = new ReorderableList(serializedObject, arrayProperty, true, false, true, true);
mReorderableList[i].drawElementCallback = DrawTargets;

private void DrawTargets(Rect rect, int index, bool isActive, bool isFocused)
    {
        EditorGUI.BeginDisabledGroup(EditorApplication.isCompiling || EditorApplication.isUpdating || EditorApplication.isPlaying);
        var field = mFields.GetArrayElementAtIndex(mCurFieldIdx);
        var targetsProperty = field.FindPropertyRelative("Targets");
        var targetProperty = targetsProperty.GetArrayElementAtIndex(index);
        var curRect = new Rect(rect.x, rect.y, 20, EditorGUIUtility.singleLineHeight);
        EditorGUI.LabelField(curRect, index.ToString());
        curRect.x += 20;
        curRect.width = rect.width - 20;
        EditorGUI.ObjectField(curRect, targetProperty, GUIContent.none);
        EditorGUI.EndDisabledGroup();
    }

16. EditorPrefs,作用同PlayerPrefs,可用于保存编辑器设置等。如:EditorPrefs.SetBool("SerializeMode", useSerializeMode);

17. 通过调用编辑器API在代码里打开脚本文件:

InternalEditorUtility.OpenFileAtLineExternal(scriptFile, 0); 甚至可以指定打开并定位到代码的几行几列。

18. 支持撤销操作Undo.RecordObject(uiForm, uiForm.name); 例如移除UI变量时提前调用Undo.RecordObject记录对象状态,然后用户撤销操作时会自动回退到这一状态:

private void RemoveField(int idx)
    {
        Undo.RecordObject(uiForm, uiForm.name);//记录移除数组元素前的对象状态
        mFields.DeleteArrayElementAtIndex(idx);
        ArrayUtility.RemoveAt(ref mReorderableList, idx);
    }

 19. 使用Unity编辑器内置图标。Unity编辑器有很多内置图标,并且是可以用获取到共用的。获取编辑器内置图标API: EditorGUIUtility.TrIconContent(), EditorGUIUtility.IconContent(); 只需传入图标名即可。

如显示一个帮助按钮, IconContent和TrIconContent的区别就是,TrIconContent("_Help", "使用说明")当鼠标悬停在图标上会有显示提示; 

if (GUILayout.Button(EditorGUIUtility.TrIconContent("_Help", "使用说明"), GUIStyle.none))
        {
            EditorUtility.DisplayDialog("使用说明", "1.打开UI界面预制体.\n2.右键节点'[Add/Remove] UI Variable'添加/移除变量.\n3.在Inspector面板点击Generate Script生成代码.", "OK");
            GUIUtility.ExitGUI();
        }

如何知道Unity所有内置图标名称呢?可以写个编辑器通过Resources.FindObjectsOfTypeAll<Texture2D>()获取全部图标,并显示出每个图标和对应图标名,一目了然。

当然也有人已经整理列出了图标及名称,但是由于Unity版本不同图标样式也会有区别,白嫖地址:GitHub - halak/unity-editor-icons

20. 一键清理prefab丢失脚本Missing Scrips, GameObjectUtility.RemoveMonoBehavioursWithMissingScript(pfb);

[MenuItem("Game Framework/GameTools/Clear Missing Scripts【清除Prefab丢失脚本】")]
    public static void ClearMissingScripts()
    {
        var pfbArr = AssetDatabase.FindAssets("t:Prefab");
        foreach (var item in pfbArr)
        {
            var pfbFileName = AssetDatabase.GUIDToAssetPath(item);
            var pfb = AssetDatabase.LoadAssetAtPath<GameObject>(pfbFileName);
            GameObjectUtility.RemoveMonoBehavioursWithMissingScript(pfb);
        }
    }

自动生成UI变量核心代码:

#if UNITY_EDITOR
using UnityEditor.SceneManagement;
using System.Linq;
using UnityEditor;
using System.Collections.Generic;
using UnityEngine;
using UnityEditorInternal;
using System;
using GameFramework;
using System.Text.RegularExpressions;
using System.Text;
using System.IO;


[CustomEditor(typeof(UIFormBase), true)]
public class UIFormBaseEditor : Editor
{
    readonly static string[] varPrefixArr = { "private", "protected", "public" };
    const string arrFlag = "Arr";
    const float fieldItemWidth = 100;
    const float fieldPrefixWidth = 100;
    const float fieldTypeWidth = 240;
    const float smallButtonWidth = 25;
    SerializedProperty mFields;
    static bool addToFieldToggle;
    static bool removeToFieldToggle;
    ReorderableList[] mReorderableList;
    UIFormBase uiForm;
    int mCurFieldIdx;

    bool useSerializeMode = false;
    static int varPrefixIndex = -1;
    static bool mShowSelectTypeMenu;

    #region #右键菜单
    private static GUIStyle normalStyle;
    private static GUIStyle selectedStyle;
    [InitializeOnLoadMethod]
    static void InitEditor()
    {

        normalStyle = new GUIStyle();
        normalStyle.normal.textColor = Color.white;

        selectedStyle = new GUIStyle();
        selectedStyle.normal.textColor = Color.green;
        Selection.selectionChanged = () =>
        {
            addToFieldToggle = false;
            removeToFieldToggle = false;
        };

        EditorApplication.hierarchyWindowItemOnGUI = delegate (int id, Rect rect)
        {
            OpenSelectComponentMenuListener(rect);
            var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
            if (prefabStage == null)
            {
                return;
            }
            var uiForm = prefabStage.prefabContentsRoot.GetComponent<UIFormBase>();
            if (uiForm == null)
            {
                return;
            }
            var curDrawNode = EditorUtility.InstanceIDToObject(id) as GameObject;
            if (curDrawNode == null)
            {
                return;
            }
            var fields = uiForm.GetFieldsProperties();
            SerializeFieldData drawItem = null;
            foreach (var item in fields)
            {
                if (item == null) continue;
                if (ArrayUtility.Contains(item.Targets, curDrawNode))
                {
                    drawItem = item;
                    break;
                }
            }
            if (drawItem != null)
            {
                rect.x = rect.xMax - Mathf.Min(300, rect.size.x * 0.35f);
                GUI.Label(rect, string.Format("{0} {1} {2}", drawItem.VarPrefix, drawItem.VarSampleType, drawItem.VarName), selectedStyle);
            }

        };
    }
    [MenuItem("GameObject/UIForm Fields Tool/Add private", false, priority = 2)]
    private static void AddPrivateVariable2UIForm()
    {
        varPrefixIndex = 0;
        mShowSelectTypeMenu = true;
    }
    [MenuItem("GameObject/UIForm Fields Tool/Add protected", false, priority = 3)]
    private static void AddProtectedVariable2UIForm()
    {
        varPrefixIndex = 1;
        mShowSelectTypeMenu = true;
    }
    [MenuItem("GameObject/UIForm Fields Tool/Add public", false, priority = 4)]
    private static void AddPublicVariable2UIForm()
    {
        varPrefixIndex = 2;
        mShowSelectTypeMenu = true;
    }

    [MenuItem("GameObject/UIForm Fields Tool/Remove", false, priority = 5)]
    private static void RemoveUIFormVariable()
    {
        if (removeToFieldToggle)
        {
            return;
        }
        if (Selection.count <= 0) return;

        var uiForm = GetPrefabRootComponent<UIFormBase>();
        if (uiForm == null)
        {
            Debug.LogWarning("UIForm Script is not exist.");
            return;
        }
        var fieldsProperties = uiForm.GetFieldsProperties();
        if (fieldsProperties == null) return;
        Undo.RecordObject(uiForm, uiForm.name);
        for (int i = 0; i < Selection.gameObjects.Length; i++)
        {
            var itm = Selection.gameObjects[i];
            if (itm == null) continue;

            for (int j = fieldsProperties.Length - 1; j >= 0; j--)
            {
                var fields = fieldsProperties[j];
                if (fields == null || fields.Targets == null || fields.Targets.Length <= 0) continue;
                for (int k = fields.Targets.Length - 1; k >= 0; k--)
                {
                    if (fields.Targets[k] == itm)
                    {
                        if (fields.Targets.Length <= 1)
                        {
                            ArrayUtility.RemoveAt(ref fieldsProperties, j);
                        }
                        else
                        {
                            ArrayUtility.RemoveAt(ref fields.Targets, k);
                        }
                    }
                }
            }
        }
        uiForm.ModifyFieldsProperties(fieldsProperties);
        EditorUtility.SetDirty(uiForm);
        removeToFieldToggle = true;
        addToFieldToggle = false;
    }
    /// <summary>
    /// 不带名字空间的类型名
    /// </summary>
    /// <returns></returns>
    private static Type GetSampleType(string fullName)
    {
        Type result = null;
        var assemblyArr = AppDomain.CurrentDomain.GetAssemblies();
        foreach (var item in assemblyArr)
        {
            foreach (var tp in item.GetTypes())
            {
                if (tp.FullName.CompareTo(fullName) == 0)
                {
                    result = tp;
                    break;
                }
            }
            if (result != null) break;
        }

        return result;
    }
    private static T GetPrefabRootComponent<T>() where T : Component
    {
        var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
        if (prefabStage == null)
        {
            Debug.LogWarning("GetCurrentPrefabStage is null.");
            return null;
        }
        return prefabStage.prefabContentsRoot.GetComponent<T>();
    }
    private static void AddToFields(string varPrefix, string varType)
    {
        if (addToFieldToggle)
        {
            return;
        }
        if (Selection.count <= 0) return;
        var uiForm = GetPrefabRootComponent<UIFormBase>();
        if (uiForm == null)
        {
            Debug.LogWarning("UIForm Script is not exist.");
            return;
        }
        var targets = GetTargetsFromSelectedNodes(Selection.gameObjects);

        var fieldsProperties = uiForm.GetFieldsProperties();
        if (fieldsProperties == null) fieldsProperties = new SerializeFieldData[0];
        Undo.RecordObject(uiForm, uiForm.name);
        SerializeFieldData field = new SerializeFieldData(GenerateFieldName(fieldsProperties, targets), targets);
        field.VarType = varType;
        field.VarSampleType = GetSampleType(field.VarType).Name;
        field.VarPrefix = varPrefix;

        ArrayUtility.Add(ref fieldsProperties, field);
        uiForm.ModifyFieldsProperties(fieldsProperties);
        EditorUtility.SetDirty(uiForm);
        addToFieldToggle = true;
        removeToFieldToggle = false;
    }
    private static GameObject[] GetTargetsFromSelectedNodes(GameObject[] selectedList)
    {
        GameObject[] targets = new GameObject[selectedList.Length];
        for (int i = 0; i < selectedList.Length; i++)
        {
            targets[i] = selectedList[i];
        }
        targets = targets.OrderBy(go => go.transform.GetSiblingIndex()).ToArray();
        return targets;
    }
    #endregion

    private void OnEnable()
    {
        useSerializeMode = EditorPrefs.GetBool("SerializeMode", true);
        varPrefixIndex = 0;
        mShowSelectTypeMenu = false;
        uiForm = (target as UIFormBase);
        if (uiForm.GetFieldsProperties() == null)
        {
            uiForm.ModifyFieldsProperties(new SerializeFieldData[0]);
        }
        mFields = serializedObject.FindProperty("_fields");
        mReorderableList = new ReorderableList[mFields.arraySize];
    }


    private static void OpenSelectComponentMenuListener(Rect rect)
    {
        if (mShowSelectTypeMenu)
        {
            int idx = -1;
            var strArr = GetPopupContents(GetTargetsFromSelectedNodes(Selection.gameObjects));
            var contents = new GUIContent[strArr.Length];
            for (int i = 0; i < strArr.Length; i++)
            {
                contents[i] = new GUIContent(strArr[i]);
            }
            rect.width = 200;
            rect.height = MathF.Max(100, contents.Length * rect.height);
            EditorUtility.DisplayCustomMenu(rect, contents, idx, (userData, contents, selected) =>
            {
                AddToFields(varPrefixArr[varPrefixIndex], contents[selected]);
            }, null);
            mShowSelectTypeMenu = false;
        }
    }

    public override void OnInspectorGUI()
    {
        CheckAndInitFields();
        serializedObject.Update();
        EditorGUILayout.BeginVertical();

        bool disableAct = EditorApplication.isCompiling || EditorApplication.isUpdating || EditorApplication.isPlaying;
        if (disableAct)
        {
            EditorGUILayout.HelpBox("Wiatting for compiling or updating...", MessageType.Warning);
        }
        EditorGUILayout.BeginHorizontal();
        EditorGUI.BeginDisabledGroup(disableAct);

        if (GUILayout.Button("1. Generate Script")) //生成脚本
        {
            GenerateUIFormVariables(uiForm, serializedObject);
        }

        if (useSerializeMode && GUILayout.Button("2. Bind Properties")) //绑定变量
        {
            SerializeFieldProperties(serializedObject, uiForm.GetFieldsProperties());
        }
        if (GUILayout.Button("Open Script"))
        {
            var monoScript = MonoScript.FromMonoBehaviour(uiForm);
            string scriptFile = AssetDatabase.GetAssetPath(monoScript);
            InternalEditorUtility.OpenFileAtLineExternal(scriptFile, 0);
        }
        if (GUILayout.Button("Open Bind Script"))
        {
            var uiFormClassName = uiForm.GetType().Name;
            string scriptFile = UtilityBuiltin.ResPath.GetCombinePath(ConstEditor.UISerializeFieldDir, Utility.Text.Format("{0}.Variables.cs", uiFormClassName));
            InternalEditorUtility.OpenFileAtLineExternal(scriptFile, 0);
        }
        if (GUILayout.Button("Clear", GUILayout.MaxWidth(55)))
        {
            mFields.ClearArray();
        }
        EditorGUILayout.EndHorizontal();
        EditorGUILayout.BeginHorizontal();
        EditorGUI.BeginChangeCheck();

        useSerializeMode = EditorGUILayout.ToggleLeft("SerializeMode", useSerializeMode, GUILayout.MaxWidth(115));
        if (EditorGUI.EndChangeCheck())
        {
            UnityEditor.EditorPrefs.SetBool("SerializeMode", useSerializeMode);
        }
        GUILayout.FlexibleSpace();
        if (GUILayout.Button(EditorGUIUtility.TrIconContent("_Help", "使用说明"), GUIStyle.none))
        {
            EditorUtility.DisplayDialog("使用说明", "1.打开UI界面预制体.\n2.右键节点'[Add/Remove] UI Variable'添加/移除变量.\n3.在Inspector面板点击Generate Script生成代码.", "OK");
            GUIUtility.ExitGUI();
        }
        EditorGUILayout.EndHorizontal();
        for (int i = 0; i < mFields.arraySize; i++)
        {
            EditorGUILayout.BeginHorizontal();
            GUILayout.Label(i.ToString(), GUILayout.Width(30f));
            var item = mFields.GetArrayElementAtIndex(i);
            var varNameProperty = item.FindPropertyRelative("VarName");
            var varTypeProperty = item.FindPropertyRelative("VarType");
            var targetsProperty = item.FindPropertyRelative("Targets");
            var unfoldProperty = item.FindPropertyRelative("Foldout");
            var varPrefixProperty = item.FindPropertyRelative("VarPrefix");
            var varSampleType = item.FindPropertyRelative("VarSampleType");

            int targetsCount = targetsProperty != null ? targetsProperty.arraySize : 0;
            string targetsTitle = $"Size {targetsCount}";

            unfoldProperty.boolValue = EditorGUILayout.BeginFoldoutHeaderGroup(unfoldProperty.boolValue, targetsTitle);
            if (EditorGUILayout.DropdownButton(new GUIContent(varPrefixProperty.stringValue), FocusType.Passive, GUILayout.MaxWidth(fieldPrefixWidth)))
            {
                GenericMenu popMenu = new GenericMenu();
                foreach (var varPrefix in varPrefixArr)
                {
                    popMenu.AddItem(new GUIContent(varPrefix), varPrefix.CompareTo(varPrefixProperty.stringValue) == 0, selectObj =>
                    {
                        varPrefixProperty.stringValue = selectObj.ToString();
                        serializedObject.ApplyModifiedProperties();
                    }, varPrefix);
                }
                popMenu.ShowAsContext();
            }
            if (EditorGUILayout.DropdownButton(new GUIContent(varTypeProperty.stringValue), FocusType.Passive, GUILayout.MaxWidth(fieldTypeWidth)))
            {
                GenericMenu popMenu = new GenericMenu();
                var popContens = GetPopupContents(targetsProperty);
                foreach (var tpName in popContens)
                {
                    popMenu.AddItem(new GUIContent(tpName), tpName.CompareTo(varTypeProperty.stringValue) == 0, selectObj =>
                    {
                        varTypeProperty.stringValue = selectObj.ToString();
                        varSampleType.stringValue = GetSampleType(varTypeProperty.stringValue).Name;
                        serializedObject.ApplyModifiedProperties();
                    }, tpName);
                }
                popMenu.ShowAsContext();
            }

            varNameProperty.stringValue = GUILayout.TextField(varNameProperty.stringValue, GUILayout.MinWidth(fieldItemWidth));

            if (GUILayout.Button("+", GUILayout.Width(smallButtonWidth)))
            {
                InsertField(i + 1);
            }
            if (GUILayout.Button("-", GUILayout.Width(smallButtonWidth)))
            {
                RemoveField(i);
            }
            EditorGUILayout.EndHorizontal();
            if (i < mFields.arraySize && mFields.GetArrayElementAtIndex(i).FindPropertyRelative("Foldout").boolValue)
            {
                item = mFields.GetArrayElementAtIndex(i);
                targetsProperty = item.FindPropertyRelative("Targets");

                mCurFieldIdx = i;
                if (mReorderableList[i] == null)
                {
                    mReorderableList[i] = new ReorderableList(serializedObject, targetsProperty, true, false, true, true);
                    mReorderableList[i].drawElementCallback = DrawTargets;
                }
                else
                {
                    mReorderableList[i].serializedProperty = targetsProperty;
                }
                mReorderableList[i].DoLayoutList();
            }
            EditorGUILayout.EndFoldoutHeaderGroup();
        }

        if (serializedObject.hasModifiedProperties)
        {
            serializedObject.ApplyModifiedProperties();
            serializedObject.Update();
        }

        EditorGUI.EndDisabledGroup();
        EditorGUILayout.EndVertical();
        base.OnInspectorGUI();
    }
    /// <summary>
    /// 生成UI脚本.cs
    /// </summary>
    /// <param name="uiForm"></param>
    /// <param name="uiFormSerializer"></param>
    private void GenerateUIFormVariables(UIFormBase uiForm, SerializedObject uiFormSerializer)
    {
        if (uiForm == null) return;

        var monoScript = MonoScript.FromMonoBehaviour(uiForm);
        var uiFormClassName = monoScript.GetClass().Name;
        string scriptFile = UtilityBuiltin.ResPath.GetCombinePath(ConstEditor.UISerializeFieldDir, Utility.Text.Format("{0}.Variables.cs", uiFormClassName));
        var fields = uiForm.GetFieldsProperties();
        if (fields == null || fields.Length <= 0)
        {
            if (File.Exists(scriptFile))
            {
                File.Delete(scriptFile);
            }
            var metaFile = scriptFile + ".meta";
            if (File.Exists(metaFile))
            {
                File.Delete(metaFile);
            }
            AssetDatabase.Refresh();
            return;
        }
        var matchResult = Regex.Match(monoScript.text, Utility.Text.Format("partial[\\s]+class[\\s]+{0}", uiFormClassName));
        string scriptPath = AssetDatabase.GetAssetPath(monoScript);
        if (!matchResult.Success)
        {
            EditorUtility.DisplayDialog("生成UI变量失败!", Utility.Text.Format("请先手动为{0}类添加'partial'修饰符!\n{1}", uiFormClassName, scriptPath), "OK");
            return;
        }
        List<string> nameSpaceList = new List<string> { "UnityEngine" };//默认自带的名字空间
        List<string> fieldList = new List<string>();
        foreach (var field in fields)
        {
            if (string.IsNullOrWhiteSpace(field.VarType) || string.IsNullOrWhiteSpace(field.VarName))
            {
                continue;
            }
            var varType = GetSampleType(field.VarType);
            if (varType == null)
            {
                continue;
            }
            if (!string.IsNullOrEmpty(varType.Namespace) && !nameSpaceList.Contains(varType.Namespace))
            {
                nameSpaceList.Add(varType.Namespace);
            }
            bool isArray = field.Targets.Length > 1;

            var varPrefix = field.VarPrefix;// varPrefixArr[field.VarPrefixSelectedIdx];
            string serializeFieldPrefix = useSerializeMode ? "[SerializeField] " : "";
            string fieldLine;
            if (isArray)
            {
                fieldLine = Utility.Text.Format("{0}{1} {2}[] {3} = null;", serializeFieldPrefix, varPrefix, varType.Name, field.VarName);
            }
            else
            {
                fieldLine = Utility.Text.Format("{0}{1} {2} {3} = null;", serializeFieldPrefix, varPrefix, varType.Name, field.VarName);
            }
            fieldList.Add(fieldLine);
        }
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.AppendLine("//---------------------------------");
        stringBuilder.AppendLine("//此文件由工具自动生成,请勿手动修改");
        stringBuilder.AppendLine($"//更新自:{CloudProjectSettings.userName}");
        stringBuilder.AppendLine($"//更新时间:{DateTime.Now}");
        stringBuilder.AppendLine("//---------------------------------");
        foreach (var item in nameSpaceList)
        {
            stringBuilder.AppendLine(Utility.Text.Format("using {0};", item));
        }
        string uiFormClassNameSpace = monoScript.GetClass().Namespace;
        bool hasNameSpace = !string.IsNullOrWhiteSpace(uiFormClassNameSpace);
        if (hasNameSpace)
        {
            stringBuilder.AppendLine(Utility.Text.Format("namespace {0}", uiFormClassNameSpace));
            stringBuilder.AppendLine("{");
        }
        stringBuilder.AppendLine(Utility.Text.Format("public partial class {0}", uiFormClassName));
        stringBuilder.AppendLine("{");
        if (useSerializeMode)
        {
            stringBuilder.AppendLine("\t[Space(10)]");
            stringBuilder.AppendLine("\t[Header(\"Auto Genertate Properties:\")]");
        }
        foreach (var item in fieldList)
        {
            stringBuilder.AppendLine("\t" + item);
        }
        //不使用Serialize模式时直接生成获取组件的代码
        if (!useSerializeMode)
        {
            GeneratePropertiesUseGetComponent(stringBuilder, fields);
        }
        stringBuilder.AppendLine("}");
        if (hasNameSpace) stringBuilder.AppendLine("}");

        File.WriteAllText(scriptFile, stringBuilder.ToString());
        AssetDatabase.Refresh();
        //SerializeFieldProperties(uiFormSerializer, fields);
    }
    private void GeneratePropertiesUseGetComponent(StringBuilder stringBuilder, SerializeFieldData[] fields)
    {
        stringBuilder.AppendLine("\tprotected override void InitUIProperties()");
        stringBuilder.AppendLine("\t{");
        stringBuilder.AppendLine("\t\tvar fields = this.GetFieldsProperties();");
        for (int i = 0; i < fields.Length; i++)
        {
            var field = fields[i];
            bool isArray = field.Targets.Length > 1;
            bool isGameObject = field.VarType.CompareTo(typeof(GameObject).FullName) == 0;
            if (isArray)
            {
                if (isGameObject)
                    stringBuilder.AppendLine(Utility.Text.Format("\t\t{0} = fields[{1}].Targets;", field.VarName, i));
                else
                    stringBuilder.AppendLine(Utility.Text.Format("\t\t{0} = fields[{1}].GetComponents<{2}>();", field.VarName, i, field.VarSampleType));
            }
            else
            {
                if (isGameObject)
                    stringBuilder.AppendLine(Utility.Text.Format("\t\t{0} = fields[{1}].Targets[0];", field.VarName, i));
                else
                    stringBuilder.AppendLine(Utility.Text.Format("\t\t{0} = fields[{1}].GetComponent<{2}>(0);", field.VarName, i, field.VarSampleType));
            }
        }
        stringBuilder.AppendLine("\t}");
    }
    private void SerializeFieldProperties(SerializedObject serializedObject, SerializeFieldData[] fields)
    {
        if (serializedObject == null)
        {
            Debug.LogError("生成UI SerializedField失败, serializedObject为null");
            return;
        }

        foreach (var item in fields)
        {
            string varName = item.VarName;
            string varType = item.VarType;
            bool isGameObject = varType.CompareTo(typeof(GameObject).FullName) == 0;
            var property = serializedObject.FindProperty(varName);
            if (property == null) continue;
            if (item.Targets.Length <= 1)
            {
                property.objectReferenceValue = isGameObject ? item.Targets[0] : item.Targets[0]?.GetComponent(GetSampleType(varType));
            }
            else if (property.isArray)
            {
                property.ClearArray();
                for (int i = 0; i < item.Targets.Length; i++)
                {
                    if (i >= property.arraySize)
                    {
                        property.InsertArrayElementAtIndex(i);
                    }
                    property.GetArrayElementAtIndex(i).objectReferenceValue = isGameObject ? item.Targets[i] : item.Targets[i]?.GetComponent(GetSampleType(varType));
                }
                //for (int i = property.arraySize - 1; i >= item.Targets.Length; i--)
                //{
                //    property.DeleteArrayElementAtIndex(i);
                //}
            }
        }
        serializedObject.ApplyModifiedProperties();
    }

    private void CheckAndInitFields()
    {
        if (uiForm.GetFieldsProperties() == null)
        {
            uiForm.ModifyFieldsProperties(new SerializeFieldData[0]);
        }
        if (mFields == null) mFields = serializedObject.FindProperty("_fields");

        if (mFields.arraySize != mReorderableList.Length)
        {
            if (mFields.arraySize > mReorderableList.Length)
            {
                for (int i = mReorderableList.Length; i < mFields.arraySize; i++)
                {
                    ArrayUtility.Insert(ref mReorderableList, mReorderableList.Length, null);
                }
            }
            else
            {
                for (int i = mFields.arraySize; i < mReorderableList.Length; i++)
                {
                    ArrayUtility.RemoveAt(ref mReorderableList, mReorderableList.Length - 1);
                }
            }
        }
    }

    private void InsertField(int idx)
    {
        Undo.RecordObject(uiForm, uiForm.name);
        mFields.InsertArrayElementAtIndex(idx);
        ArrayUtility.Insert(ref mReorderableList, idx, null);
        var lastField = mFields.GetArrayElementAtIndex(idx);
        if (lastField != null)
        {
            var lastVarName = lastField.FindPropertyRelative("VarName");
            if (!string.IsNullOrEmpty(lastVarName.stringValue))
            {
                lastVarName.stringValue += idx.ToString();
            }
        }
    }
    private void RemoveField(int idx)
    {
        Undo.RecordObject(uiForm, uiForm.name);
        mFields.DeleteArrayElementAtIndex(idx);
        ArrayUtility.RemoveAt(ref mReorderableList, idx);
    }
    private void DrawTargets(Rect rect, int index, bool isActive, bool isFocused)
    {
        EditorGUI.BeginDisabledGroup(EditorApplication.isCompiling || EditorApplication.isUpdating || EditorApplication.isPlaying);
        var field = mFields.GetArrayElementAtIndex(mCurFieldIdx);
        var targetsProperty = field.FindPropertyRelative("Targets");
        var targetProperty = targetsProperty.GetArrayElementAtIndex(index);
        var curRect = new Rect(rect.x, rect.y, 20, EditorGUIUtility.singleLineHeight);
        EditorGUI.LabelField(curRect, index.ToString());
        curRect.x += 20;
        curRect.width = rect.width - 20;
        EditorGUI.ObjectField(curRect, targetProperty, GUIContent.none);
        EditorGUI.EndDisabledGroup();
    }
    private static string[] GetPopupContents(GameObject[] targets)
    {
        if (targets == null || targets.Length <= 0)
        {
            return new string[0];
        }
        var typeNames = GetIntersectionComponents(targets);
        if (typeNames == null || typeNames.Length <= 0)
        {
            return new string[0];
        }
        ArrayUtility.Insert(ref typeNames, 0, typeof(GameObject).FullName);
        return typeNames;
    }
    private static string[] GetPopupContents(SerializedProperty targets)
    {
        var goArr = new GameObject[targets.arraySize];
        for (int i = 0; i < targets.arraySize; i++)
        {
            var pp = targets.GetArrayElementAtIndex(i);
            goArr[i] = (pp != null && pp.objectReferenceValue != null) ? (pp.objectReferenceValue as GameObject) : null;
        }
        return GetPopupContents(goArr);
    }
    private static string[] GetIntersectionComponents(GameObject[] targets)
    {
        var firstItm = targets[0];
        if (firstItm == null)
        {
            return new string[0];
        }
        var coms = firstItm.GetComponents(typeof(Component));
        coms = coms.Distinct().ToArray();//去重

        for (int i = coms.Length - 1; i >= 1; i--)
        {
            var comType = coms[i].GetType().FullName;
            bool allContains = true;
            for (int j = 1; j < targets.Length; j++)
            {
                var target = targets[j];
                if (target == null) return new string[0];
                var tComs = target.GetComponents(typeof(Component));
                bool containsType = false;
                for (int k = 0; k < tComs.Length; k++)
                {
                    if (tComs[k].GetType().FullName.CompareTo(comType) == 0)
                    {
                        containsType = true;
                        break;
                    }
                }
                allContains &= containsType;
                if (!allContains) break;
            }
            if (!allContains)
            {
                ArrayUtility.RemoveAt(ref coms, i);
            }
        }
        string[] typesArr = new string[coms.Length];
        for (int i = 0; i < coms.Length; i++)
        {
            typesArr[i] = coms[i].GetType().FullName;
        }
        return typesArr;
    }

    /// <summary>
    /// 生成一个与变量列表里不重名的变量名
    /// </summary>
    /// <param name="fields"></param>
    /// <param name="name"></param>
    /// <returns></returns>
    private static string GenerateFieldName(SerializeFieldData[] fields, GameObject[] targets)
    {
        var go = targets[0];
        string varName = Regex.Replace(go.name, "[^\\w]", string.Empty);
        if (fields == null || fields.Length <= 0)
        {
            return FirstCharToLower(targets.Length > 1 ? varName + arrFlag : varName);
        }
        bool contains = false;

        foreach (SerializeFieldData item in fields)
        {
            if (item != null && item.VarName.CompareTo(varName) == 0)
            {
                contains = true;
            }
        }
        if (targets.Length > 1)
        {
            varName += arrFlag;
        }
        if (contains)
        {
            varName += go.GetInstanceID();
        }

        return FirstCharToLower(varName);
    }
    private static string FirstCharToLower(string str)
    {
        if (string.IsNullOrEmpty(str))
        {
            return string.Empty;
        }
        return str.Substring(0, 1).ToLower() + str.Substring(1);
    }
}
#endif

猜你喜欢

转载自blog.csdn.net/final5788/article/details/126570070
今日推荐