Unity 无论是测试还是上线都需要打包,而每次打包我们还要打各种平台(安卓、Ios、WebGL、Windows…),有可能不同的打包时机还要有不同的配置项,这种工作枯燥、繁琐且易错,为了解决这一困扰就想到了能不能做一个工具来专门负责打包,甚至打完包能够自动的发送给测试人员或直接上传至服务器就更完美了。然后查了一些资料,发现Jenkins就是一种特别完美的工具,能够完美的解决我刚才提出的所有问题,至于Jenkins的搭建本章内容不会关注,这篇文章主要是解决Jenkins流水线配置的最后阶段,通过Jenkins调用Shell进而通知Unity自动打包的逻辑。
先看一下Unity官方文档对命令行参数的描写:命令行参数。
可以看出Unity官方其实是支持开发者们通过自动打包工具来提升效率的,并且也准备了大量的命令行参数来进行支持。
再通过对shell编程的了解,我编写了下面的shell脚本来通知Unity自动打包并设置指定配置,如下:
#!/bin/sh
#Unity安装路径
UnityPath="C:\Program Files\Unity\Hub\Editor\2020.3.22f1c1\Editor\Unity.exe"
#工程路径
ProjectPath="E:\ARWork\AutoBuildDemo"
#UnityLog输出路径
UnityLogPath="C:\Users\Administrator\Desktop\ShellTest\unitylog.txt"
#需要调用Unity的静态方法
UnityFunc=S.Utility.Editor.ShellHelper.OnReceive
#版本号
Version=1.0.0
#目标平台 Android、iOS、WebGL、StandaloneWindows、StandaloneWindows64
BuildTarget=iOS
#获取当前时间
Time=$(date "+%Y-%m-%d_%H-%M-%S")
#输出的app名字或文件夹名字
AppName="OutRoot/$BuildTarget/$Time"
#Unity要执行的行为
Do="Build"
echo 开始启动Unity工程并执行方法命令
"${UnityPath}" -ProjectPath $ProjectPath -batchmode -quit -executeMethod $UnityFunc -logFile $UnityLogPath Version=$Version BuildTarget=$BuildTarget AppName=$AppName Do=$Do
# 控制台输出Unity的Log信息
while read line
do
echo $line
done < $UnityLogPath
echo Unity处理完毕
我们只需要把UnityPath、ProjectPath配置成自己的路径,UnityLogPath可以写成固定的路径也可以接收Jenkins的参数,UnityFunc
是需要调用的Unity中的静态方法,这个在后面会讲到,至于Version、BuildTarget、AppName这几个参数动态可变我们可以通过$n 来接收Jenkins的参数来进行赋值,Do是通知Unity要做的操作,然后Unity会接收到这个操作标记并对应执行相关操作,这个也会在后面讲。
至于下面的while循环,其实就是读取了Unity执行后的log日志文件并逐行在控制台打印出来。
其实通过上面的解释,我们应该能知道,最重要的点就是UnityFunc和Do这两个字段的含义,UnityFunc是指向Unity工程里的一个静态方法,这个方法不可接受参数,这样就达成了Shell与Unity的通讯,虽然这个静态方法不支持接收参数,但是Unity确做了相关的处理能够拿到shell的参数,其实我们的参数就是Version=$Version BuildTarget=$BuildTarget AppName=$AppName Do=$Do 当然,这里不严谨,其实整段语句都是Unity可接受的参数,只不过我们只取我们关注的这几个信息而已。
那下面我们来看看Unity怎么接受这个shell指令。
ShellHelper
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;
namespace S.Utility.Editor
{
/// <summary>
/// Shell传递过来的参数集
/// </summary>
public interface IShellParams
{
/// <summary>
/// 是否存在参数Key
/// </summary>
/// <param name="key">参数key</param>
/// <returns></returns>
bool HasKey(string key);
/// <summary>
/// 得到参数值
/// </summary>
/// <param name="key">参数key</param>
/// <returns></returns>
string GetValue(string key);
/// <summary>
/// 得到所有的参数key
/// </summary>
/// <returns></returns>
string[] GetKeys();
}
/// <summary>
/// Shell命令执行接口
/// </summary>
public interface IDo
{
/// <summary>
/// 执行
/// </summary>
/// <param name="shellParams">Shell参数</param>
void Do(IShellParams shellParams);
}
/// <summary>
/// Shell解析辅助器
/// </summary>
public class ShellHelper
{
private static Dictionary<string, string> commandLineDict = new Dictionary<string, string>();
public static IShellParams shellParams { get; private set; }
private static DoConfig config = new DoConfig();
/// <summary>
/// 去解析doType的行为
/// </summary>
/// <param name="doType">doType</param>
private static void Do(string doType)
{
Debug.Log($"开始处理Do {doType} 行为!");
config.GetDo(doType)?.Do(shellParams);
}
/// <summary>
/// 接收到Shell消息
/// </summary>
public static void OnReceive()
{
if (shellParams == null) shellParams = new ShellParams();
(shellParams as ShellParams).Refresh();
string doType = shellParams.GetValue("Do");
if (string.IsNullOrEmpty(doType))
{
Debug.LogError("Unity处理Shell消息失败,未找到Do指令!");
}
else
{
Do(doType);
}
}
/// <summary>
/// shell参数集的实现
/// </summary>
private class ShellParams : IShellParams
{
private Dictionary<string, string> commandLineDict = new Dictionary<string, string>();
/// <summary>
/// 参数key-value分割符
/// </summary>
public char splitChar = '=';
public void Refresh()
{
if (commandLineDict == null) commandLineDict = new Dictionary<string, string>();
else commandLineDict.Clear();
string[] parameters = Environment.GetCommandLineArgs();
int paramCount = parameters == null ? 0 : parameters.Length;
if (paramCount == 0) return;
string pattern = $"^(.*?){splitChar}(.*)";
foreach (var p in parameters)
{
Match match = Regex.Match(p, pattern);
GroupCollection groups = match.Groups;
if (groups == null || groups.Count != 3) commandLineDict[p] = null;
else
{
commandLineDict[groups[1].Value] = groups[2].Value;
}
}
}
public bool HasKey(string key)
{
if (commandLineDict == null || commandLineDict.Count == 0) return false;
return commandLineDict.ContainsKey(key);
}
public string GetValue(string key)
{
if (commandLineDict == null || commandLineDict.Count == 0) return null;
string value;
if (commandLineDict.TryGetValue(key, out value))
{
return value;
}
return null;
}
public string[] GetKeys()
{
if (commandLineDict == null || commandLineDict.Count == 0) return null;
return commandLineDict.Keys.ToArray();
}
}
}
}
DoConfig
namespace S.Utility.Editor
{
/// <summary>
/// Shell Do配置
/// </summary>
public class DoConfig
{
public IDo GetDo(string doType)
{
switch (doType)
{
case "Build":
return new DoBuild();
default:
return null;
}
}
}
}
DoBuild
using System;
using System.IO;
using UnityEditor;
using UnityEngine.SceneManagement;
namespace S.Utility.Editor
{
/// <summary>
/// 打包操作
/// </summary>
public class DoBuild : IDo
{
public struct BuildData
{
public BuildTarget buildTarget;
public string version;
public string appName;
public override string ToString()
{
return $"BuildTarget:{buildTarget} Version:{version} AppName:{appName}";
}
}
public BuildData buildData { get; private set; }
public void Do(IShellParams shellParams)
{
string buildTarget = shellParams.GetValue("BuildTarget");
string version = shellParams.GetValue("Version");
string appName = shellParams.GetValue("AppName");
buildData = new BuildData()
{
version = string.IsNullOrEmpty(version) ? PlayerSettings.bundleVersion : version,
buildTarget = string.IsNullOrEmpty(buildTarget)
? EditorUserBuildSettings.activeBuildTarget
: (BuildTarget)Enum.Parse(typeof(BuildTarget),buildTarget),
appName = string.IsNullOrEmpty(appName) ? DateTime.Now.ToString() : appName
};
PlayerSettings.bundleVersion = buildData.version;
if (EditorUserBuildSettings.activeBuildTarget != buildData.buildTarget) //当前平台不是目标平台
{
//切换至目标平台
BuildTargetGroup buildTargetGroup = BuildPipeline.GetBuildTargetGroup(buildData.buildTarget);
EditorUserBuildSettings.SwitchActiveBuildTarget(buildTargetGroup, buildData.buildTarget);
}
switch (buildData.buildTarget)
{
case BuildTarget.Android:
BuildAndroid();
break;
case BuildTarget.iOS:
BuildIos();
break;
case BuildTarget.WebGL:
BuildWebGL();
break;
case BuildTarget.StandaloneWindows:
case BuildTarget.StandaloneWindows64:
BuildWindows();
break;
}
}
/// <summary>
/// 打安卓包
/// </summary>
void BuildAndroid()
{
string outPath = buildData.appName + ".apk";
BuildPlayerOptions options = GetBaseBuildOptions();
options.locationPathName = outPath;
BuildPipeline.BuildPlayer(options);
}
/// <summary>
/// 打Ios包
/// </summary>
void BuildIos()
{
string outPath = buildData.appName;
if (Directory.Exists(outPath))
{
Directory.Delete(outPath);
}
BuildPlayerOptions options = new BuildPlayerOptions();
options.locationPathName = outPath;
options.target = buildData.buildTarget;
options.targetGroup = BuildPipeline.GetBuildTargetGroup(buildData.buildTarget);
BuildPipeline.BuildPlayer(options);
}
/// <summary>
/// 打WebGL包
/// </summary>
void BuildWebGL()
{
string outPath = buildData.appName;
if (Directory.Exists(outPath))
{
Directory.Delete(outPath);
}
BuildPlayerOptions options = GetBaseBuildOptions();
options.locationPathName = outPath;
BuildPipeline.BuildPlayer(options);
}
/// <summary>
/// 打Windows包
/// </summary>
void BuildWindows()
{
string outPath = buildData.appName;
if (Directory.Exists(outPath))
{
Directory.Delete(outPath);
}
BuildPlayerOptions options = GetBaseBuildOptions();
options.locationPathName = outPath;
BuildPipeline.BuildPlayer(options);
}
/// <summary>
/// 得到基础的打包配置
/// </summary>
/// <returns></returns>
private BuildPlayerOptions GetBaseBuildOptions()
{
BuildPlayerOptions options = new BuildPlayerOptions();
options.target = buildData.buildTarget;
options.targetGroup = BuildPipeline.GetBuildTargetGroup(buildData.buildTarget);
options.scenes = GetAllScenePath();
return options;
}
/// <summary>
/// 获取PlayerSettings中所有场景的路径
/// </summary>
/// <returns></returns>
private string[] GetAllScenePath()
{
int count = SceneManager.sceneCountInBuildSettings;
if (count == 0) return null;
string[] sceneArray = new string[count];
for (int i = 0; i < count; i++)
{
sceneArray[i] = SceneUtility.GetScenePathByBuildIndex(i);
}
return sceneArray;
}
}
}
分析一下C#这部分的代码:
首先最主要的就是ShellHelper对象的OnReceive方法,这个方法就是上面的shell中的UnityFunc字段,它负责响应Shell的指令。
然后我们通过(shellParams as ShellParams).Refresh();刷新了shell传递过来的参数,最后包装成IShellParams接口来传递这些参数。
然后我们在ShellHelper对象的Do(string doType) 方法通过DoConfig配置对象来获取对应的IDo执行对象,在执行对象中去具体实现指令逻辑。
所以后期如果我们想解析其它指令,例如BuildAB(打AB包)我们只需要在DoConfig的GetDo(string doType)方法中指定相应的解析IDo对象就可以完美的实现功能。
OK到这里我们的Unity就可以接收Shell的指令和参数了,后续再和Jenkins联动就可以实现自动打包的功能了。