前言
使用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了:
工程已经下载好了,迫不及待开始部署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直接拖动到命令行下,路径就会自动出来。