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

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

unityvr

前言

持續(xù)集成的意義就不多說了。unity通常打包一般就直接build&run,但是在實際項目中,往往直接在服務(wù)器build包,所以命令行打包必不可少,這里一方面分享unity打包做持續(xù)集成,一方面分享使用unity管理多平臺打包,例如一個vrapp需要支持gear版本,支持小米版本,支持cardboard版本等等~懂的人就知道這里具有一定的管理維護(hù)成本。

我們做vr相關(guān)的app,需要支持gear、cardboard、小米、vivo、大朋、暴風(fēng)、Idealens、pico、nibiru、酷開等一大堆平臺,曾經(jīng)還有l(wèi)g和htcvive、oculus平臺,未來還會有更多的平臺,所以關(guān)于unity項目的多平臺管理是很重要的,在這方面我們也在探索,積累了一點經(jīng)驗。這里介紹的主要是基于unity中c#寫的打包和多平臺管理,如果將其中一部分功能使用python和其他配置文件來實現(xiàn)也是可以的,只是用c#直接做會方便許多。

jenkins

jenkins

https://jenkins.io/index.html

jenkins不用多說,懂的人都了解是干什么的,來源于hudson,可以比較容易的搭起一個持續(xù)集成服務(wù)器,支持svn和git等版本管理。支持bash,所以可以用bash、python等大部分腳本來寫打包腳本和前后的處理。

unity命令行打包

unity如何進(jìn)行命令行打包呢,其實unity是支持以命令行方式啟動的,但是需要關(guān)閉editor支持執(zhí)行命令,如下:

${unity可執(zhí)行文件路徑} -projectPath ${項目路徑} 
-executeMethod CloudBuild.PerformBuildAndroidCloudAlphaRelease 
-batchmode -quit -logFile ${放log的路徑} 
-ForceExitEditor

整體比較容易理解,其中CloudBuild.PerformBuildAndroidCloudAlphaRelease是一個類的靜態(tài)方法,然后在這個方法中寫打包相關(guān)邏輯即可。

EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTarget.Android);
string[] scenes = { "Assets/Scenes/Init.unity", "Assets/Scenes/Main.unity" };
BuildPipeline.BuildPlayer(scenes, buildpath, BuildTarget.Android, BuildOptions.None);

核心邏輯就這么多,就會開始打一個android包并且生成到buildpath下面

多平臺打包管理

準(zhǔn)備工作

對unity插件不熟悉的可以看下 開發(fā)unity插件——一次搞定unity編輯器常用功能

我這里準(zhǔn)備了一個全局的配置文件,當(dāng)然這個可以使用外部配置文件來管理配置,是一個道理的。這里定義了兩個平臺版本一個是alpha一個是beta,使用宏來區(qū)分不同平臺的版本號。

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是編輯器工具,包含菜單項和打包功能,兩個平臺分別依賴不同的so文件和manifest文件。

dir

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

menu

manifest管理

寫個簡單的腳本進(jìn)行manifest替換就好,對于alpha平臺和beta平臺各有自己的manifest文件,在切換平臺的時候?qū)?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();//因為修改了manifest文件,所以刷新unity的assets
}

依賴包管理

為什么要做依賴包管理呢?因為在使用不同平臺sdk的時候,可能會引入很多sdk,每個sdk里包含自己的so、jar、aar包等,如果什么都不管理,直接打包的話,那么這些依賴的文件都會打進(jìn)所有的apk包,簡單來說就會增加包的體積,更嚴(yán)重的情況下,這些不同平臺sdk里的依賴庫可能還會有沖突,如果打進(jìn)同一個apk包,后果不堪設(shè)想~

做依賴包管理主要依賴unity自己的assetimport管理如下圖,那么只要在需要的時候勾選不需要的時候取消勾選就好了,我們要做的就是用代碼來自動實現(xiàn)這個功能。

assetsimport

先準(zhǔn)備好各個平臺的依賴包路徑

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",
};

然后在切換不同平臺的時候?qū)@些依賴包的import做處理,這塊Asset屬于plugin,所以使用pluginimporter來管理勾選的問題。

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);
    }
}

非常簡單一看就可以懂,然后試一下就明白了。

平臺切換

平臺切換功能主要是在editor里調(diào)試各個平臺功能的時候使用的菜單項,功能也很簡單,就做了下面幾件事情

  • 切平臺和宏定義
  • 切playersetting參數(shù)
  • 切buildscene配置
  • 切manifest和依賴包
  • 保存及打開對應(yīng)平臺的場景(如果場景不是復(fù)用的)

代碼示例如下,因為我們做vr相關(guān)的app,所以在gear平臺時vrsupport為true,其他平臺時為false,如果不同平臺的scene不一樣,那么在最后問用戶是否保存當(dāng)前場景,然后打開對應(yīng)平臺的場景。

/// <summary>
/// 切換alpha平臺
/// </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)
    };//場景
    UseAndroidManifest("Alpha");
    ChangePluginToAlpha();
    EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
    EditorSceneManager.OpenScene("Assets/Scenes/Main.unity");
}

打包

那么最后打包腳本如下,粗看信息量可能比較大,實際只做了幾件事情

  • 準(zhǔn)備好要打包的scene
  • 將當(dāng)前editor的狀態(tài)保存一下,以便打完包恢復(fù),這是為了開發(fā)使用方便而已,否則在開發(fā)機(jī)上打個包就發(fā)現(xiàn)editor的很多屬性變了有時很尷尬
  • 處理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平臺
    /// </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)
        };//場景
        UseAndroidManifest("Alpha");
        ChangePluginToAlpha();
        EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
        EditorSceneManager.OpenScene("Assets/Scenes/Main.unity");
    }

    /// <summary>
    /// 切換beta平臺
    /// </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)
        };//場景
        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包的過程,ios的build過程會稍微長一些,要先build好xcode project然后再通過xcode的命令行去打包,所以前半部分與android是可以復(fù)用的,只要稍加修改就可以支持ios的build。另外我們現(xiàn)在做vr相關(guān)的app,大部分都是android版本,所以apk的管理比較實用。

VR開發(fā)或者unity相關(guān)交流可以郵件madcloudsong@qq.com
轉(zhuǎn)載請注明原文鏈接
http://www.lxweimin.com/p/b00f9b7fdb06

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內(nèi)容