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;
#endif
namespace 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_NEWER
static 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);
#else
static PropertyInfo m_viewVisualTree = m_guiViewType.GetProperty("visualTree",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
#endif
static FieldInfo m_imguiContainerOnGui = typeof(IMGUIContainer).GetField("m_OnGUIHandler",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
static ScriptableObject m_currentToolbar;
/// <summary>
/// Callback for toolbar OnGUI method.
/// </summary>
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 changes
if (m_currentToolbar == null)
{
// Find toolbar
var toolbars = Resources.FindObjectsOfTypeAll(m_toolbarType);
m_currentToolbar = toolbars.Length > 0 ? (ScriptableObject)toolbars[0] : null;
if (m_currentToolbar != null)
{
#if UNITY_2021_1_OR_NEWER
var 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_NEWER
var windowBackend = m_windowBackend.GetValue(m_currentToolbar);
// Get it's visual tree
var visualTree = (VisualElement) m_viewVisualTree.GetValue(windowBackend, null);
#else
// Get it's visual tree
var visualTree = (VisualElement) m_viewVisualTree.GetValue(m_currentToolbar, null);
#endif
// Get first child which 'happens' to be toolbar IMGUIContainer
var container = (IMGUIContainer) visualTree[0];
// (Re)attach handler
var 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<Action> LeftToolbarGUI = new List<Action>();
public static readonly List<Action> RightToolbarGUI = new List<Action>();
static UnityEditorToolbar()
{
Type toolbarType = typeof(Editor).Assembly.GetType("UnityEditor.Toolbar");
#if UNITY_2019_1_OR_NEWER
string fieldName = "k_ToolCount";
#else
string fieldName = "s_ShownToolIcons";
#endif
FieldInfo toolIcons = toolbarType.GetField(fieldName,
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
#if UNITY_2019_3_OR_NEWER
m_toolCount = toolIcons != null ? ((int)toolIcons.GetValue(null)) : 8;
#elif UNITY_2019_1_OR_NEWER
m_toolCount = toolIcons != null ? ((int) toolIcons.GetValue(null)) : 7;
#elif UNITY_2018_1_OR_NEWER
m_toolCount = toolIcons != null ? ((Array) toolIcons.GetValue(null)).Length : 6;
#else
m_toolCount = toolIcons != null ? ((Array) toolIcons.GetValue(null)).Length : 5;
#endif
ToolbarCallback.OnToolbarGUI = OnGUI;
ToolbarCallback.OnToolbarGUILeft = GUILeft;
ToolbarCallback.OnToolbarGUIRight = GUIRight;
}
#if UNITY_2019_3_OR_NEWER
public const float space = 8;
#else
public const float space = 10;
#endif
public const float largeSpace = 20;
public const float buttonWidth = 32;
public const float dropdownWidth = 80;
#if UNITY_2019_1_OR_NEWER
public const float playPauseStopWidth = 140;
#else
public const float playPauseStopWidth = 100;
#endif
static void OnGUI()
{
// Create two containers, left and right
// Screen is whole toolbar
if (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 left
leftRect.xMin += buttonWidth * m_toolCount; // Tool buttons
#if UNITY_2019_3_OR_NEWER
leftRect.xMin += space; // Spacing between tools and pivot
#else
leftRect.xMin += largeSpace; // Spacing between tools and pivot
#endif
leftRect.xMin += 64 * 2; // Pivot buttons
leftRect.xMax = playButtonsPosition;
Rect rightRect = new Rect(0, 0, screenWidth, Screen.height);
rightRect.xMin = playButtonsPosition;
rightRect.xMin += m_commandStyle.fixedWidth * 3; // Play buttons
rightRect.xMax = screenWidth;
rightRect.xMax -= space; // Spacing right
rightRect.xMax -= dropdownWidth; // Layout
rightRect.xMax -= space; // Spacing between layout and layers
rightRect.xMax -= dropdownWidth; // Layers
#if UNITY_2019_3_OR_NEWER
rightRect.xMax -= space; // Spacing between layers and account
#else
rightRect.xMax -= largeSpace; // Spacing between layers and account
#endif
rightRect.xMax -= dropdownWidth; // Account
rightRect.xMax -= space; // Spacing between account and cloud
rightRect.xMax -= buttonWidth; // Cloud
rightRect.xMax -= space; // Spacing between cloud and collab
rightRect.xMax -= 78; // Colab
// Add spacing around existing controls
leftRect.xMin += space;
leftRect.xMax -= space;
rightRect.xMin += space;
rightRect.xMax -= space;
// Add top and bottom margins
#if UNITY_2019_3_OR_NEWER
leftRect.y = 4;
leftRect.height = 22;
rightRect.y = 4;
rightRect.height = 22;
#else
leftRect.y = 5;
leftRect.height = 24;
rightRect.y = 5;
rightRect.height = 24;
#endif
if (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右侧绘制UI
if (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<ResourceComponent>();
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>("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_HYBRIDCLR
HybridCLR.Editor.Commands.PrebuildCommand.GenerateAll();
#endif
var 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
{
/// <summary>
/// 资源生成器。
/// </summary>
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<AppBuildEidtor>("App Builder", true);
#if UNITY_2019_3_OR_NEWER
window.minSize = new Vector2(800f, 800f);
#else
window.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<AppSettings>(), "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_HYBRIDCLR
MyGameTools.DisableHybridCLR();
#endif
}
else
{
#if DISABLE_HYBRIDCLR
MyGameTools.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_ANDROID
AppBuildSettings.Instance.BuildForGooglePlay = EditorUserBuildSettings.buildAppBundle = EditorGUILayout.ToggleLeft("Build App Bundle(GP)", AppBuildSettings.Instance.BuildForGooglePlay);
#endif
AppBuildSettings.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_ANDROID
EditorGUILayout.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_IOS
EditorGUILayout.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_HYBRIDCLR
HybridCLR.Editor.Commands.PrebuildCommand.GenerateAll();
#endif
var 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));
}
}
}