前言
使用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