FairyGUI-Unity侧菜单扩展
目录
缘由:
分析:
准备:
完整代码:
缘由:
在使用FairyGUI作为项目UI开发时,有时会使用FairyGUI提供的Scripting Define Symbols。当前FairyGUI中的Scripting Define Symbols有:
骨骼动画 Spine:FAIRYGUI_SPINE,龙骨:FAIRYGUI_DRAGONBONES
字体 TextMeshPro:FAIRYGUI_TMPRO
使用ToLua:FAIRYGUI_TOLUA
使用Puerts:FAIRYGUI_PUERTS
显示阿拉伯文本:RTL_TEXT_SUPPORT
UI自动化测试:FAIRYGUI_TEST
为了方便开发,以上Scripting Define Symbols我将其做成了Unity的菜单,直接看完整代码。
分析:
在开始制作菜单之前,需要做一些准备工作。比如上述的Scripting Define Symbols是否需要额外的Unity资源包,是否需要熟悉Unity编辑器中的一些方法才能进行?
而且需要注意的是新建的Unity工程中,TextMeshPro组件的必要资源库是需要手动导入或引用的,所以在使用FGUI提供的FAIRYGUI_TMPRO时,需要对TextMeshPro包进行检测。
对于编辑器中的Scripting Define Symbols设置可以通过方法GetScriptingDefineSymbolsForGrouph和SetScriptingDefineSymbolsForGroup进行操作。菜单状态则可以通过方法:GetChecked和SetChecked来进行操作。菜单状态的变化Scripting Define Symbols设置有关,所以还要自定义一个同步刷新菜单状态的方法RefreshMenuState。
准备:
分析结束,我们在代码中写一下上面的分析结果。新建脚本EditorMenuTool,空间名设置为FairyGUIEditor,添加对UnityEditor的引用。将新建的EditorMenuTool脚本,放到FairyGUI的Editor目录下(也可以根据项目目录结构放置),打开脚本。
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;namespace FairyGUIEditor
{public class EditorMenuTool{}
}
#endif
Scripting Define Symbols的操作方法:
/// <summary>/// 获取Scripting Define Symbols的值/// </summary>/// <returns></returns>private static string GetScriptingDefineSymbolsForGroup(){return PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup);}/// <summary>/// 设置Scripting Define Symbols的值/// </summary>/// <param name="newSymbol">新的宏定义</param>private static void SetScriptingDefineSymbolsForGroup(string newSymbol){PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup,newSymbol);}/// <summary>/// 检测Scripting Define Symbols中是否存在目标值/// </summary>/// <param name="define">目标宏</param>/// <returns></returns>private static bool CheckScriptingDefineSymbolsExist(string define){string symbol = GetScriptingDefineSymbolsForGroup();return symbol.Contains(define);}/// <summary>/// 根据菜单状态修改目标宏/// </summary>/// <param name="define">目标宏定义</param>/// <param name="menuState">当前菜单状态</param>private static void SwitchToTargetState(string define, bool menuState = false){//检测目标宏定义if (define == null)return;//获取当前的宏定义string symbol = GetScriptingDefineSymbolsForGroup();if (menuState) //菜单已选中{//获取目标宏所在的位置int index = symbol.IndexOf(define);if (index < 0)return;//如果不在第一个 则将其前面的分号删掉if (index > 0)index -= 1;int length = define.Length;//如果当前宏长度大于要删除的当前长度,才会有分号if (symbol.Length > length)length += 1;//删除目标宏定义symbol = symbol.Remove(index, length);SetScriptingDefineSymbolsForGroup(symbol);}else //菜单未选中{//如果当前的宏是空的,则直接将目标的宏加入if (symbol.Equals(string.Empty))SetScriptingDefineSymbolsForGroup(define);else{//否则,以分号分割加入目标宏string newSymbol = symbol + ";" + define;SetScriptingDefineSymbolsForGroup(newSymbol);}}}
包管理(PackageManager)的检测方法:
/// <summary>/// 检测目标包是否存在/// </summary>/// <param name="packageName">包名</param>/// <param name="callback"></param>private static void CheckTargetPackageExists(string packageName,Action<bool> callback = null){// 创建一个ListRequest请求,用来查询PackageManager中已经安装的packages列表ListRequest request = Client.List();// 发送ListRequest请求,并在每一帧检查请求是否已经完成EditorApplication.CallbackFunction checkUpdateAction = null;checkUpdateAction = () =>{if (request.IsCompleted){bool packageExists = false;EditorUtility.ClearProgressBar();if (request.Status == StatusCode.Success){// 遍历packages列表,查找目标packageName是否存在foreach (var package in request.Result){if (package.name.Contains(packageName) || package.displayName.Contains(packageName)){packageExists = true;break;}}}else if (request.Status >= StatusCode.Failure){Debug.LogError(request.Error.message);}callback?.Invoke(packageExists);// 取消update回调函数EditorApplication.update -= checkUpdateAction;}};EditorApplication.update += checkUpdateAction;EditorUtility.DisplayProgressBar("Check", "Please wait...", 0f);}
同步刷新菜单的方法:
/// <summary>
/// 刷新菜单状态
/// 标签"UnityEditor.Callbacks.DidReloadScripts"可以在脚本编译完成后自动回调这个方法
/// 标签"RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)"在场景加载前执行这个方法
/// </summary>
[UnityEditor.Callbacks.DidReloadScripts,RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void RefreshMenuState()
{//添加需要更新菜单
}
添加UnityEditor.Callbacks.DidReloadScripts和RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)标签,会在脚本编译完或场景加载前进行状态同步,是个很方便的功能。举例:和小伙伴们一起开发时,只需要自己这边配置好,同步到其他小伙伴那边时,他们的编译器自动就会刷新新配置对应菜单状态。
完整代码:
#if UNITY_EDITOR
using System;
using UnityEditor;
using UnityEngine;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.UI;
using UnityEditor.PackageManager.Requests;namespace FairyGUIEditor
{public class EditorMenuTool{#region Define Symbols/// <summary>/// <a href="https://www.fairygui.com/docs/editor/skeleton#%E5%9C%A8unity%E4%B8%AD%E4%BD%BF%E7%94%A8"> 骨骼动画 </a> Spine:FAIRYGUI_SPINE,龙骨:FAIRYGUI_DRAGONBONES/// </summary>private const string DefineSpine = "FAIRYGUI_SPINE", DefineDragonBones = "FAIRYGUI_DRAGONBONES";/// <summary>/// <a href="https://www.fairygui.com/docs/editor/font#textmeshpro%E6%94%AF%E6%8C%81"> 字体 TextMeshPro </a>:FAIRYGUI_TMPRO/// </summary>private const string DefineTMP = "FAIRYGUI_TMPRO";/// <summary>/// <a href="https://www.fairygui.com/docs/unity/lua"> 使用ToLua </a>:FAIRYGUI_TOLUA/// </summary>private const string DefineToLua = "FAIRYGUI_TOLUA";/// <summary>/// <a href="https://www.fairygui.com/docs/unity/puerts"> 使用Puerts </a>:FAIRYGUI_PUERTS/// </summary>private const string DefinePuerts = "FAIRYGUI_PUERTS";/// <summary>/// <a href="https://www.fairygui.com/docs/unity/special#%E9%98%BF%E6%8B%89%E4%BC%AF%E8%AF%AD%E8%A8%80%E6%96%87%E5%AD%97%E6%98%BE%E7%A4%BA"> 阿拉伯语言文字显示 </a>:RTL_TEXT_SUPPORT/// </summary>private const string DefineRTL = "RTL_TEXT_SUPPORT";/// <summary>/// <a href="https://www.fairygui.com/docs/unity/special#ui%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95"> UI自动化测试 </a>:FAIRYGUI_TEST/// </summary>private const string DefineAirTest = "FAIRYGUI_TEST";#endregionprivate const string PackageName = "TextMeshPro";private const string TextMeshProCheckPkg = "FairyGUI/TextMeshPro/Check Package";private const string TextMeshProEssential = "FairyGUI/TextMeshPro/Import TMP Essential Resources";private const string MenuNameTextMeshPro = "FairyGUI/TextMeshPro/Use TextMeshPro";private const string MenuNameSpine = "FairyGUI/Use Spine";private const string MenuNameDragonBones = "FairyGUI/Use DragonBones";private const string MenuNameToLua = "FairyGUI/Use ToLua";private const string MenuNamePuerts = "FairyGUI/Use Puerts";private const string MenuNameRTLTextSupport = "FairyGUI/Use RTLTextSupport";private const string MenuNameAirTest = "FairyGUI/Use AirTest";/// <summary>/// 检测TextMeshPro包是否存在/// </summary>[MenuItem(TextMeshProCheckPkg,false,1000)]private static void CheckTextMeshProPackage(){CheckTargetPackageExists(PackageName, b =>{//不存在则打开包管理面板,并搜索if (!b){//UnityEditor.PackageManager.UIWindow.Open(PackageName);}else{//提示已经安装了EditorUtility.DisplayDialog("Tips", $"{PackageName} package is installed!\n{PackageName}包已存在!","Ok");}});}/// <summary>/// 导入TextMeshPro包必备资源/// </summary>[MenuItem(TextMeshProEssential,false,1015)]private static void ImportTMPEssentialResources(){//做一次安全检查if (UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.textmeshpro/Scripts/Runtime/TextMeshPro.cs") == null) {Debug.LogError("TextMeshPro package is not installed.");return;}//执行目标菜单项EditorApplication.ExecuteMenuItem("Window/TextMeshPro/Import TMP Essential Resources");}/// <summary>/// 启用或关闭TextMeshPro支持/// </summary>[MenuItem(MenuNameTextMeshPro,false,1030)]private static void SelectTextMeshPro(){//获取当前状态bool state = Menu.GetChecked(MenuNameTextMeshPro);//切换宏定义状态SwitchToTargetState(DefineTMP,state);//刷新菜单状态RefreshMenuState();//更新AssetDatabase.Refresh();}/// <summary>/// 启用或关闭Spine/// </summary>[MenuItem(MenuNameSpine,false,1015)]private static void SelectSpine(){//获取当前状态bool state = Menu.GetChecked(MenuNameSpine);//切换宏定义状态SwitchToTargetState(DefineSpine,state);//刷新菜单状态RefreshMenuState();//更新AssetDatabase.Refresh();}/// <summary>/// 启用或关闭DragonBones/// </summary>[MenuItem(MenuNameDragonBones,false,1030)]private static void SelectDragonBones(){//获取当前状态bool state = Menu.GetChecked(MenuNameDragonBones);//切换宏定义状态SwitchToTargetState(DefineDragonBones,state);//刷新菜单状态RefreshMenuState();//更新AssetDatabase.Refresh();}/// <summary>/// 启用或关闭ToLua/// </summary>[MenuItem(MenuNameToLua,false,1045)]private static void SelectToLua(){//获取当前状态bool state = Menu.GetChecked(MenuNameToLua);//切换宏定义状态SwitchToTargetState(DefineToLua,state);//刷新菜单状态RefreshMenuState();//更新AssetDatabase.Refresh();}/// <summary>/// 启用或关闭Puerts/// </summary>[MenuItem(MenuNamePuerts,false,1060)]private static void SelectPuerts(){//获取当前状态bool state = Menu.GetChecked(MenuNamePuerts);//切换宏定义状态SwitchToTargetState(DefinePuerts,state);//刷新菜单状态RefreshMenuState();//更新AssetDatabase.Refresh();}/// <summary>/// 启用或关闭阿拉伯语言文字/// </summary>[MenuItem(MenuNameRTLTextSupport,false,1075)]private static void SelectRTLTextSupport(){//获取当前状态bool state = Menu.GetChecked(MenuNameRTLTextSupport);//切换宏定义状态SwitchToTargetState(DefineRTL,state);//刷新菜单状态RefreshMenuState();//更新AssetDatabase.Refresh();}/// <summary>/// 启用或关闭UI自动化测试/// </summary>[MenuItem(MenuNameAirTest,false,1090)]private static void SelectUIAirTest(){//获取当前状态bool state = Menu.GetChecked(MenuNameAirTest);//切换宏定义状态SwitchToTargetState(DefineAirTest,state);//刷新菜单状态RefreshMenuState();//更新AssetDatabase.Refresh();}/// <summary>/// 刷新菜单状态/// 标签"UnityEditor.Callbacks.DidReloadScripts"可以在脚本编译完成后自动回调这个方法/// 标签"RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)"在场景加载前执行这个方法/// </summary>[UnityEditor.Callbacks.DidReloadScripts,RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]private static void RefreshMenuState(){//放置需要更新菜单Menu.SetChecked(MenuNameTextMeshPro, CheckScriptingDefineSymbolsExist(DefineTMP));Menu.SetChecked(MenuNameSpine, CheckScriptingDefineSymbolsExist(DefineSpine));Menu.SetChecked(MenuNameDragonBones, CheckScriptingDefineSymbolsExist(DefineDragonBones));Menu.SetChecked(MenuNameToLua, CheckScriptingDefineSymbolsExist(DefineToLua));Menu.SetChecked(MenuNamePuerts, CheckScriptingDefineSymbolsExist(DefinePuerts));Menu.SetChecked(MenuNameRTLTextSupport, CheckScriptingDefineSymbolsExist(DefineRTL));Menu.SetChecked(MenuNameAirTest, CheckScriptingDefineSymbolsExist(DefineAirTest));}#region Package Check/// <summary>/// 检测目标包是否存在/// </summary>/// <param name="packageName">包名</param>/// <param name="callback"></param>private static void CheckTargetPackageExists(string packageName,Action<bool> callback = null){// 创建一个ListRequest请求,用来查询PackageManager中已经安装的packages列表ListRequest request = Client.List();// 发送ListRequest请求,并在每一帧检查请求是否已经完成EditorApplication.CallbackFunction checkUpdateAction = null;checkUpdateAction = () =>{if (request.IsCompleted){bool packageExists = false;EditorUtility.ClearProgressBar();if (request.Status == StatusCode.Success){// 遍历packages列表,查找目标packageName是否存在foreach (var package in request.Result){if (package.name.Contains(packageName) || package.displayName.Contains(packageName)){packageExists = true;break;}}}else if (request.Status >= StatusCode.Failure){Debug.LogError(request.Error.message);}callback?.Invoke(packageExists);// 取消update回调函数EditorApplication.update -= checkUpdateAction;}};EditorApplication.update += checkUpdateAction;EditorUtility.DisplayProgressBar("Check", "Please wait...", 0f);}#endregion#region Scripting Define Symbols/// <summary>/// 获取Scripting Define Symbols的值/// </summary>/// <returns></returns>private static string GetScriptingDefineSymbolsForGroup(){return PlayerSettings.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup);}/// <summary>/// 设置Scripting Define Symbols的值/// </summary>/// <param name="newSymbol">新的宏定义</param>private static void SetScriptingDefineSymbolsForGroup(string newSymbol){PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup,newSymbol);}/// <summary>/// 检测Scripting Define Symbols中是否存在目标值/// </summary>/// <param name="define">目标宏</param>/// <returns></returns>private static bool CheckScriptingDefineSymbolsExist(string define){string symbol = GetScriptingDefineSymbolsForGroup();return symbol.Contains(define);}/// <summary>/// 更新菜单状态修改目标宏/// </summary>/// <param name="define">目标宏定义</param>/// <param name="menuState">当前菜单状态</param>private static void SwitchToTargetState(string define, bool menuState = false){//检测目标宏定义if (define == null)return;//获取当前的宏定义string symbol = GetScriptingDefineSymbolsForGroup();if (menuState) //菜单已选中{//获取目标宏所在的位置int index = symbol.IndexOf(define);if (index < 0)return;//如果不在第一个 则将其前面的分号删掉if (index > 0)index -= 1;int length = define.Length;//如果当前宏长度大于要删除的当前长度,才会有分号if (symbol.Length > length)length += 1;//删除目标宏定义symbol = symbol.Remove(index, length);SetScriptingDefineSymbolsForGroup(symbol);}else //菜单未选中{//如果当前的宏是空的,则直接将目标的宏加入if (symbol.Equals(string.Empty))SetScriptingDefineSymbolsForGroup(define);else{//否则,以分号分割加入目标宏string newSymbol = symbol + ";" + define;SetScriptingDefineSymbolsForGroup(newSymbol);}}}#endregion}
}#endif