前言

使用GF框架时,有没有发现很神奇的情况,继承任何模块的辅助器基类脚本(Helper)都会被检视面板自动识别,这里以GF框架为例讲述一下如何做到自动识别脚本的。

1.自动识别脚本

不知道GF框架是何物的,也不影响这篇文章的观看,这里先讲述一下具体效果,按照框架模块中的本地化模块为例,分析GF框架是如何更新检视面板下的辅助器枚举,首先看到以下截图:

unity 实现点击替换按钮_ide

Localization Helper下的枚举就是自动识别的,创建出的脚本继承了DefaultLocalizationHelper,并且脚本的路径在Asset下,它这里选项就自动会添加刚刚创建的脚本,amazing!!!怎么做到的这个功能的,也太神奇了。 

unity 实现点击替换按钮_Text_02

为什么在下的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),框架启动时通过反射将创建出本地化辅助器实例,如此一来就实现自定义和扩展框架功能了,是不是感觉屌炸天了。

unity 实现点击替换按钮_Text_03

 2.花里胡哨的检视(Inspector)界面

看到Unity自带的组件检视界面是如此花里胡哨的(比如Button,Material这些花里胡哨检视界面),用时并且想模仿出类似的检视界面,有这个想法的话就已经成功一半了,毕竟只要想模仿才是迈出成功的第一步,首先看一下按钮组件的检视界面长啥样,虽然没有吃过猪肉起码看过猪跑,为了和模拟出来的界面进行比较,还是把自带按钮的检视界面给各位放出来看看。

unity 实现点击替换按钮_ide_04

标准按钮组件的检视界面就是长成这样的,接下来就模拟一下按钮组件的检视界面样式,经过笔者一段时间猛如虎的操作,再展示模拟出的按钮检视界面效果之前,各位看官注意拿好手机或抱好电脑屏幕,具体图片如下:

unity 实现点击替换按钮_UGUI相关_05

雌兔脚扑朔,雄兔眼迷离。可能会说狗贼别拿Photoshop以后的效果来唬弄,这里是分享知识的地方,怎么就被玷污了,这百分百是p出来的。对不起各位看官!这个确实靠重写OnInspectorGUI函数得到的效果。

unity 实现点击替换按钮_UGUI相关_06

接下来就展示一下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会自动去帮你替换脚本图标, 以上的脚本只是模仿检视界面,没有任何实际的功能,俗话说的好花瓶虽然好看,但是一点用没有。

unity 实现点击替换按钮_UGUI相关_07

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

花瓶好看却是毫无实际功能,怎么办呢?接下来我就要给大家一份厚礼了,记得关注投币喂食三连,不对不对,禁止投食...

Unity官方下载UGUI源代码链接是:https://bitbucket.org/Unity-Technologies/ui/downloads/?tab=tags

网速属实太慢的话,给大家上传到csdn了:

工程已经下载好了,迫不及待开始部署UIGUI到Unity里进行学习吧,先查看下载过来的压缩包有那些东西,可以看到以下的文件夹,具体截图如下:

unity 实现点击替换按钮_UGUI相关_08

只需要把UnityEngine.UI放到Unity下即可,然后把UnityEditor.UI、UnityEngine.UI-Editor放到Assets/Editor路径。记住这些文件夹下所有和代码不相关的东西都可以删掉,还需要移除掉Editor\Data\UnityExtensions\Unity\GUISystem文件夹,然后就是创建UnityEditor.UI和UnityEngine.UI的Assembly Denfinition,查看打印什么错误给它们添加上缺失的引用,具体的引用添加如下图:

unity 实现点击替换按钮_unity 实现点击替换按钮_09

Unity2018以上有一个坑爹的地方就是Packages里的有一个TestMeshPro引用了UnityEngine.UI,但是这个包的所有文件是不让修改的,无法给它添加上引用,如图所示:

unity 实现点击替换按钮_unity_10

直接通过Window下的Package Manager选项移除掉这个包,具体界面如下:

unity 实现点击替换按钮_unity 实现点击替换按钮_11

之后就可以调试UGUI模块了,如果各位有什么比较好的想法需要添加集成到Unity的UGUI模块里,这时可以去替换GUISystem文件夹下所有文件,就是自行定制UGUI模块了 ,具体截图如下:

unity 实现点击替换按钮_unity 实现点击替换按钮_12

需要注意的是,Unity的mdb而不是pdb,所以还需要一个工具将pdb转成mdb,所有pdb都需要转换,Unity有自带的转换工具交pdb2mdb.exe,通过命令行执行这个程序,具体执行界面如下:

unity 实现点击替换按钮_UGUI相关_13

 先写上pdb2mdb.exe然后将dll直接拖动到命令行下,路径就会自动出来。