【Unity编辑器扩展】GF_HybridCLR自定义Toolbar, 一键出包/打热更扩展工具
创始人
2024-03-21 16:23:21

GF_HybridCLR是基于GameFramework + HybridCLR的一款工具链完善,工作流简洁的游戏框架。拥有标准高效的开发工作流,开箱即用,适用于快速研发。

出包时经常遇到忘记刷新配置表、忘记重新打AB包等等,接入HybridCLR每次打热更包也需要重新编译热更dll,新发App时需要生成桥接函数等。各种琐碎的打包准备工作,一旦忘记操作就容易出故障。基于工作中遇到的痛点,迫切需要写一个傻瓜式一键打包/打热更的工具。

为了这个一键打包工具入口突出,就把它放在Unity编辑器的Toolbar栏,如图:

 点击Toolbar栏Build App/Hotfix后打开一键打包/打热更工具:

一,扩展Unity编辑器的菜单栏(Toolbar):

Toolbar扩展方法可参考github开源项目: GitHub - marijnz/unity-toolbar-extender: Extend the Unity Toolbar with your own Editor UI code.

 实现原理,通过反射获取UnityEditor的Toolbar类,扩展GUI出回调。

Toolbar扩展插件源代码:

using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
#if UNITY_2019_1_OR_NEWER
using UnityEngine.UIElements;
#else
using UnityEngine.Experimental.UIElements;
#endifnamespace UnityToolbarExtender
{public static class ToolbarCallback{static Type m_toolbarType = typeof(Editor).Assembly.GetType("UnityEditor.Toolbar");static Type m_guiViewType = typeof(Editor).Assembly.GetType("UnityEditor.GUIView");
#if UNITY_2020_1_OR_NEWERstatic Type m_iWindowBackendType = typeof(Editor).Assembly.GetType("UnityEditor.IWindowBackend");static PropertyInfo m_windowBackend = m_guiViewType.GetProperty("windowBackend",BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);static PropertyInfo m_viewVisualTree = m_iWindowBackendType.GetProperty("visualTree",BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
#elsestatic PropertyInfo m_viewVisualTree = m_guiViewType.GetProperty("visualTree",BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
#endifstatic FieldInfo m_imguiContainerOnGui = typeof(IMGUIContainer).GetField("m_OnGUIHandler",BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);static ScriptableObject m_currentToolbar;/// /// Callback for toolbar OnGUI method./// public static Action OnToolbarGUI;public static Action OnToolbarGUILeft;public static Action OnToolbarGUIRight;static ToolbarCallback(){EditorApplication.update -= OnUpdate;EditorApplication.update += OnUpdate;}static void OnUpdate(){// Relying on the fact that toolbar is ScriptableObject and gets deleted when layout changesif (m_currentToolbar == null){// Find toolbarvar toolbars = Resources.FindObjectsOfTypeAll(m_toolbarType);m_currentToolbar = toolbars.Length > 0 ? (ScriptableObject)toolbars[0] : null;if (m_currentToolbar != null){
#if UNITY_2021_1_OR_NEWERvar root = m_currentToolbar.GetType().GetField("m_Root", BindingFlags.NonPublic | BindingFlags.Instance);var rawRoot = root.GetValue(m_currentToolbar);var mRoot = rawRoot as VisualElement;RegisterCallback("ToolbarZoneLeftAlign", OnToolbarGUILeft);RegisterCallback("ToolbarZoneRightAlign", OnToolbarGUIRight);void RegisterCallback(string root, Action cb){var toolbarZone = mRoot.Q(root);var parent = new VisualElement(){style = {flexGrow = 1,flexDirection = FlexDirection.Row,}};var container = new IMGUIContainer();container.style.flexGrow = 1;container.onGUIHandler += () => {cb?.Invoke();};parent.Add(container);toolbarZone.Add(parent);}
#else
#if UNITY_2020_1_OR_NEWERvar windowBackend = m_windowBackend.GetValue(m_currentToolbar);// Get it's visual treevar visualTree = (VisualElement) m_viewVisualTree.GetValue(windowBackend, null);
#else// Get it's visual treevar visualTree = (VisualElement) m_viewVisualTree.GetValue(m_currentToolbar, null);
#endif// Get first child which 'happens' to be toolbar IMGUIContainervar container = (IMGUIContainer) visualTree[0];// (Re)attach handlervar handler = (Action) m_imguiContainerOnGui.GetValue(container);handler -= OnGUI;handler += OnGUI;m_imguiContainerOnGui.SetValue(container, handler);#endif}}}static void OnGUI(){var handler = OnToolbarGUI;if (handler != null) handler();}}[InitializeOnLoad]public static class UnityEditorToolbar{static int m_toolCount;static GUIStyle m_commandStyle = null;public static readonly List LeftToolbarGUI = new List();public static readonly List RightToolbarGUI = new List();static UnityEditorToolbar(){Type toolbarType = typeof(Editor).Assembly.GetType("UnityEditor.Toolbar");#if UNITY_2019_1_OR_NEWERstring fieldName = "k_ToolCount";
#elsestring fieldName = "s_ShownToolIcons";
#endifFieldInfo toolIcons = toolbarType.GetField(fieldName,BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);#if UNITY_2019_3_OR_NEWERm_toolCount = toolIcons != null ? ((int)toolIcons.GetValue(null)) : 8;
#elif UNITY_2019_1_OR_NEWERm_toolCount = toolIcons != null ? ((int) toolIcons.GetValue(null)) : 7;
#elif UNITY_2018_1_OR_NEWERm_toolCount = toolIcons != null ? ((Array) toolIcons.GetValue(null)).Length : 6;
#elsem_toolCount = toolIcons != null ? ((Array) toolIcons.GetValue(null)).Length : 5;
#endifToolbarCallback.OnToolbarGUI = OnGUI;ToolbarCallback.OnToolbarGUILeft = GUILeft;ToolbarCallback.OnToolbarGUIRight = GUIRight;}#if UNITY_2019_3_OR_NEWERpublic const float space = 8;
#elsepublic const float space = 10;
#endifpublic const float largeSpace = 20;public const float buttonWidth = 32;public const float dropdownWidth = 80;
#if UNITY_2019_1_OR_NEWERpublic const float playPauseStopWidth = 140;
#elsepublic const float playPauseStopWidth = 100;
#endifstatic void OnGUI(){// Create two containers, left and right// Screen is whole toolbarif (m_commandStyle == null){m_commandStyle = new GUIStyle("CommandLeft");}var screenWidth = EditorGUIUtility.currentViewWidth;// Following calculations match code reflected from Toolbar.OldOnGUI()float playButtonsPosition = Mathf.RoundToInt((screenWidth - playPauseStopWidth) / 2);Rect leftRect = new Rect(0, 0, screenWidth, Screen.height);leftRect.xMin += space; // Spacing leftleftRect.xMin += buttonWidth * m_toolCount; // Tool buttons
#if UNITY_2019_3_OR_NEWERleftRect.xMin += space; // Spacing between tools and pivot
#elseleftRect.xMin += largeSpace; // Spacing between tools and pivot
#endifleftRect.xMin += 64 * 2; // Pivot buttonsleftRect.xMax = playButtonsPosition;Rect rightRect = new Rect(0, 0, screenWidth, Screen.height);rightRect.xMin = playButtonsPosition;rightRect.xMin += m_commandStyle.fixedWidth * 3; // Play buttonsrightRect.xMax = screenWidth;rightRect.xMax -= space; // Spacing rightrightRect.xMax -= dropdownWidth; // LayoutrightRect.xMax -= space; // Spacing between layout and layersrightRect.xMax -= dropdownWidth; // Layers
#if UNITY_2019_3_OR_NEWERrightRect.xMax -= space; // Spacing between layers and account
#elserightRect.xMax -= largeSpace; // Spacing between layers and account
#endifrightRect.xMax -= dropdownWidth; // AccountrightRect.xMax -= space; // Spacing between account and cloudrightRect.xMax -= buttonWidth; // CloudrightRect.xMax -= space; // Spacing between cloud and collabrightRect.xMax -= 78; // Colab// Add spacing around existing controlsleftRect.xMin += space;leftRect.xMax -= space;rightRect.xMin += space;rightRect.xMax -= space;// Add top and bottom margins
#if UNITY_2019_3_OR_NEWERleftRect.y = 4;leftRect.height = 22;rightRect.y = 4;rightRect.height = 22;
#elseleftRect.y = 5;leftRect.height = 24;rightRect.y = 5;rightRect.height = 24;
#endifif (leftRect.width > 0){GUILayout.BeginArea(leftRect);GUILayout.BeginHorizontal();foreach (var handler in LeftToolbarGUI){handler();}GUILayout.EndHorizontal();GUILayout.EndArea();}if (rightRect.width > 0){GUILayout.BeginArea(rightRect);GUILayout.BeginHorizontal();foreach (var handler in RightToolbarGUI){handler();}GUILayout.EndHorizontal();GUILayout.EndArea();}}public static void GUILeft(){GUILayout.BeginHorizontal();foreach (var handler in LeftToolbarGUI){handler();}GUILayout.EndHorizontal();}public static void GUIRight(){GUILayout.BeginHorizontal();foreach (var handler in RightToolbarGUI){handler();}GUILayout.EndHorizontal();}}
}

使用方法: 

定义一个静态类添加[UnityEditor.InitializeOnLoad],使其自动执行构造函数。

在Toolbar右侧绘制GUI: UnityEditorToolbar.RightToolbarGUI.Add(OnRightToolbarGUI);
在Toolbar左侧绘制GUI: UnityEditorToolbar.LeftToolbarGUI.Add(OnLeftToolbarGUI);

using UnityEngine;
using UnityEditor;
using UnityToolbarExtender;
using UnityGameFramework.Editor.ResourceTools;
[UnityEditor.InitializeOnLoad]
public static class EditorToolbarExtension
{private static GUIContent buildBtContent;static EditorToolbarExtension(){buildBtContent = EditorGUIUtility.TrTextContentWithIcon("Build App/Hotfix","打新包/打热更", "UnityLogo");UnityEditorToolbar.RightToolbarGUI.Add(OnRightToolbarGUI);UnityEditorToolbar.LeftToolbarGUI.Add(OnLeftToolbarGUI);}private static void OnLeftToolbarGUI(){//在Toolbar左侧绘制UI}private static void OnRightToolbarGUI(){
//在Toolbar右侧绘制UIif (GUILayout.Button(buildBtContent,EditorStyles.toolbarButton, GUILayout.MaxWidth(125), GUILayout.Height(EditorGUIUtility.singleLineHeight))){AppBuildEidtor.Open();GUIUtility.ExitGUI();}GUILayout.FlexibleSpace();}
}

二,打包工具功能设计: 

 先明确工具要解决的问题:

1. 工具界面可配置打资源和打App的相关设置,切配置持久化保存。

2. 可一键打热更资源,一键出包,简化流程。

具体功能设计:

1. 打单机包或增量热更包:

单机包或增量热更包出包时都需要把AB资源打进包里,点击Build App按钮逻辑流程为:若是热更包则生成热更(hotfix)Dll => 自动处理AB包重复依赖资源 =>  打AB包 => 把AB包复制到SteamingAssets目录 => 若是热更包则执行HybridCLR预处理命令(生成link.xml,桥接函数等) => 把AOT泛型补充dll自动复制到Resources目录 => Build出包;

Build出包需要根据目标平台留出一些打包常用的参数设置入口,例如app版本号、Version Code, 打aab(谷歌商店包),开发者模式,安卓密钥等。

2. 打全热更包:

①全热更包是进入游戏后再从热更地址下载资源,所以出包时不用打AB包。点击Build App按钮逻辑流程为:HybridCLR预处理命令(生成link.xml,桥接函数等) => 把AOT泛型补充dll自动复制到Resources目录 => Build出包;

②打热更资源和dll,对于热更包(增量热更/全热更),每次更新只需要点击Build Resources按钮打出热更资源,然后把热更资源上传到资源服务器即可。点击Build Resources按钮逻辑流程为:一生成热更dll => 自动处理AB包重复依赖资源 =>  打AB包;把打出的AB包提交到热更新资源服务器即可。

3. 其它功能:

打资源/出包常用配置项可在界面中配置并持久化保存配置数据;

Resource Mode: 可选择资源模式,单机模式 / 全热更模式 / 部分热更模式(即,需要某部分资源时再热更)

除了上述部分,还需要在各个功能模块区域显示对应的一键跳转按钮,如:

Resource Editor按钮: 打开AB包编辑器

Hotfix Settings按钮:打开HybridCLR Settings界面,配置C#代码热更相关(一般只需要配置一次)

Player Settings按钮:打开Player Setting界面,设置出包参数。

三,具体功能实现:

由于GF框架内置的打AB包工具已经有了打资源的相关配置和功能按钮,索性直接基于GF的Resource Builder工具做修改。

1. Resource Editor按钮, 打开GF的Resource Editor(AB包编辑器):

UnityGameFramework.Editor.ResourceTools.ResourceEditor类有个打开窗口的静态私有方法“Open”, 只需要通过反射调用即可:

private void OpenResourcesEditor(){var resEditorClass = Utility.Assembly.GetType("UnityGameFramework.Editor.ResourceTools.ResourceEditor");resEditorClass?.GetMethod("Open", BindingFlags.Static | BindingFlags.NonPublic)?.Invoke(null, null);}

2. Resource Mode资源模式切换(单机/全热更/需要时热更):

ResourceComponent留出了SetResourceMode()方法,但运行时调用却报错,原来ResourceComponent在Start回调里根据Resource Mode做一次初始化,不允许初始化之后再修改,即使修改了ResourceMode也是无效的。为了保持低耦合不能改GF源码,只能特殊处理,在其它MonoBehavior脚本的Awake方法中通过反射修改ResourceComponent的私有变量m_ResourceMode,Awake方法早于ResourceComponent的Start,这样设置就能生效了。

private void Awake(){var resCom = GameEntry.GetComponent();if (resCom != null){var resTp = resCom.GetType();var m_ResourceMode = resTp.GetField("m_ResourceMode", BindingFlags.Instance | BindingFlags.NonPublic);m_ResourceMode.SetValue(resCom, AppSettings.Instance.ResourceMode);Log.Info("------------Set ResourceMode:{0}", AppSettings.Instance.ResourceMode);}}

其中AppSettings是一个运行时的ScriptableObject,用于保存一些运行时配置,如是否开启debug模式,ResourceMode类型等。

AppSettings配置文件实现:

using GameFramework.Resource;
using UnityEngine;[CreateAssetMenu(fileName = "AppSettings", menuName = "ScriptableObject/AppSettings")]
public class AppSettings : ScriptableObject
{private static AppSettings mInstance = null;public static AppSettings Instance{get{if (mInstance == null){mInstance = Resources.Load("AppSettings");}return mInstance;}}[Tooltip("debug模式,默认显示debug窗口")]public bool DebugMode = false;[Tooltip("资源模式: 单机/全热更/需要时热更")]public ResourceMode ResourceMode = ResourceMode.Package;
}

AppSettings是全局配置,因此使用单例模式。当打包工具界面打开时,检测Resource目录是否存在AppSettings配置文件,若无则自动创建。工具界面ResourceMode设置实时同步保存到AppSettings, 游戏运行时获取并应用AppSettings中的配置。

Hotfix Settings(热更相关设置):

打热更资源时根据这些配置自动生成version.json文件,其中信息包含热更包hash code, 资源大小、资源版本号、热更下载地址、App是否有新版本、是否强制更新App、当前版本资源适用于哪些App版本等。游戏启动时会先从服务器请求version.json信息检测是否需要更新。

热更基本流程

 Update Prefix Uri: 热更资源下载地址;

Applicable Verison:当前版本资源适用哪些App版本,多版本用‘|’分割;

App Update Url:App下载跳转链接;

Force Update:是否强制更新App;

App Update Description:App更新说明,显示在新版本提示对话框;

Hotfix Settings跳转按钮,跳转到HybridCLR设置界面:

SettingsService.OpenProjectSettings("Project/HybridCLR Settings");

跳转到Player Settings界面:

SettingsService.OpenProjectSettings("Project/Player");

Build App Settings(出包相关设置):

Build App Buindle: 打谷歌商店aab文件;

Development Build: 开发者模式打包;

Debug Mode: 调试模式,true:默认显示GF Debug窗口;

Use Custom Keystore: 使用自定义keystore打安卓包;

选择keystore文件:

if (GUILayout.Button("Select Keystore", GUILayout.Width(160f))){var keystoreDir = string.IsNullOrWhiteSpace(AppBuildSettings.Instance.AndroidKeystoreName) ? Directory.GetParent(Application.dataPath).FullName : Path.GetDirectoryName(AppBuildSettings.Instance.AndroidKeyAliasName);var openPath = Directory.Exists(keystoreDir) ? keystoreDir : Directory.GetParent(Application.dataPath).FullName;string path = EditorUtility.OpenFilePanel("Select Keystore", openPath, "keystore,jks,ks");AppBuildSettings.Instance.AndroidKeystoreName = PlayerSettings.Android.keystoreName = path;GUIUtility.ExitGUI();}

一键打热更资源实现:

直接调用GF框架自带的ResourceBuilderController的BuildResources()方法即可;

一键出包:

Unity的Build Settings界面已经有了现成的出包功能,可以直接通过反射调用。

 从Unity开源代码中可以找到具体实现:https://github.com/Unity-Technologies/UnityCsReference

 在BuildPlayerWindow.cs可以看到,Build按钮调用了CallBuildMethods静态方法:

private void CallBuildMethods(){
#if !DISABLE_HYBRIDCLRHybridCLR.Editor.Commands.PrebuildCommand.GenerateAll();
#endifvar buildWin = Utility.Assembly.GetType("UnityEditor.BuildPlayerWindow");if (buildWin != null){var buildFunc = buildWin.GetMethod("CallBuildMethods", System.Reflection.BindingFlags.Static | BindingFlags.NonPublic);buildFunc?.Invoke(null, new object[] { true, BuildOptions.ShowBuiltPlayer });}}private void BuildApp(){if ((m_Controller.OutputPackageSelected || m_Controller.OutputPackedSelected)){if (m_Controller.BuildResources()){AssetDatabase.Refresh();CallBuildMethods();}}else if (m_Controller.OutputFullSelected){DeleteStreamingAssets();CallBuildMethods();}}

工具完整代码:

using GameFramework;
using System;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using GameFramework.Resource;namespace UnityGameFramework.Editor.ResourceTools
{/// /// 资源生成器。/// public class AppBuildEidtor : EditorWindow{private ResourceBuilderController m_Controller = null;private bool m_OrderBuildResources = false;private int m_CompressionHelperTypeNameIndex = 0;private int m_BuildEventHandlerTypeNameIndex = 0;private GUIContent hotfixUrlContent;private GUIContent applicableVerContent;private GUIContent forceUpdateAppContent;private GUIContent appUpdateUrlContent;private GUIContent appUpdateDescContent;private GUIContent revealFolderContent;private GUIContent buildResBtContent;private GUIContent buildAppBtContent;private GUIContent saveBtContent;private GUIContent playerSettingBtContent;private GUIContent hybridclrSettingBtContent;private Vector2 scrollPosition;public static void Open(){AppBuildEidtor window = GetWindow("App Builder", true);
#if UNITY_2019_3_OR_NEWERwindow.minSize = new Vector2(800f, 800f);
#elsewindow.minSize = new Vector2(800f, 750f);
#endif}private void OnEnable(){hotfixUrlContent = new GUIContent("Update Prefix Uri", "热更新资源服务器地址");applicableVerContent = new GUIContent("Applicable Version", "资源适用的客户端版本号,多版本用'|'分割");forceUpdateAppContent = new GUIContent("Force Update", "是否强制更新App");appUpdateUrlContent = new GUIContent("App Update Url", "App更新下载地址");appUpdateDescContent = new GUIContent("App Update Description:", "App更新公告,用于显示在对话框(支持TextMeshPro富文本)");revealFolderContent = new GUIContent("Reveal Folder", "打包完成后打开资源输出目录");buildResBtContent = EditorGUIUtility.TrTextContentWithIcon("Build Resources", "打AB包/热更", "CloudConnect@2x");buildAppBtContent = EditorGUIUtility.TrTextContentWithIcon("Build App", "打新包", "UnityLogo");playerSettingBtContent = EditorGUIUtility.TrTextContentWithIcon("Player Settings", "打开Player Settings界面", "Settings");hybridclrSettingBtContent = EditorGUIUtility.TrTextContentWithIcon("Hotfix Settings", "打开HybridCLR Settings界面", "Settings");saveBtContent = EditorGUIUtility.TrTextContentWithIcon("Save", "保存设置", "SaveAs@2x");if (AppSettings.Instance == null){AssetDatabase.CreateAsset(CreateInstance(), "Assets/Resources/AppSettings.asset");}RefreshHybridCLREnable();m_Controller = new ResourceBuilderController();m_Controller.OnLoadingResource += OnLoadingResource;m_Controller.OnLoadingAsset += OnLoadingAsset;m_Controller.OnLoadCompleted += OnLoadCompleted;m_Controller.OnAnalyzingAsset += OnAnalyzingAsset;m_Controller.OnAnalyzeCompleted += OnAnalyzeCompleted;m_Controller.ProcessingAssetBundle += OnProcessingAssetBundle;m_Controller.ProcessingBinary += OnProcessingBinary;m_Controller.ProcessResourceComplete += OnProcessResourceComplete;m_Controller.BuildResourceError += OnBuildResourceError;m_OrderBuildResources = false;if (m_Controller.Load()){Debug.Log("Load configuration success.");m_CompressionHelperTypeNameIndex = 0;string[] compressionHelperTypeNames = m_Controller.GetCompressionHelperTypeNames();for (int i = 0; i < compressionHelperTypeNames.Length; i++){if (m_Controller.CompressionHelperTypeName == compressionHelperTypeNames[i]){m_CompressionHelperTypeNameIndex = i;break;}}m_Controller.RefreshCompressionHelper();m_BuildEventHandlerTypeNameIndex = 0;string[] buildEventHandlerTypeNames = m_Controller.GetBuildEventHandlerTypeNames();for (int i = 0; i < buildEventHandlerTypeNames.Length; i++){if (m_Controller.BuildEventHandlerTypeName == buildEventHandlerTypeNames[i]){m_BuildEventHandlerTypeNameIndex = i;break;}}m_Controller.RefreshBuildEventHandler();}else{Debug.LogWarning("Load configuration failure.");}if (string.IsNullOrWhiteSpace(m_Controller.OutputDirectory) || !Directory.Exists(m_Controller.OutputDirectory)){m_Controller.OutputDirectory = ConstEditor.AssetBundleOutputPath;}}private void Update(){if (m_OrderBuildResources){m_OrderBuildResources = false;BuildResources();}}private void OnGUI(){scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);EditorGUILayout.BeginVertical(GUILayout.Width(position.width), GUILayout.Height(position.height));{GUILayout.Space(5f);EditorGUILayout.LabelField("Environment Information", EditorStyles.boldLabel);EditorGUILayout.BeginVertical("box");{EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Product Name", GUILayout.Width(160f));EditorGUILayout.LabelField(m_Controller.ProductName);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Company Name", GUILayout.Width(160f));EditorGUILayout.LabelField(m_Controller.CompanyName);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Game Identifier", GUILayout.Width(160f));EditorGUILayout.LabelField(m_Controller.GameIdentifier);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Game Framework Version", GUILayout.Width(160f));EditorGUILayout.LabelField(m_Controller.GameFrameworkVersion);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Unity Version", GUILayout.Width(160f));EditorGUILayout.LabelField(m_Controller.UnityVersion);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Applicable Game Version", GUILayout.Width(160f));EditorGUILayout.LabelField(m_Controller.ApplicableGameVersion);}EditorGUILayout.EndHorizontal();}EditorGUILayout.EndVertical();GUILayout.Space(5f);EditorGUILayout.BeginHorizontal();{EditorGUILayout.BeginVertical();{EditorGUILayout.LabelField("Platforms", EditorStyles.boldLabel);EditorGUILayout.BeginHorizontal("box");{EditorGUILayout.BeginVertical();{DrawPlatform(Platform.Windows, "Windows");DrawPlatform(Platform.Windows64, "Windows x64");DrawPlatform(Platform.MacOS, "macOS");}EditorGUILayout.EndVertical();EditorGUILayout.BeginVertical();{DrawPlatform(Platform.Linux, "Linux");DrawPlatform(Platform.IOS, "iOS");DrawPlatform(Platform.Android, "Android");}EditorGUILayout.EndVertical();EditorGUILayout.BeginVertical();{DrawPlatform(Platform.WindowsStore, "Windows Store");DrawPlatform(Platform.WebGL, "WebGL");}EditorGUILayout.EndVertical();}EditorGUILayout.EndHorizontal();}EditorGUILayout.EndVertical();}EditorGUILayout.EndHorizontal();GUILayout.Space(5f);EditorGUILayout.LabelField("Compression", EditorStyles.boldLabel);EditorGUILayout.BeginVertical("box");{EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("AssetBundle Compression", GUILayout.Width(160f));m_Controller.AssetBundleCompression = (AssetBundleCompressionType)EditorGUILayout.EnumPopup(m_Controller.AssetBundleCompression);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Compression Helper", GUILayout.Width(160f));string[] names = m_Controller.GetCompressionHelperTypeNames();int selectedIndex = EditorGUILayout.Popup(m_CompressionHelperTypeNameIndex, names);if (selectedIndex != m_CompressionHelperTypeNameIndex){m_CompressionHelperTypeNameIndex = selectedIndex;m_Controller.CompressionHelperTypeName = selectedIndex <= 0 ? string.Empty : names[selectedIndex];if (m_Controller.RefreshCompressionHelper()){Debug.Log("Set compression helper success.");}else{Debug.LogWarning("Set compression helper failure.");}}}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Additional Compression", GUILayout.Width(160f));m_Controller.AdditionalCompressionSelected = EditorGUILayout.ToggleLeft("Additional Compression for Output Full Resources with Compression Helper", m_Controller.AdditionalCompressionSelected);}EditorGUILayout.EndHorizontal();}EditorGUILayout.EndVertical();GUILayout.Space(5f);EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Build Resources Settings", EditorStyles.boldLabel);if (GUILayout.Button("Resources Editor", GUILayout.Width(160f))){OpenResourcesEditor();GUIUtility.ExitGUI();}}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginVertical("box");{EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Force Rebuild AssetBundle", GUILayout.Width(160f));m_Controller.ForceRebuildAssetBundleSelected = EditorGUILayout.Toggle(m_Controller.ForceRebuildAssetBundleSelected);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Build Event Handler", GUILayout.Width(160f));string[] names = m_Controller.GetBuildEventHandlerTypeNames();int selectedIndex = EditorGUILayout.Popup(m_BuildEventHandlerTypeNameIndex, names);if (selectedIndex != m_BuildEventHandlerTypeNameIndex){m_BuildEventHandlerTypeNameIndex = selectedIndex;m_Controller.BuildEventHandlerTypeName = selectedIndex <= 0 ? string.Empty : names[selectedIndex];if (m_Controller.RefreshBuildEventHandler()){Debug.Log("Set build event handler success.");}else{Debug.LogWarning("Set build event handler failure.");}}}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Internal Resource Version", GUILayout.Width(160f));m_Controller.InternalResourceVersion = EditorGUILayout.IntField(m_Controller.InternalResourceVersion);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Resource Version", GUILayout.Width(160f));GUILayout.Label(Utility.Text.Format("{0} ({1})", m_Controller.ApplicableGameVersion, m_Controller.InternalResourceVersion.ToString()));}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Output Directory", GUILayout.Width(160f));m_Controller.OutputDirectory = EditorGUILayout.TextField(m_Controller.OutputDirectory);if (GUILayout.Button("Browse...", GUILayout.Width(80f))){BrowseOutputDirectory();}}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Output Resources Path", GUILayout.Width(160f));GUILayout.Label(GetResourceOupoutPathByMode(AppSettings.Instance.ResourceMode));EditorGUILayout.LabelField("Resource Mode:", GUILayout.Width(100f));EditorGUI.BeginChangeCheck();{AppSettings.Instance.ResourceMode = (ResourceMode)EditorGUILayout.EnumPopup(AppSettings.Instance.ResourceMode, GUILayout.Width(160f));}if (EditorGUI.EndChangeCheck()){RefreshHybridCLREnable();}if (AppSettings.Instance.ResourceMode != ResourceMode.Unspecified){SetResourceMode(AppSettings.Instance.ResourceMode);}AppBuildSettings.Instance.RevealFolder = EditorGUILayout.ToggleLeft(revealFolderContent, AppBuildSettings.Instance.RevealFolder, GUILayout.Width(105f));}EditorGUILayout.EndHorizontal();if (AppSettings.Instance.ResourceMode == ResourceMode.Unspecified){EditorGUILayout.HelpBox("ResourceMode is invalid.", MessageType.Error);}EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Working Path", GUILayout.Width(160f));GUILayout.Label(m_Controller.WorkingPath);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Build Report Path", GUILayout.Width(160f));GUILayout.Label(m_Controller.BuildReportPath);}EditorGUILayout.EndHorizontal();}EditorGUILayout.EndVertical();string buildMessage = string.Empty;MessageType buildMessageType = MessageType.None;GetBuildMessage(out buildMessage, out buildMessageType);EditorGUILayout.HelpBox(buildMessage, buildMessageType);if (m_Controller.OutputFullSelected || m_Controller.OutputPackedSelected){DrawHotfixConfigPanel();}DrawAppBuildSettingsPanel();GUILayout.Space(2f);EditorGUILayout.BeginHorizontal();{EditorGUI.BeginDisabledGroup(m_Controller.Platforms == Platform.Undefined || string.IsNullOrEmpty(m_Controller.CompressionHelperTypeName) || !m_Controller.IsValidOutputDirectory || AppSettings.Instance.ResourceMode == ResourceMode.Unspecified);{if (GUILayout.Button(buildResBtContent, GUILayout.Height(35))){m_OrderBuildResources = true;}if (GUILayout.Button(buildAppBtContent, GUILayout.Height(35))){BuildApp();GUIUtility.ExitGUI();}}EditorGUI.EndDisabledGroup();if (GUILayout.Button(saveBtContent, GUILayout.Width(140), GUILayout.Height(35))){SaveConfiguration();}}EditorGUILayout.EndHorizontal();}EditorGUILayout.EndVertical();EditorGUILayout.EndScrollView();}private void RefreshHybridCLREnable(){if (AppSettings.Instance.ResourceMode != ResourceMode.Unspecified){if (AppSettings.Instance.ResourceMode == ResourceMode.Package){
#if !DISABLE_HYBRIDCLRMyGameTools.DisableHybridCLR();
#endif}else{
#if DISABLE_HYBRIDCLRMyGameTools.EnableHybridCLR();
#endif}}}private string GetResourceOupoutPathByMode(ResourceMode mode){string result = null;switch (mode){case ResourceMode.Package:result = m_Controller.OutputPackagePath;break;case ResourceMode.Updatable:result = m_Controller.OutputFullPath;break;case ResourceMode.UpdatableWhilePlaying:result = m_Controller.OutputPackedPath;break;}return result;}private void SetResourceMode(ResourceMode mode){m_Controller.OutputPackageSelected = false;m_Controller.OutputFullSelected = false;m_Controller.OutputPackedSelected = false;switch (mode){case ResourceMode.Package:m_Controller.OutputPackageSelected = true;break;case ResourceMode.Updatable:m_Controller.OutputFullSelected = true;break;case ResourceMode.UpdatableWhilePlaying:m_Controller.OutputPackedSelected = true;break;}}private void OpenResourcesEditor(){var resEditorClass = Utility.Assembly.GetType("UnityGameFramework.Editor.ResourceTools.ResourceEditor");resEditorClass?.GetMethod("Open", BindingFlags.Static | BindingFlags.NonPublic)?.Invoke(null, null);}private void DrawAppBuildSettingsPanel(){GUILayout.Space(5f);EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Build App Settings:", EditorStyles.boldLabel, GUILayout.Width(160));
#if UNITY_ANDROIDAppBuildSettings.Instance.BuildForGooglePlay = EditorUserBuildSettings.buildAppBundle = EditorGUILayout.ToggleLeft("Build App Bundle(GP)", AppBuildSettings.Instance.BuildForGooglePlay);
#endifAppBuildSettings.Instance.DevelopmentBuild = EditorUserBuildSettings.development = EditorGUILayout.ToggleLeft("Development Build", AppBuildSettings.Instance.DevelopmentBuild);AppSettings.Instance.DebugMode = EditorGUILayout.ToggleLeft("Debug Mode", AppSettings.Instance.DebugMode);if (GUILayout.Button(playerSettingBtContent)){SettingsService.OpenProjectSettings("Project/Player");GUIUtility.ExitGUI();}}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginVertical("box");{EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Version", GUILayout.Width(160f));PlayerSettings.bundleVersion = EditorGUILayout.TextField(PlayerSettings.bundleVersion);}EditorGUILayout.EndHorizontal();
#if UNITY_ANDROIDEditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Version Code", GUILayout.Width(160f));PlayerSettings.Android.bundleVersionCode = EditorGUILayout.IntField(PlayerSettings.Android.bundleVersionCode);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{PlayerSettings.Android.useCustomKeystore = EditorGUILayout.ToggleLeft("Use Custom Keystore", PlayerSettings.Android.useCustomKeystore, GUILayout.Width(160f));EditorGUI.BeginDisabledGroup(!PlayerSettings.Android.useCustomKeystore);{AppBuildSettings.Instance.AndroidKeystoreName = PlayerSettings.Android.keystoreName = EditorGUILayout.TextField(AppBuildSettings.Instance.AndroidKeystoreName);if (GUILayout.Button("Select Keystore", GUILayout.Width(160f))){var keystoreDir = string.IsNullOrWhiteSpace(AppBuildSettings.Instance.AndroidKeystoreName) ? Directory.GetParent(Application.dataPath).FullName : Path.GetDirectoryName(AppBuildSettings.Instance.AndroidKeyAliasName);var openPath = Directory.Exists(keystoreDir) ? keystoreDir : Directory.GetParent(Application.dataPath).FullName;string path = EditorUtility.OpenFilePanel("Select Keystore", openPath, "keystore,jks,ks");AppBuildSettings.Instance.AndroidKeystoreName = PlayerSettings.Android.keystoreName = path;GUIUtility.ExitGUI();}}EditorGUI.EndDisabledGroup();}EditorGUILayout.EndHorizontal();if (PlayerSettings.Android.useCustomKeystore){EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Keystore Password", GUILayout.Width(160f));AppBuildSettings.Instance.KeystorePass = PlayerSettings.keystorePass = EditorGUILayout.TextField(AppBuildSettings.Instance.KeystorePass);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("KeyAliasName", GUILayout.Width(160f));AppBuildSettings.Instance.AndroidKeyAliasName = PlayerSettings.Android.keyaliasName = EditorGUILayout.TextField(AppBuildSettings.Instance.AndroidKeyAliasName);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Alias Password", GUILayout.Width(160f));AppBuildSettings.Instance.KeyAliasPass = PlayerSettings.keyaliasPass = EditorGUILayout.TextField(AppBuildSettings.Instance.KeyAliasPass);}EditorGUILayout.EndHorizontal();}#elif UNITY_IOSEditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Build Number", GUILayout.Width(160f));PlayerSettings.iOS.buildNumber = EditorGUILayout.TextField(PlayerSettings.iOS.buildNumber);}EditorGUILayout.EndHorizontal();
#endif}EditorGUILayout.EndVertical();}private void DrawHotfixConfigPanel(){GUILayout.Space(5f);EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField("Hotfix Settings:", EditorStyles.boldLabel);if (GUILayout.Button(hybridclrSettingBtContent, GUILayout.Width(160f))){SettingsService.OpenProjectSettings("Project/HybridCLR Settings");GUIUtility.ExitGUI();}}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginVertical("box");{EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField(hotfixUrlContent, GUILayout.Width(160f));AppBuildSettings.Instance.UpdatePrefixUri = EditorGUILayout.TextField(AppBuildSettings.Instance.UpdatePrefixUri);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField(applicableVerContent, GUILayout.Width(160f));AppBuildSettings.Instance.ApplicableGameVersion = EditorGUILayout.TextField(AppBuildSettings.Instance.ApplicableGameVersion);}EditorGUILayout.EndHorizontal();EditorGUILayout.BeginHorizontal();{EditorGUILayout.LabelField(appUpdateUrlContent, GUILayout.Width(160f));AppBuildSettings.Instance.AppUpdateUrl = EditorGUILayout.TextField(AppBuildSettings.Instance.AppUpdateUrl);AppBuildSettings.Instance.ForceUpdateApp = EditorGUILayout.ToggleLeft(forceUpdateAppContent, AppBuildSettings.Instance.ForceUpdateApp, GUILayout.Width(100f));}EditorGUILayout.EndHorizontal();EditorGUILayout.Space(5);EditorGUILayout.LabelField(appUpdateDescContent, GUILayout.Width(160f));AppBuildSettings.Instance.AppUpdateDesc = EditorGUILayout.TextArea(AppBuildSettings.Instance.AppUpdateDesc, GUILayout.Height(50));}EditorGUILayout.EndVertical();}private void BuildApp(){if ((m_Controller.OutputPackageSelected || m_Controller.OutputPackedSelected)){if (m_Controller.BuildResources()){AssetDatabase.Refresh();CallBuildMethods();}}else if (m_Controller.OutputFullSelected){DeleteStreamingAssets();CallBuildMethods();}}private void DeleteStreamingAssets(){string streamingAssetsPath = Path.Combine(Application.dataPath, "StreamingAssets");if (Directory.Exists(streamingAssetsPath)){Directory.Delete(streamingAssetsPath, true);}string streamMetaFile = streamingAssetsPath + ".meta";if (File.Exists(streamMetaFile)){File.Delete(streamMetaFile);}}private void CallBuildMethods(){
#if !DISABLE_HYBRIDCLRHybridCLR.Editor.Commands.PrebuildCommand.GenerateAll();
#endifvar buildWin = Utility.Assembly.GetType("UnityEditor.BuildPlayerWindow");if (buildWin != null){var buildFunc = buildWin.GetMethod("CallBuildMethods", System.Reflection.BindingFlags.Static | BindingFlags.NonPublic);buildFunc?.Invoke(null, new object[] { true, BuildOptions.ShowBuiltPlayer });}}private void BrowseOutputDirectory(){string directory = EditorUtility.OpenFolderPanel("Select Output Directory", m_Controller.OutputDirectory, string.Empty);if (!string.IsNullOrEmpty(directory)){m_Controller.OutputDirectory = directory;}}private void GetBuildMessage(out string message, out MessageType messageType){message = string.Empty;messageType = MessageType.Error;if (m_Controller.Platforms == Platform.Undefined){if (!string.IsNullOrEmpty(message)){message += Environment.NewLine;}message += "Platform is invalid.";}if (string.IsNullOrEmpty(m_Controller.CompressionHelperTypeName)){if (!string.IsNullOrEmpty(message)){message += Environment.NewLine;}message += "Compression helper is invalid.";}if (!m_Controller.IsValidOutputDirectory){if (!string.IsNullOrEmpty(message)){message += Environment.NewLine;}message += "Output directory is invalid.";}if (!string.IsNullOrEmpty(message)){return;}messageType = MessageType.Info;if (Directory.Exists(m_Controller.OutputPackagePath)){message += Utility.Text.Format("{0} will be overwritten.", m_Controller.OutputPackagePath);messageType = MessageType.Warning;}if (Directory.Exists(m_Controller.OutputFullPath)){if (message.Length > 0){message += " ";}message += Utility.Text.Format("{0} will be overwritten.", m_Controller.OutputFullPath);messageType = MessageType.Warning;}if (Directory.Exists(m_Controller.OutputPackedPath)){if (message.Length > 0){message += " ";}message += Utility.Text.Format("{0} will be overwritten.", m_Controller.OutputPackedPath);messageType = MessageType.Warning;}if (messageType == MessageType.Warning){return;}message = "Ready to build.";}private void BuildResources(){if (m_Controller.BuildResources()){Debug.Log("Build resources success.");SaveConfiguration();}else{Debug.LogWarning("Build resources failure.");}}private void SaveConfiguration(){EditorUtility.SetDirty(AppSettings.Instance);AppBuildSettings.Save();if (m_Controller.Save()){Debug.Log("Save configuration success.");}else{Debug.LogWarning("Save configuration failure.");}}private void DrawPlatform(Platform platform, string platformName){m_Controller.SelectPlatform(platform, EditorGUILayout.ToggleLeft(platformName, m_Controller.IsPlatformSelected(platform)));}private void OnLoadingResource(int index, int count){EditorUtility.DisplayProgressBar("Loading Resources", Utility.Text.Format("Loading resources, {0}/{1} loaded.", index.ToString(), count.ToString()), (float)index / count);}private void OnLoadingAsset(int index, int count){EditorUtility.DisplayProgressBar("Loading Assets", Utility.Text.Format("Loading assets, {0}/{1} loaded.", index.ToString(), count.ToString()), (float)index / count);}private void OnLoadCompleted(){EditorUtility.ClearProgressBar();}private void OnAnalyzingAsset(int index, int count){EditorUtility.DisplayProgressBar("Analyzing Assets", Utility.Text.Format("Analyzing assets, {0}/{1} analyzed.", index.ToString(), count.ToString()), (float)index / count);}private void OnAnalyzeCompleted(){EditorUtility.ClearProgressBar();}private bool OnProcessingAssetBundle(string assetBundleName, float progress){if (EditorUtility.DisplayCancelableProgressBar("Processing AssetBundle", Utility.Text.Format("Processing '{0}'...", assetBundleName), progress)){EditorUtility.ClearProgressBar();return true;}else{Repaint();return false;}}private bool OnProcessingBinary(string binaryName, float progress){if (EditorUtility.DisplayCancelableProgressBar("Processing Binary", Utility.Text.Format("Processing '{0}'...", binaryName), progress)){EditorUtility.ClearProgressBar();return true;}else{Repaint();return false;}}private void OnProcessResourceComplete(Platform platform){EditorUtility.ClearProgressBar();Debug.Log(Utility.Text.Format("Build resources for '{0}' complete.", platform.ToString()));if (AppBuildSettings.Instance.RevealFolder){EditorUtility.RevealInFinder(UtilityBuiltin.ResPath.GetCombinePath(GetResourceOupoutPathByMode(AppSettings.Instance.ResourceMode), platform.ToString()));}}private void OnBuildResourceError(string errorMessage){EditorUtility.ClearProgressBar();Debug.LogWarning(Utility.Text.Format("Build resources error with error message '{0}'.", errorMessage));}}
}

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...