【狂云歌之unity_vr】unity項(xiàng)目持續(xù)集成dailybuild以及多平臺(tái)打包管理

前言
持續(xù)集成的意義就不多說(shuō)了。unity通常打包一般就直接build&run,但是在實(shí)際項(xiàng)目中,往往直接在服務(wù)器build包,所以命令行打包必不可少,這里一方面分享unity打包做持續(xù)集成,一方面分享使用unity管理多平臺(tái)打包,例如一個(gè)vrapp需要支持gear版本,支持小米版本,支持cardboard版本等等~懂的人就知道這里具有一定的管理維護(hù)成本。
我們做vr相關(guān)的app,需要支持gear、cardboard、小米、vivo、大朋、暴風(fēng)、Idealens、pico、nibiru、酷開等一大堆平臺(tái),曾經(jīng)還有l(wèi)g和htcvive、oculus平臺(tái),未來(lái)還會(huì)有更多的平臺(tái),所以關(guān)于unity項(xiàng)目的多平臺(tái)管理是很重要的,在這方面我們也在探索,積累了一點(diǎn)經(jīng)驗(yàn)。這里介紹的主要是基于unity中c#寫的打包和多平臺(tái)管理,如果將其中一部分功能使用python和其他配置文件來(lái)實(shí)現(xiàn)也是可以的,只是用c#直接做會(huì)方便許多。
jenkins

jenkins不用多說(shuō),懂的人都了解是干什么的,來(lái)源于hudson,可以比較容易的搭起一個(gè)持續(xù)集成服務(wù)器,支持svn和git等版本管理。支持bash,所以可以用bash、python等大部分腳本來(lái)寫打包腳本和前后的處理。
unity命令行打包
unity如何進(jìn)行命令行打包呢,其實(shí)unity是支持以命令行方式啟動(dòng)的,但是需要關(guān)閉editor支持執(zhí)行命令,如下:
${unity可執(zhí)行文件路徑} -projectPath ${項(xiàng)目路徑}
-executeMethod CloudBuild.PerformBuildAndroidCloudAlphaRelease
-batchmode -quit -logFile ${放log的路徑}
-ForceExitEditor
整體比較容易理解,其中CloudBuild.PerformBuildAndroidCloudAlphaRelease是一個(gè)類的靜態(tài)方法,然后在這個(gè)方法中寫打包相關(guān)邏輯即可。
EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTarget.Android);
string[] scenes = { "Assets/Scenes/Init.unity", "Assets/Scenes/Main.unity" };
BuildPipeline.BuildPlayer(scenes, buildpath, BuildTarget.Android, BuildOptions.None);
核心邏輯就這么多,就會(huì)開始打一個(gè)android包并且生成到buildpath下面
多平臺(tái)打包管理
準(zhǔn)備工作
對(duì)unity插件不熟悉的可以看下 開發(fā)unity插件——一次搞定unity編輯器常用功能
我這里準(zhǔn)備了一個(gè)全局的配置文件,當(dāng)然這個(gè)可以使用外部配置文件來(lái)管理配置,是一個(gè)道理的。這里定義了兩個(gè)平臺(tái)版本一個(gè)是alpha一個(gè)是beta,使用宏來(lái)區(qū)分不同平臺(tái)的版本號(hào)。
using UnityEngine;
using System.Collections;
public class GlobalConfig {
#if CLOUD_ALPHA
public const int ClientVersionCode = 1;
public const string ClientVersion = "1.0";
#elif CLOUD_BETA
public const int ClientVersionCode = 2;
public const string ClientVersion = "1.1";
#endif
}
文件目錄組織大概如下,其中CloudBuild是編輯器工具,包含菜單項(xiàng)和打包功能,兩個(gè)平臺(tái)分別依賴不同的so文件和manifest文件。

制作了一個(gè)menu,主要包含的功能是可以在alpha和beta平臺(tái)之間切換,可以打alpha平臺(tái)的apk包,當(dāng)然想打beta平臺(tái)的包只需要簡(jiǎn)單修改。

manifest管理
寫個(gè)簡(jiǎn)單的腳本進(jìn)行manifest替換就好,對(duì)于alpha平臺(tái)和beta平臺(tái)各有自己的manifest文件,在切換平臺(tái)的時(shí)候?qū)?duì)應(yīng)的manifest復(fù)制替換。
/// <summary>
/// 使用相應(yīng)的androidmanifest
/// </summary>
static void UseAndroidManifest(string filename)
{
string src_filename = string.Format("AndroidManifest-{0}.xml", filename);
string dst_filename = "AndroidManifest.xml";
string path = Application.dataPath + "/Plugins/Android/";
File.Copy(path + src_filename, path + dst_filename, true);
AssetDatabase.Refresh();//因?yàn)樾薷牧薽anifest文件,所以刷新unity的assets
}
依賴包管理
為什么要做依賴包管理呢?因?yàn)樵谑褂貌煌脚_(tái)sdk的時(shí)候,可能會(huì)引入很多sdk,每個(gè)sdk里包含自己的so、jar、aar包等,如果什么都不管理,直接打包的話,那么這些依賴的文件都會(huì)打進(jìn)所有的apk包,簡(jiǎn)單來(lái)說(shuō)就會(huì)增加包的體積,更嚴(yán)重的情況下,這些不同平臺(tái)sdk里的依賴庫(kù)可能還會(huì)有沖突,如果打進(jìn)同一個(gè)apk包,后果不堪設(shè)想~
做依賴包管理主要依賴unity自己的assetimport管理如下圖,那么只要在需要的時(shí)候勾選不需要的時(shí)候取消勾選就好了,我們要做的就是用代碼來(lái)自動(dòng)實(shí)現(xiàn)這個(gè)功能。

先準(zhǔn)備好各個(gè)平臺(tái)的依賴包路徑
static string[] Plugins_Alpha = new string[] {
"Assets/Plugins/Android/libs/armeabi-v7a/alpha.so",
};
static string[] Plugins_Beta = new string[] {
"Assets/Plugins/Android/libs/armeabi-v7a/beta.so",
};
然后在切換不同平臺(tái)的時(shí)候?qū)@些依賴包的import做處理,這塊Asset屬于plugin,所以使用pluginimporter來(lái)管理勾選的問題。
static void ChangePluginToAlpha()
{
SetEnablePluginImport(Plugins_Alpha, true);
SetEnablePluginImport(Plugins_Beta, false);
}
static void ChangePluginToBeta()
{
SetEnablePluginImport(Plugins_Alpha, false);
SetEnablePluginImport(Plugins_Beta, true);
}
static void SetEnablePluginImport(string[] plugins, bool enable = true)
{
foreach(var path in plugins)
{
PluginImporter vrlib = AssetImporter.GetAtPath(path) as PluginImporter;
vrlib.SetCompatibleWithPlatform(BuildTarget.Android, enable);
}
}
非常簡(jiǎn)單一看就可以懂,然后試一下就明白了。
平臺(tái)切換
平臺(tái)切換功能主要是在editor里調(diào)試各個(gè)平臺(tái)功能的時(shí)候使用的菜單項(xiàng),功能也很簡(jiǎn)單,就做了下面幾件事情
- 切平臺(tái)和宏定義
- 切playersetting參數(shù)
- 切buildscene配置
- 切manifest和依賴包
- 保存及打開對(duì)應(yīng)平臺(tái)的場(chǎng)景(如果場(chǎng)景不是復(fù)用的)
代碼示例如下,因?yàn)槲覀冏鰒r相關(guān)的app,所以在gear平臺(tái)時(shí)vrsupport為true,其他平臺(tái)時(shí)為false,如果不同平臺(tái)的scene不一樣,那么在最后問用戶是否保存當(dāng)前場(chǎng)景,然后打開對(duì)應(yīng)平臺(tái)的場(chǎng)景。
/// <summary>
/// 切換alpha平臺(tái)
/// </summary>
[UnityEditor.MenuItem("CloudBuild/SwitchToAlpha", priority = 50)]
static void SwitchToAlpha()
{
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android, DEFINES_ALPHA);
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.iOS, DEFINES_ALPHA);
PlayerSettings.virtualRealitySupported = true;
EditorBuildSettings.scenes = new EditorBuildSettingsScene[] {
new EditorBuildSettingsScene("Assets/Scenes/Init.unity", true),
new EditorBuildSettingsScene("Assets/Scenes/Main.unity", true)
};//場(chǎng)景
UseAndroidManifest("Alpha");
ChangePluginToAlpha();
EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
EditorSceneManager.OpenScene("Assets/Scenes/Main.unity");
}
打包
那么最后打包腳本如下,粗看信息量可能比較大,實(shí)際只做了幾件事情
- 準(zhǔn)備好要打包的scene
- 將當(dāng)前editor的狀態(tài)保存一下,以便打完包恢復(fù),這是為了開發(fā)使用方便而已,否則在開發(fā)機(jī)上打個(gè)包就發(fā)現(xiàn)editor的很多屬性變了有時(shí)很尷尬
- 處理android的簽名問題
- 打包
- 最后如果是windows,一般是開發(fā)機(jī),直接打開build好的apk所在文件夾,方便使用
/// <summary>
/// </summary>
[UnityEditor.MenuItem("CloudBuild/CloudAlpha-Release")]
static void PerformBuildAndroidCloudAlphaRelease()
{
EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTarget.Android);
string[] scenes = { "Assets/Scenes/Init.unity", "Assets/Scenes/Main.unity" };
string path = GetBuildPathAndroid();
if (scenes == null || scenes.Length == 0 || path == null)
{
Debug.LogError("error scene is null");
return;
}
string tempid = PlayerSettings.bundleIdentifier;
string name = PlayerSettings.productName;
bool virtualRealitySupported = PlayerSettings.virtualRealitySupported;
PlayerSettings.virtualRealitySupported = true;
PlayerSettings.bundleIdentifier = "net.itsong.vralpha";
PlayerSettings.productName = PRODUCT_NAME;
PlayerSettings.Android.keystoreName = "";
PlayerSettings.Android.keyaliasName = "";
PlayerSettings.Android.keyaliasPass = "";
PlayerSettings.Android.keystorePass = "";
PlayerSettings.Android.bundleVersionCode = GlobalConfig.ClientVersionCode;
PlayerSettings.bundleVersion = GlobalConfig.ClientVersion;
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android);
UseAndroidManifest("Alpha");
ChangePluginToAlpha();
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android, DEFINES_ALPHA + DEFINES_RELEASE);
string buildpath = path + string.Format("cloudvr_alpha_release_{0}.apk", DateTime.Now.ToString("MMddHHmm", DateTimeFormatInfo.InvariantInfo));
BuildPipeline.BuildPlayer(scenes, buildpath, BuildTarget.Android, BuildOptions.None);
PlayerSettings.virtualRealitySupported = virtualRealitySupported;
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android, defines);
PlayerSettings.bundleIdentifier = tempid;
PlayerSettings.productName = name;
PlayerSettings.Android.keystoreName = "";
PlayerSettings.Android.keyaliasName = "";
PlayerSettings.Android.keyaliasPass = "";
PlayerSettings.Android.keystorePass = "";
string dir = path.Replace('/', '\\');
#if UNITY_EDITOR_WIN
System.Diagnostics.Process.Start("explorer.exe", "\"" + dir + "\"");
#endif
}
完整的CloudBuild文件如下:
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.IO;
using System;
using System.Globalization;
using UnityEditor.SceneManagement;
/// <summary>
/// </summary>
partial class CloudBuild
{
const string PRODUCT_NAME = "狂云歌VR";
const string DEFINES_ALPHA = "CROSS_PLATFORM_INPUT;MOBILE_INPUT;CLOUD_ALPHA;";
const string DEFINES_BETA = "CROSS_PLATFORM_INPUT;MOBILE_INPUT;CLOUD_BETA;";
const string DEFINES_RELEASE = "CLOUD_RELEASE";
static string[] Plugins_Alpha = new string[] {
"Assets/Plugins/Android/libs/armeabi-v7a/alpha.so",
};
static string[] Plugins_Beta = new string[] {
"Assets/Plugins/Android/libs/armeabi-v7a/beta.so",
};
// Build the Android APK and place into main project folder
static string GetBuildPathAndroid()
{
string dirPath = Application.dataPath + "/../build/android/";
if (!System.IO.Directory.Exists(dirPath))
{
System.IO.Directory.CreateDirectory(dirPath);
}
return dirPath;
}
/// <summary>
/// </summary>
[UnityEditor.MenuItem("CloudBuild/CloudAlpha-Release")]
static void PerformBuildAndroidCloudAlphaRelease()
{
EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTarget.Android);
string[] scenes = { "Assets/Scenes/Init.unity", "Assets/Scenes/Main.unity" };
string path = GetBuildPathAndroid();
if (scenes == null || scenes.Length == 0 || path == null)
{
Debug.LogError("error scene is null");
return;
}
string tempid = PlayerSettings.bundleIdentifier;
string name = PlayerSettings.productName;
bool virtualRealitySupported = PlayerSettings.virtualRealitySupported;
PlayerSettings.virtualRealitySupported = true;
PlayerSettings.bundleIdentifier = "net.itsong.vralpha";
PlayerSettings.productName = PRODUCT_NAME;
PlayerSettings.Android.keystoreName = "";
PlayerSettings.Android.keyaliasName = "";
PlayerSettings.Android.keyaliasPass = "";
PlayerSettings.Android.keystorePass = "";
PlayerSettings.Android.bundleVersionCode = GlobalConfig.ClientVersionCode;
PlayerSettings.bundleVersion = GlobalConfig.ClientVersion;
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android);
UseAndroidManifest("Alpha");
ChangePluginToAlpha();
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android, DEFINES_ALPHA + DEFINES_RELEASE);
string buildpath = path + string.Format("cloudvr_alpha_release_{0}.apk", DateTime.Now.ToString("MMddHHmm", DateTimeFormatInfo.InvariantInfo));
BuildPipeline.BuildPlayer(scenes, buildpath, BuildTarget.Android, BuildOptions.None);
PlayerSettings.virtualRealitySupported = virtualRealitySupported;
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android, defines);
PlayerSettings.bundleIdentifier = tempid;
PlayerSettings.productName = name;
PlayerSettings.Android.keystoreName = "";
PlayerSettings.Android.keyaliasName = "";
PlayerSettings.Android.keyaliasPass = "";
PlayerSettings.Android.keystorePass = "";
string dir = path.Replace('/', '\\');
#if UNITY_EDITOR_WIN
System.Diagnostics.Process.Start("explorer.exe", "\"" + dir + "\"");
#endif
}
/// <summary>
/// 切換alpha平臺(tái)
/// </summary>
[UnityEditor.MenuItem("CloudBuild/SwitchToAlpha", priority = 50)]
static void SwitchToAlpha()
{
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android, DEFINES_ALPHA);
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.iOS, DEFINES_ALPHA);
PlayerSettings.virtualRealitySupported = true;
EditorBuildSettings.scenes = new EditorBuildSettingsScene[] {
new EditorBuildSettingsScene("Assets/Scenes/Init.unity", true),
new EditorBuildSettingsScene("Assets/Scenes/Main.unity", true)
};//場(chǎng)景
UseAndroidManifest("Alpha");
ChangePluginToAlpha();
EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
EditorSceneManager.OpenScene("Assets/Scenes/Main.unity");
}
/// <summary>
/// 切換beta平臺(tái)
/// </summary>
[UnityEditor.MenuItem("CloudBuild/SwitchToBeta", priority = 50)]
static void SwitchToBeta()
{
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android, DEFINES_BETA);
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.iOS, DEFINES_BETA);
PlayerSettings.virtualRealitySupported = true;
EditorBuildSettings.scenes = new EditorBuildSettingsScene[] {
new EditorBuildSettingsScene("Assets/Scenes/Init.unity", true),
new EditorBuildSettingsScene("Assets/Scenes/Main.unity", true)
};//場(chǎng)景
UseAndroidManifest("Beta");
ChangePluginToBeta();
EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
EditorSceneManager.OpenScene("Assets/Scenes/Main.unity");
}
/// <summary>
/// 使用相應(yīng)的androidmanifest
/// </summary>
static void UseAndroidManifest(string filename)
{
string src_filename = string.Format("AndroidManifest-{0}.xml", filename);
string dst_filename = "AndroidManifest.xml";
string path = Application.dataPath + "/Plugins/Android/";
File.Copy(path + src_filename, path + dst_filename, true);
PlayerSettings.Android.bundleVersionCode = GlobalConfig.ClientVersionCode;
PlayerSettings.bundleVersion = GlobalConfig.ClientVersion;
AssetDatabase.Refresh();
}
static void ChangePluginToAlpha()
{
SetEnablePluginImport(Plugins_Alpha, true);
SetEnablePluginImport(Plugins_Beta, false);
}
static void ChangePluginToBeta()
{
SetEnablePluginImport(Plugins_Alpha, false);
SetEnablePluginImport(Plugins_Beta, true);
}
static void SetEnablePluginImport(string[] plugins, bool enable = true)
{
foreach(var path in plugins)
{
PluginImporter vrlib = AssetImporter.GetAtPath(path) as PluginImporter;
vrlib.SetCompatibleWithPlatform(BuildTarget.Android, enable);
}
}
}
后續(xù)
這里沒寫build ios ipa包的過(guò)程,ios的build過(guò)程會(huì)稍微長(zhǎng)一些,要先build好xcode project然后再通過(guò)xcode的命令行去打包,所以前半部分與android是可以復(fù)用的,只要稍加修改就可以支持ios的build。另外我們現(xiàn)在做vr相關(guān)的app,大部分都是android版本,所以apk的管理比較實(shí)用。
VR開發(fā)或者unity相關(guān)交流可以郵件madcloudsong@qq.com
轉(zhuǎn)載請(qǐng)注明原文鏈接
http://www.itdecent.cn/p/b00f9b7fdb06