打包配置项
csharp
using UnityEngine;
using System.Collections.Generic;
namespace MYTOOL.Build
{
/// <summary>
/// 批量打包配置文件
/// </summary>
[CreateAssetMenu]
public class BatchBuildProfile : ScriptableObject
{
public List<BuildTask> tasks = new List<BuildTask>(0);
}
}
打包功能
csharp
using UnityEditor;
using UnityEngine;
using System;
using System.IO;
using System.Collections.Generic;
namespace MYTOOL.Build
{
public class LogMessage
{
public LogType type;
public string message;
public LogMessage(LogType type, string message)
{
this.type = type;
this.message = message;
}
}
[CustomEditor(typeof(BatchBuildProfile))]
public class BatchBuildProfileInspector : Editor
{
//配置文件
private BatchBuildProfile profile;
//折叠栏
private Dictionary<BuildTask, bool> foldoutMap;
//记录日志
private List<LogMessage> logsList;
private void OnEnable()
{
profile = target as BatchBuildProfile;
foldoutMap = new Dictionary<BuildTask, bool>();
logsList = new List<LogMessage>();
}
public override void OnInspectorGUI()
{
OnMenuGUI();
OnListGUI();
serializedObject.ApplyModifiedProperties();
if (GUI.changed)
EditorUtility.SetDirty(profile);
}
/// <summary>
/// 菜单项
/// </summary>
private void OnMenuGUI()
{
EditorGUILayout.HelpBox($"已有打包工作项:{profile.tasks.Count}个", MessageType.Info);
EditorGUILayout.HelpBox($"打包时会先进行排序, 优先打包当前平台【{EditorUserBuildSettings.activeBuildTarget}】", MessageType.Info);
//限制20个
if (profile.tasks.Count < 20)
{
//新建工作项
if (GUILayout.Button("新建工作项", GUILayout.Height(30)))
{
Undo.RecordObject(profile, "Create");
string buildPath = Path.Combine(Directory.GetParent(Application.dataPath).FullName, ".Build");
if (Directory.Exists(buildPath) == false)
{
Directory.CreateDirectory(buildPath);
Debug.LogFormat("创建构建目录:{0}", buildPath);
}
var task = new BuildTask(PlayerSettings.productName, (MyBuildTarget)EditorUserBuildSettings.activeBuildTarget, buildPath);
profile.tasks.Add(task);
}
}
else
{
EditorGUILayout.HelpBox($"无法新建打包工作项", MessageType.Warning);
}
if (profile.tasks.Count > 0)
{
//清空
GUI.color = Color.yellow;
if (GUILayout.Button("清空工作项", GUILayout.Height(30)))
{
Undo.RecordObject(profile, "Clear");
if (EditorUtility.DisplayDialog("提醒", "是否确认清理所有打包工作项?", "确定", "取消"))
{
Debug.LogWarningFormat("清理{0}个打包工作项", profile.tasks.Count);
profile.tasks.Clear();
foldoutMap.Clear();
}
}
//开始打包
GUI.color = Color.cyan;
if (GUILayout.Button("开始打包", GUILayout.Height(30)))
{
if (EditorUtility.DisplayDialog("确认操作", "即将开始打包过程,这可能需要一些时间。您希望继续吗?", "继续", "取消"))
{
logsList.Clear();
OnBuild(false);
}
return;
}
//清理并打包
GUI.color = Color.yellow;
if (GUILayout.Button("清理并打包", GUILayout.Height(30)))
{
if (EditorUtility.DisplayDialog("确认操作", "即将进行清理并开始打包过程,这可能需要一些时间。您希望继续吗?", "继续", "取消"))
{
if (EditorUtility.DisplayDialog("重要提醒", "清理操作将移除当前构建平台的所有文件,请确保已备份重要数据。是否要继续?此操作不可逆。", "确定继续", "取消"))
{
logsList.Clear();
OnBuild(true);
}
}
return;
}
}
GUI.color = Color.white;
//排序
if (profile.tasks.Count > 1)
{
if (GUILayout.Button("排序工作项", GUILayout.Height(30)))
{
Debug.Log("排序打包工作项");
profile.tasks.Sort(new BuildTaskComparer());
return;
}
}
}
/// <summary>
/// 任务项
/// </summary>
private void OnListGUI()
{
for (int i = 0; i < profile.tasks.Count; i++)
{
var task = profile.tasks[i];
if (foldoutMap.ContainsKey(task) == false)
{
foldoutMap.Add(task, true);
}
GUILayout.Space(20);
GUILayout.BeginHorizontal("Badge");
GUILayout.Space(20);
foldoutMap[task] = EditorGUILayout.Foldout(foldoutMap[task], task.ToString(), true);
if (GUILayout.Button(EditorGUIUtility.IconContent("TreeEditor.Trash"), "IconButton", GUILayout.Width(20)))
{
Undo.RecordObject(profile, "Delete Task");
foldoutMap.Remove(task);
profile.tasks.Remove(task);
break;
}
GUILayout.EndHorizontal();
//折叠栏
if (foldoutMap[task])
{
GUILayout.BeginVertical("Box");
//是否激活
GUILayout.BeginHorizontal();
GUILayout.Label("是否激活:", GUILayout.Width(70));
task.enableTask = GUILayout.Toggle(task.enableTask, "");
GUILayout.EndHorizontal();
//打包场景
GUILayout.BeginHorizontal();
GUILayout.Label("打包场景:", GUILayout.Width(70));
if (GUILayout.Button("+", GUILayout.Width(20f)))
{
task.sceneAssets.Add(null);
}
GUILayout.EndHorizontal();
//场景列表
if (task.sceneAssets.Count > 0)
{
OnSceneAssetsList(task);
}
//产品名称
GUILayout.BeginHorizontal();
GUILayout.Label("产品名称:", GUILayout.Width(70));
var newPN = GUILayout.TextField(task.productName);
if (task.productName != newPN)
{
Undo.RecordObject(profile, "Product Name");
task.productName = newPN;
}
GUILayout.EndHorizontal();
//打包平台
GUILayout.BeginHorizontal();
GUILayout.Label("打包平台:", GUILayout.Width(70));
var newBT = (MyBuildTarget)EditorGUILayout.EnumPopup(task.buildTarget);
if (task.buildTarget != newBT)
{
Undo.RecordObject(profile, "Build Target");
task.buildTarget = newBT;
//这些平台只能使用IL2CPP
if (task.buildTarget == MyBuildTarget.iOS || task.buildTarget == MyBuildTarget.WebGL || task.buildTarget == MyBuildTarget.WeixinMiniGame)
{
task.scriptMode = ScriptingImplementation.IL2CPP;
}
//其它平台默认切换到Player
if (task.buildTarget != MyBuildTarget.StandaloneWindows64 && task.buildTarget != MyBuildTarget.StandaloneLinux64 && task.buildTarget != MyBuildTarget.NoTarget)
{
task.buildSubtarget = StandaloneBuildSubtarget.Player;
}
}
GUILayout.EndHorizontal();
//Windows Linux添加打包子平台
if (task.buildTarget == MyBuildTarget.StandaloneWindows64 || task.buildTarget == MyBuildTarget.StandaloneLinux64)
{
GUILayout.BeginHorizontal();
GUILayout.Label("打包子平台:", GUILayout.Width(70));
var newBS = (StandaloneBuildSubtarget)EditorGUILayout.EnumPopup(task.buildSubtarget);
if (task.buildSubtarget != newBS)
{
Undo.RecordObject(profile, "Build Subtarget");
task.buildSubtarget = newBS;
}
GUILayout.EndHorizontal();
}
//打包选项
GUILayout.BeginHorizontal();
GUILayout.Label("打包选项:", GUILayout.Width(70));
var newBO = (BuildOptions)EditorGUILayout.EnumFlagsField(task.buildOptions);
if (task.buildOptions != newBO)
{
Undo.RecordObject(profile, "Build Options");
task.buildOptions = newBO;
}
GUILayout.EndHorizontal();
//脚本模式
GUILayout.BeginHorizontal();
GUILayout.Label("脚本模式:", GUILayout.Width(70));
var newSM = (ScriptingImplementation)EditorGUILayout.EnumPopup(task.scriptMode);
if (task.scriptMode != newSM)
{
Undo.RecordObject(profile, "Script Mode");
task.scriptMode = newSM;
}
GUILayout.EndHorizontal();
//打包路径
GUILayout.BeginHorizontal();
GUILayout.Label("打包路径:", GUILayout.Width(70));
GUILayout.TextField(task.buildPath);
if (GUILayout.Button("浏览", GUILayout.Width(40f)))
{
string path = EditorUtility.SaveFolderPanel("Build Path", task.buildPath, "");
if (!string.IsNullOrWhiteSpace(path))
{
task.buildPath = path;
}
GUIUtility.ExitGUI();
}
GUILayout.EndHorizontal();
//安卓平台添加其它选项
if (task.buildTarget == MyBuildTarget.Android)
{
GUILayout.BeginHorizontal();
GUILayout.Label("keystore:", GUILayout.Width(70));
PlayerSettings.Android.keystorePass = EditorGUILayout.PasswordField(PlayerSettings.Android.keystorePass);
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUILayout.Label("keyalias:", GUILayout.Width(70));
PlayerSettings.Android.keyaliasPass = EditorGUILayout.PasswordField(PlayerSettings.Android.keyaliasPass);
GUILayout.EndHorizontal();
//导出工程
GUILayout.BeginHorizontal();
GUILayout.Label("导出工程:", GUILayout.Width(70));
EditorUserBuildSettings.exportAsGoogleAndroidProject = GUILayout.Toggle(EditorUserBuildSettings.exportAsGoogleAndroidProject, "");
GUILayout.EndHorizontal();
}
GUILayout.EndVertical();
}
}
}
private void OnSceneAssetsList(BuildTask task)
{
GUILayout.BeginHorizontal();
GUILayout.Space(75);
GUILayout.BeginVertical("Badge");
for (int j = 0; j < task.sceneAssets.Count; j++)
{
var sceneAsset = task.sceneAssets[j];
GUILayout.BeginHorizontal();
GUILayout.Label($"{j + 1}.", GUILayout.Width(20));
task.sceneAssets[j] = EditorGUILayout.ObjectField(sceneAsset, typeof(SceneAsset), false) as SceneAsset;
if (GUILayout.Button("↑", "MiniButtonLeft", GUILayout.Width(20)))
{
if (j > 0)
{
Undo.RecordObject(profile, "Move Up Scene Assets");
var temp = task.sceneAssets[j - 1];
task.sceneAssets[j - 1] = sceneAsset;
task.sceneAssets[j] = temp;
}
}
if (GUILayout.Button("↓", "MiniButtonMid", GUILayout.Width(20)))
{
if (j < task.sceneAssets.Count - 1)
{
Undo.RecordObject(profile, "Move Down Scene Assets");
var temp = task.sceneAssets[j + 1];
task.sceneAssets[j + 1] = sceneAsset;
task.sceneAssets[j] = temp;
}
}
if (GUILayout.Button("+", "MiniButtonMid", GUILayout.Width(20)))
{
Undo.RecordObject(profile, "Add Scene Assets");
task.sceneAssets.Insert(j + 1, null);
break;
}
if (GUILayout.Button("-", "MiniButtonMid", GUILayout.Width(20)))
{
Undo.RecordObject(profile, "Delete Scene Assets");
task.sceneAssets.RemoveAt(j);
break;
}
GUILayout.EndHorizontal();
}
GUILayout.EndVertical();
GUILayout.EndHorizontal();
}
/// <summary>
/// 开始打包
/// </summary>
/// <param name="clearBuild">清理旧的构建</param>
private void OnBuild(bool clearBuild)
{
//排序,优先当前平台的任务
profile.tasks.Sort(new BuildTaskComparer());
//旧版本号
string oldVersion = PlayerSettings.bundleVersion;
//新版本号
string newVersion = GetNewVersion();
try
{
for (int i = 0; i < profile.tasks.Count; i++)
{
var task = profile.tasks[i];
if (task.enableTask == false || task.buildTarget == MyBuildTarget.NoTarget)
{
logsList.Add(new LogMessage(LogType.Log, $"跳过: {task}"));
continue;
}
BuildTarget buildTarget = (BuildTarget)task.buildTarget;
BuildTargetGroup targetGroup = BuildPipeline.GetBuildTargetGroup(buildTarget);
BuildPlayerOptions buildPlayerOptions = SetBuildParams(targetGroup, task);
EditorUtility.DisplayProgressBar("正在打包", profile.tasks[i].ToString(), (float)i + 1 / profile.tasks.Count);
if (string.IsNullOrEmpty(buildPlayerOptions.locationPathName))
{
throw new Exception(($"无法打包 {task},产品名称可能为空"));
}
if (buildPlayerOptions.scenes.Length == 0)
{
throw new Exception($"无法打包 {task},打包场景为空");
}
//切换平台
if (buildTarget != EditorUserBuildSettings.activeBuildTarget)
{
EditorUserBuildSettings.SwitchActiveBuildTarget(targetGroup, buildTarget);
}
//设置新版本号
PlayerSettings.bundleVersion = newVersion;
PlayerSettings.SetScriptingBackend(targetGroup, task.scriptMode);
string path = Path.GetDirectoryName(buildPlayerOptions.locationPathName);
if (clearBuild && Directory.Exists(path))
{
Directory.Delete(path, true);
}
if (Directory.Exists(path) == false)
{
Directory.CreateDirectory(path);
}
//开始打包
var report = BuildPipeline.BuildPlayer(buildPlayerOptions);
switch (report.summary.result)
{
case UnityEditor.Build.Reporting.BuildResult.Unknown:
logsList.Add(new LogMessage(LogType.Error, $"{task} 出现未知错误"));
break;
case UnityEditor.Build.Reporting.BuildResult.Succeeded:
logsList.Add(new LogMessage(LogType.Log, $"{task} 打包耗时: {(report.summary.buildEndedAt - report.summary.buildStartedAt).TotalSeconds}秒"));
break;
case UnityEditor.Build.Reporting.BuildResult.Failed:
string errorMsg = "";
foreach (var file in report.GetFiles())
{
errorMsg += file.path + "\n";
}
foreach (var step in report.steps)
{
foreach (var stepmsg in step.messages)
{
errorMsg += "\n" + stepmsg.content;
}
errorMsg += "\n";
}
logsList.Add(new LogMessage(LogType.Error, $"{task} 打包失败: {errorMsg}"));
break;
case UnityEditor.Build.Reporting.BuildResult.Cancelled:
logsList.Add(new LogMessage(LogType.Log, $"{task} 取消打包"));
break;
}
//打包成功,打开目录并记录版本号
if (report.summary.result == UnityEditor.Build.Reporting.BuildResult.Succeeded)
{
File.WriteAllText(string.Format("{0}/version.txt", path), newVersion);
Application.OpenURL(path);
}
}
}
catch (Exception ex)
{
//异常情况下还原版本号
PlayerSettings.bundleVersion = oldVersion;
Debug.LogFormat("还原打包版本号:{0}", oldVersion);
Debug.LogException(ex);
}
finally
{
EditorUtility.ClearProgressBar();
Debug.LogFormat("当前打包版本号:{0}", newVersion);
foreach (var log in logsList)
{
Debug.unityLogger.Log(log.type, log.message);
}
logsList.Clear();
}
}
/// <summary>
/// 获取新版本号
/// </summary>
/// <returns></returns>
private string GetNewVersion()
{
try
{
Version version = new Version(PlayerSettings.bundleVersion);
int major = version.Major; //主版本
int minor = version.Minor; //次版本
int build = version.Build; //构建版本
int revision = version.Revision + 1; //修订版本
if (revision >= 100)
{
build += 1;
revision = 0;
}
if (build >= 100)
{
minor += 1;
build = 0;
}
if (minor >= 100)
{
major += 1;
minor = 0;
}
return $"{major}.{minor}.{build}.{revision}";
}
catch (Exception)
{
return "1.0.0.0";
}
}
/// <summary>
/// 设置构建参数
/// </summary>
/// <param name="targetGroup"></param>
/// <param name="task"></param>
/// <returns></returns>
private BuildPlayerOptions SetBuildParams(BuildTargetGroup targetGroup, BuildTask task)
{
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
List<string> levels = new List<string>();
string[] activeLevels = EditorBuildSettingsScene.GetActiveSceneList(EditorBuildSettings.scenes);
if (activeLevels.Length > 0)
{
levels.AddRange(activeLevels);
}
for (int i = 0; i < task.sceneAssets.Count; i++)
{
var scenePath = AssetDatabase.GetAssetPath(task.sceneAssets[i]);
if (!string.IsNullOrEmpty(scenePath) && !levels.Contains(scenePath))
{
levels.Add(scenePath);
}
}
buildPlayerOptions.scenes = levels.ToArray();
buildPlayerOptions.target = (BuildTarget)task.buildTarget;
buildPlayerOptions.subtarget = (int)task.buildSubtarget;
buildPlayerOptions.targetGroup = targetGroup;
buildPlayerOptions.options = task.buildOptions;
buildPlayerOptions.locationPathName = GetBuildTargetPath(task.buildTarget, task.buildSubtarget, task.buildOptions, task.buildPath, task.productName);
return buildPlayerOptions;
}
/// <summary>
/// 获取构建路径
/// </summary>
/// <param name="buildTarget"></param>
/// <param name="buildOptions"></param>
/// <param name="buildPath"></param>
/// <param name="productName"></param>
/// <returns></returns>
private string GetBuildTargetPath(MyBuildTarget buildTarget, StandaloneBuildSubtarget buildSubtarget, BuildOptions buildOptions, string buildPath, string productName)
{
if (string.IsNullOrEmpty(productName))
{
return string.Empty;
}
bool isDevelopment = buildOptions.HasFlag(BuildOptions.Development);
string currentDate = DateTime.Now.ToString("yyMMdd");
string locationPathName = Path.Combine(buildPath, buildTarget.ToString(), buildSubtarget.ToString(), currentDate, productName);
switch (buildTarget)
{
case MyBuildTarget.StandaloneOSX:
{
if (isDevelopment)
locationPathName += "_dev.app";
else
locationPathName += ".app";
}
break;
case MyBuildTarget.StandaloneWindows64:
{
if (isDevelopment)
locationPathName += "_dev.exe";
else
locationPathName += ".exe";
}
break;
case MyBuildTarget.StandaloneLinux64:
{
if (isDevelopment)
locationPathName += "_dev.x86_64";
else
locationPathName += ".x86_64";
}
break;
case MyBuildTarget.Android:
{
if (isDevelopment)
locationPathName += $"_{currentDate}_dev";
else
locationPathName += $"_{currentDate}";
if (EditorUserBuildSettings.exportAsGoogleAndroidProject == false)
locationPathName += ".APK";
}
break;
case MyBuildTarget.iOS:
{
if (isDevelopment)
locationPathName += $"_{currentDate}_dev";
else
locationPathName += $"_{currentDate}";
}
break;
}
return locationPathName;
}
}
}
任务配置项
csharp
using System;
using System.Collections.Generic;
using UnityEditor;
namespace MYTOOL.Build
{
/// <summary>
/// 打包的目标平台
/// </summary>
public enum MyBuildTarget
{
NoTarget = -2,
//
// 摘要:
// Build a macOS standalone (Intel 64-bit).
StandaloneOSX = 2,
//
// 摘要:
// Build a Windows 64-bit standalone.
StandaloneWindows64 = 19,
//
// 摘要:
// Build a Linux 64-bit standalone.
StandaloneLinux64 = 24,
//
// 摘要:
// Build an iOS player.
iOS = 9,
//
// 摘要:
// Build an Android .apk standalone app.
Android = 13,
//
// 摘要:
// Build to WebGL platform.
WebGL = 20,
//
// 摘要:
// Build to WeixinMiniGame platform.
WeixinMiniGame = 47,
//
// 摘要:
// Build an OpenHarmony .hap standalone app.
OpenHarmony = 48,
}
/// <summary>
/// 打包工作项
/// </summary>
[Serializable]
public class BuildTask
{
/// <summary>
/// 是否激活
/// </summary>
public bool enableTask;
/// <summary>
/// 打包的产品名称
/// </summary>
public string productName;
/// <summary>
/// 打包的目标平台
/// </summary>
public MyBuildTarget buildTarget;
/// <summary>
/// 打包的目标子平台
/// </summary>
public StandaloneBuildSubtarget buildSubtarget;
/// <summary>
/// 打包的选项
/// </summary>
public BuildOptions buildOptions;
/// <summary>
/// 脚本模式
/// </summary>
public ScriptingImplementation scriptMode;
/// <summary>
/// 打包的保存路径
/// </summary>
public string buildPath;
/// <summary>
/// 打包的场景列表
/// </summary>
public List<SceneAsset> sceneAssets;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="productName">产品名称</param>
/// <param name="buildTarget">目标平台</param>
/// <param name="buildPath">保存路径</param>
public BuildTask(string productName, MyBuildTarget buildTarget, string buildPath)
{
this.productName = productName;
this.buildTarget = buildTarget;
this.buildPath = buildPath;
enableTask = true;
buildSubtarget = StandaloneBuildSubtarget.Player;
buildOptions = BuildOptions.CleanBuildCache;
scriptMode = ScriptingImplementation.IL2CPP;
sceneAssets = new List<SceneAsset>();
}
public override string ToString()
{
return string.Format("{0}【{1}】", productName, buildTarget);
}
}
class BuildTaskComparer : IComparer<BuildTask>
{
public int Compare(BuildTask x, BuildTask y)
{
if ((BuildTarget)x.buildTarget == EditorUserBuildSettings.activeBuildTarget && (BuildTarget)y.buildTarget != EditorUserBuildSettings.activeBuildTarget)
{
return -1; // x排在前
}
else if ((BuildTarget)x.buildTarget != EditorUserBuildSettings.activeBuildTarget && (BuildTarget)y.buildTarget == EditorUserBuildSettings.activeBuildTarget)
{
return 1; // y排在前
}
else
{
return x.buildTarget.ToString().CompareTo(y.buildTarget.ToString());
}
}
}
}
效果图,可以将它锁定在这里,方便后面使用
使用也很简单,选择打包的平台,并设置一些参数。点击开始打包或清理并打包。
注意:打包场景字段是额外添加, 每次打包都会先获取Build Settings里激活的场景,并添加上打包场景中的设置
其它解释:
有些字段是直接使用Unity的,所以数据是共享的,比如安卓特有的选项,一个地方修改,相对于的位置也发生改变。
构建目录格式:打包路径+打包平台+打包子平台+日期(yyMMdd)
为什么添加打包子平台字段,因为我的项目中需要打包服务端(Dedicated Server)