游戏项目中的全自动打包机制

🗨️字数统计=1.3k字 ⏳阅读时长≈5分钟

项目中的全自动打包机制

大部分成熟的线上项目,在项目成形期都会有一套完整的自动打包流程,用于解放人力(不用让所有人都了解打包机制,使打包过程透明)。

实际上,先后经历了多个线上项目,每个项目的自动打包流程其实大同小异。本质上都是通过jenkins运行带参的bat或者shell脚本来完成自动化打包流程的。

商业项目还会分渠道打包,打出相应的渠道包。在Assets文件夹外部创建一个文件夹来放所有需要对接的平台相关资源,各个平台资源对应着不同的目录。

由此,抽象出了一整套自动化打包流程。

前期准备工作

1、各个渠道的sdk资源按渠道分类至各个渠道文件夹中,包括这个渠道打包所需要的【AndroidManife.xml、res、so、jar包、java代码】(Android)或【framework、.a文件、InfoPlisst、蕴含生命周期的UnityAppController.mm/AppDelegate.m文件】(ios)等资源文件。

2、一台单独打包机器(资金充足的话建议单独配打包机器),建议用MAC机,因为ios包只能用MAC机来打,而MAC机还能打安卓apk。

3、Jenkins,一个开源的可拓展的自动化服务器,关于Jenkins可以单独拿出来将一篇文章了,再此简单的说一下Jenkins的持续集成流程:提交代码–>拉取代码–>编译–>打包–>测试–>反馈问题–>开发处理–>提交代码,从这一流程就可以窥探到Jenkins的便利。

4、打包所用的脚本:build.bat或者build.sh脚本。

5、unity项目代码中可供打包脚本调用的打包相关静态方法。

具体流程

1、Jenkins中Build with Parameters里输入相应的打包参数,坐等打包完成,打包过程对你来说是透明的,完毕…哈哈,开个玩笑,接下用最通俗的语言带你揭秘打包流程。

2、全自动化打包流程最重要的在于打包脚本,接收到你的输入参数后开始工作,首先覆盖平台相关资源,其次拉取svn到指定版本,然后再将将指定平台写入代码中,最后关闭打包机上unity和资源管理器窗口。关闭unity尤为重要,不然unity会以进程被占用为由告诉你打包失败。

3、设置Unity宏定义,表明这个包是某个渠道的宏定义。

4、设置游戏版本号,便于后期维护发热更新。

5、构建游戏Aesstbundle资源:调用打包代码,构建资源输出到指定目录,将资源打成压缩包输出到resource目录下。

6、构建游戏安装包。

相关代码

build.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
#define var

UnityCmd=/Applications/Unity/Unity.app/Contents/MacOS/Unity
CurrentPath=$(cd `(dirname $0)`; pwd)
PROJECTHOME=${CurrentPath}/../
FUNCTION_NAME=""
BUILDHOME=""
APP_NAME=""

cmdBuildAB="${UnityCmd} -quit -batchmode -projectPath ${PROJECTHOME} -executeMethod BuildEditor.BuildAndroidAB"
$cmdBuildAB

cmd="${UnityCmd} -quit -batchmode -projectPath ${PROJECTHOME} -executeMethod AutoBuild.${FUNCTION_NAME} -outPath ${BUILDHOME}/${APP_NAME} -logFile"
$cmd

C#静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//打AssetBundle
//state 0:apk 1: ios: 2: editor 3:iOS不压缩ab 4:不挪动UI 5:GOOGLE OBB
public static void BuildAssetBundle(int state)
{
DirectoryInfo direc = new DirectoryInfo(Application.streamingAssetsPath + "/AssetsBundle");
if (direc.Exists)
{
direc.Delete(true);
}

m_abDataLs.list.Clear();

m_options = BuildAssetBundleOptions.CollectDependencies;
if(state == 0 || state == 10)
{
m_options = BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.UncompressedAssetBundle;
m_target = BuildTarget.Android;
}
else
{
m_options = BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.UncompressedAssetBundle;
if(state == 1 || state == 3)
{
m_target = BuildTarget.iPhone;
}
else if(state == 2)
{
m_target = BuildTarget.StandaloneWindows;
}
}
if (state == 0 || state == 1)
{
//移动打包资源
AutoBuild.RemoveRessToTmp(AutoBuild.SourcePrefabFileName, AutoBuild.DirecPrefabFileName);
AutoBuild.MoveUIResToTemp();
}

if (state == 5)
{
//移除一部分资源去打obb包
AutoBuild.RemoveRessToTmp(AutoBuild.SourcePrefabFileNameObb, AutoBuild.DirecPrefabFileNameObb);
}

if (state == 3 || state == 4)
{
// 只移动Resource/Prefab
AutoBuild.RemoveRessToTmp(AutoBuild.SourcePrefabFileName, AutoBuild.DirecPrefabFileName);
}

//打AssetBundle
BuildAB(state);


if (state != 3)
{
//压缩AssetBundle
AssetBundleCompress();
}
}

//打apk
public static void Build_Apk(int XXqudao)
{
PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Android, "PLAT_SDK_XXqudao");
PlayerSettings.bundleIdentifier = "com.xxx.xxx.xxx";
BuildAndroid(XXqudao);
}

//移动相关的资源
static void BuildAndroid(int XXqudao)
{
//移动相关平台sdk资源文件夹
MovePluginDir(XXqudao);
//替换一些DLL文件
ReplaceDll("Android", "DLL");
//替换一些包体内需要用到的图片icon文件
ReplaceLoadingImage(XXqudao);
//开始build
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
buildPlayerOptions.scenes = new[] { "Assets/Scene1.unity", "Assets/Scene2.unity" };
buildPlayerOptions.locationPathName = m_outPath;
buildPlayerOptions.target = BuildTarget.Android;
buildPlayerOptions.options = BuildOptions.None;

BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);
BuildSummary summary = report.summary;

if (summary.result == BuildResult.Succeeded)
{
Debug.Log("Build succeeded: " + summary.totalSize + " bytes");
}

if (summary.result == BuildResult.Failed)
{
Debug.Log("Build failed");
}
//将editor使用的DLL恢复
RecoveryDll("DLL");
//恢复sdk资源文件夹
RecoveryPluginDir();
}

写在最后

这里只是提供一个大体的思路,实际上在自动化流程中还是有很多大大小小的坎坷的,比如一些已经成形的项目,其项目目录结构需要单独进行适配,一些渠道打包需要单独进行调整等,这些都是要考虑到的。

关于sdk:建议单独分出一个集成sdk的部门,用于将各个渠道的sdk集成到一个主的sdk上,游戏项目只需要接入一个sdk即可,使得sdk接入工作透明化,游戏开发人员专注于游戏业务逻辑的开发,更加高效。

分享到