嗨,大家好,我是新发。
前几天我写了一篇文章,【游戏开发进阶】教你自制离线Maven仓库,实现Unity离线环境使用Gradle打包(Unity | Android | 谷歌 | Gradle),里面我提到了Unity
使用Jenkins
实现自动化打包,
不过那篇文章中我只是一笔带过,没有细说具体操作流程。今天,我就专门写一篇关于Unity
通过Jenkins
实现自动化打包的教程吧~
特别说明:
我的电脑系统环境是Windows 10
,所以下面的操作环境都是在Windows 10
系统下的。
相信很多人都知道Jenkins
,不过为了照顾萌新,我这里还是简单说下Jenkins
是什么。
Jenkins
官网:https://www.jenkins.io/
Jenkins
是一个开源软件项目,是基于Java
开发的一个持续集成工具(CI
),具有友好的操作界面,主要用于持续、自动的构建/测试软件项目、监控外部任务的运行。它可以在Tomcat
等流行的servlet
容器中运行,也可独立运行。通常与版本管理工具(SCM
)、构建工具结合使用。常用的版本控制工具有SVN
、GIT
,构建工具有Maven
、Ant
、Gradle
。
注:
什么是集成?
代码由编译、发布和测试、直到上线的一个过程。
什么是持续集成?
高效的、持续性的不断迭代代码的集成工作。
这样讲好像也不是很直观,没关系,它就是一个工具,我们学会使用它就好,下面我来一步步教大家如何使用Jenkins
。
因为Jenkins
是基于Java
开发的,要运行Jenkins
需要Java
环境,即JDK
,所以我们需要先安装下JDK
。
JDK
下载:https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html
根据你的系统环境选择对应的JDK
下载,
下载下来后双击即可执行安装,安装过程没什么,这里就不啰嗦了。
安装完毕后,配置一下JDK
的 环境变量。
最后在命令行中输入java -version
,如果能正常输出版本号,则说明JDK
环境弄好了。
进入Jenkins
官网:https://www.jenkins.io/
点击Download
,
根据你的系统和环境选择对应的安装包,因为我是Windows
系统,所以我下载Windows
版的安装包,
下载下来是一个msi
文件,
双击jenkins.msi
,执行安装,设置一下安装路径,
选择Run service as LocalSystem
(即使用本地系统账号)
设置端口号,比如我设置为8075
,然后点击Test Port
按钮测试一下端口有没有被占用,
确认端口没被占用后,点击Next
,
设置JDK
所在的路径,
继续Next
,
点击Install
开始安装,
注意,安装过程中可能会弹出360
提醒,选择允许即可。
完整完毕,
上面安装完毕后会自动启动Jenkins
服务,我们可以在任务管理器中看到一个Java
的进程,它就是Jenkins
的服务进程。
我们在浏览器中访问 http://localhost:8075
,此时会显示需要解锁Jenkins
,如下
我们找到这个initialAdminPassword
文件,使用文本编辑器打开它,
可以看到里面是一串密码,我们复制它,
回到浏览器页面中,在管理员密码栏中粘贴刚刚的密码,然后点击继续,
接下来是插件安装界面,因为Jenkins
插件的下载需要,所以如果你可以科学上网,则点击安装推荐的插件
,当然也可以先不安装插件,后续有需要再安装对应的插件即可,
如果是离线环境(比如内网环境),则点击跳过插件安装(下文我会教如何在离线环境下安装插件),
接着创建管理员账号,
完成,进入Jenkins
主页,
上面我们说到,在任务管理器中可以看到一个Java
进程,它就是Jenkins
的服务进程,
如果你直接暴力杀掉这个Java
进程,那么Jenkins
也就关闭了,不过不建议这么做。
以管理员身份运行命令net stop jenkins
,如下(我是使用管理员身份运行PowerShell
来执行命令的)
注意,如果你不是以管理员身份执行上面的命令,则会提示
发生系统错误 5
如下(普通账号权限下通过cmd
执行命令)
如何以管理员身份运行cmd?
进入cmd
所在目录:
C:\Users\linxinfa\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\System Tools
右键命令提示符
,点击以管理员身份运行
即可,
如果觉得麻烦的话,也可以直接在系统的开始菜单那里直接以管理员身份运行PowerShell
,
进入Jenkins
的安装目录,如下,
在地址栏输入cmd
,然后执行jenkins stop
,如下,与上面的效果是一样的,
以管理员身份执行命令net start jenkins
,如下
进入Jenkins
的安装目录,
执行命令jenkins start
,如下
如果想重启Jenkins
,则执行jenkins restart
,如下
先关闭Jenkins
,进入Jenkins
的安装目录,可以看到里面有一个jenkins.xml
,使用文本编辑器打开它,
把--httpPort
的端口改为别的,比如我改成8076
,
重新启动Jenkins
服务,在浏览器中使用新的端口进行测试,能够正常访问则说明端口修改成功了。
Jenkins
可能需要多人登录,我们可以新建一些账号供其他人登录。
在Jenkins
主页的左侧栏中点击Manage Jenkins
,
接着点击Manager Users
,
然后点击Create User
,
输入要创建的新账号的账号密码,点击创建即可,
创建成功,可以看到多了一个账号了,
我们可以退出当前账号,使用这个新账号登录,
登录成功,
点击账号的齿轮按钮,
修改Password
,点击Save
即可,
在Manage Jenkins
页面中,点击Manage Plugins
,
搜索需要的插件名称进行安装即可(需要能科学上网才行)
Jenkins CLI
就是Jenkins
的命令行工具,类似于MacOS
的终端。
我们可以在Jenkins
的Manage Jenkins
页面中看到Jenkins CLI
,点击进入,
点击jenkins-cli.jar
,
把下载下来的jenkins-cli.jar
放到Jenkins
安装目录中,
接着我们就可以通过命令来操作Jenkins
了,具体命令参数可以看Jenkins CLI
页面,
我们可以看到安装插件的参数是install-plugin
,
点击去可以看到具体的使用方法,
我们进入jenkins-cli.jar
所在的目录,通过下面的命令即可安装插件,(注意端口根据你的Jenkins
的实际端口号而定)
java -jar jenkins-cli.jar -s http://localhost:8075/ 插件名
如果不清楚插件名可以上Jenkins
的插件官网查看:https://plugins.jenkins.io/
以Maven Integration
插件为例,搜索Maven Integration
,点击搜索到的插件,
点击Releases
页面,即可看到,插件名就是maven-plugin:3.12
,
对应的插件安装命令就是:
java -jar jenkins-cli.jar -s http://localhost:8075/ maven-plugin:3.12
注意,你可能会提示
ERROR: anonymous is missing the Overall/Read permission
我们需要在Configure Global Security
中勾选项目矩阵授权策略
,给Anonymous
添加Administer
权限即可。
上面两种方式都需要联网,而我们有可能需要把Jenkins
部署在离线环境的电脑上(比如内网),这个时候就只能通过离线安装的方式了。
这个时候,我们需要先在有网络(能科学上网)的电脑上下载安装插件。
安装好的插件可以在这个目录中找到:
C:\Windows\System32\config\systemprofile\AppData\Local\Jenkins\.jenkins\plugins
将其拷贝到内网机的相同路径中,然后重启Jenkins
即可。
我以创建一个HelloWorld
任务为例来演示一下。
点击New Item
,
输入任务名,比如HelloWorld
,点击Freesytyle project
,点击OK
,
输入任务描述,
Build
选项选择Execute Windows batch command
(即批处理,也就是我们说的bat
)
然后在Command
中编写我们要执行的bat
命令,比如
echo "Hello World"
如下
最后点击保存,
这样我们的任务就创建成功了,我们可以点击Build Now
来执行这个任务,
按F5
刷新一下浏览器,可以看到任务执行的进度,
执行完后我们可以查看对应的日志,
从日志中我们可以看到我们输出的Hello World
,
有时候我们需要创建带参数的任务。
我们勾选This project is parameterized
,然后点击Add Parameter
,可以看到它提供了多种类型的参数,
我以选择项
参数为例,
分别填写参数名、选项(每个选项一行)、描述,
编写bat
命令,
点击Build with Parameters
,然后设置好参数,
最后点击Build
,
执行完毕可以看到输出日志,结果正确,
我们看到任务Build
中并没有Phython
的选项,
但我们又想要让Jenkins
可以执行Python
,怎么办呢?很简单,在bat
中call python
就好啦,
其中python
代码如下:
print("Hello, I am python")
有时候我们需要周期性地执行任务,比如每天8
点触发一次执行任务,或者每隔30
分钟触发一次执行任务。
在Build Triggers
(触发器)中勾选Build periodically
,
然后在Schedule
中编写规则。
格式:
MINUTE HOUR DOM MONTH DOW
字段 | 说明 | 取值范围 |
---|---|---|
MINUTE | 分钟 | 0~59 |
HOUR | 小时 | 0~23 |
DOM | 一个月中的第几天 | 1~31 |
MONTH | 月 | 1~12 |
DOW | 星期 | 0~7(0和7代表的都是周日) |
语法:
*
:匹配范围内所有值,例:* * * * *
M-N
:匹配M~N
范围内所有值,例:10-30 * * * *
M-N/X
:在指定M~N
范围内每隔X
构建一次,例:10-30/5 * * * *
*/X
:整个有效区间内每隔X
构建一次,例:*/30 * * * *
A,B,...,Z
:匹配多个值,例:10,20,30 * * * *
关于符号H:
为了在系统中生成定时任务,符号H
(代表Hash
,后面用散列
代替)应该用在可能用到的地方,例如:为十几个日常任务配置0 0 * * *
将会在午夜产生较大峰值。相比之下,配置H H * * *
仍将每天一次执行每个任务,不是都在同一时刻,可以更好的使用有限资源。
符号H
可用于范围,例如,H H(0-7) * * *
代表凌晨0:00
到 上午7:59
一段时间。
符号H
在一定范围内可被认为是一个随机值,但实际上它是任务名称的一个散列而不是随机函数。
案例:
每30分钟
构建一次
H/30 * * * *
每2小时
构建一次
H H/2 * * *
每天早上8点
构建一次
0 8 * * *
每天的8点
,12点
,22点
,一天构建3
次
0 8,12,22 * * *
每前半小时中每隔10分钟
构建一次
H(0-29)/10 * * * *
每个工作日从早上9点45分
开始到下午4点45分
结束这段时间内每间隔2小时
的45分钟
那一刻构建一次
45 9-16/2 * * 1-5
每月(除了12月
)从1号
到15号
这段时间内某刻构建一次
H H 1,15 1-11 *
好了,案例就列举这么多了。
现在,为了演示,我设置为每隔1
分钟执行一次,
命令如下,
可以看到它每分钟就触发执行一次任务,
下面我演示一下通过Jenkins
来调用Unity
打包Android
的APK
。
我先画个流程图,方便大家理解:
现在,我们开始吧。
点击 File / Build Settings
菜单,切换成Android
平台,
点击Edit / Preferences
,在External Tools
中设置好JDK
、Android SDK
、Gradle
,
在Player Settings
中设置一下包名,比如com.linxinfa.test
,
添加要打包的场景,手动点击Build
,测试一下是否能正常打出APK
,
可以正常打出APK
,说明打包环境设置都正确,
新建一个Editor
文件夹,
在Editor
文件夹中新建一个BuildTools
脚本,
BuildTools.cs
脚本代码如下:
using UnityEngine;
using UnityEditor;
public class BuildTools
{
[MenuItem("Build/Build APK")]
public static void BuildApk()
{
BuildPlayerOptions opt = new BuildPlayerOptions();
opt.scenes = new string[] { "Assets/Scenes/SampleScene.unity" };
opt.locationPathName = Application.dataPath + "/../Bin/test.apk";
opt.target = BuildTarget.Android;
opt.options = BuildOptions.None;
BuildPipeline.BuildPlayer(opt);
Debug.Log("Build App Done!");
}
}
点击菜单Build / Build Apk
,
可以正常打出APK
,
Unity
提供了命令行模式给开发者,我们可以写bat
脚本来调用Unity
中的静态函数,比如我们的打包函数。
格式:
Unity程序 -参数 -projectPath 工程地址 -executeMethod 静态函数
例:
"D:\software\Unity\2021.1.7f1c1\Editor\Unity.exe" ^
-quit ^
-batchmode ^
-projectPath "E:\UnityProject\UnityDemo" ^
-executeMethod BuildTools.BuildApk ^
-logFile "E:\UnityProject\UnityDemo\output.log"
注:为了阅读方便,命令我写成多行,在
bat
中连接多行的符号是^
我们可以在Unity
官方手册看到具体的命令参数说明:https://docs.unity3d.com/Manual/CommandLineArguments.html
-batchmode
在 批处理模式下运行Unity
,它不会弹出窗口。当脚本代码在执行过程中发生异常或其他操作失败时Unity
将立即退出,并返回代码为1
。
-quit
命令执行完毕后将退出Unity
编辑器。请注意,这可能会导致错误消息被隐藏(但他们将显示在Editor.log
文件)
-buildWindowsPlayer
构建一个32位
的Windows
平台的exe
(例如:-buildWindowsPlayer path/to/your/build.exe
)
-buildWindows64Player
构建一个64位
的Windows
平台的exe
(例如:-buildWindows64Player path/to/your/build.exe
)
-importPackage
导入一个的package
,不会显示导入对话框
-createProject
根据提供的路径建立一个空项目
-projectPath
打开指定路径的项目
-logFile
指定输出的日志文件
-nographics
当运行在批处理模式,不会初始化显卡设备,不需要GPU
参与;但如果你需要执行光照烘焙等操作,则不能使用这个参数,因为它需要GPU
运算。
-executeMethod
在Unity
启动的同时会执行静态方法。也就是说,使用executeMethod
我们需要在编辑文件夹有一个脚本并且类里有一个静态函数。
-single-instance
在同一时间只允许一个游戏实例运行。如果另一个实例已在运行,然后再次通过-single-instance
启动它的话会调节到现有的这个实例。
-nolog
不产生输出日志。 通常output_log.txt
被写在游戏输出目录下的*_Data
文件夹中。
我们知道,一个Unity
工程只能打开一个实例,所以如果我们已经手动用Unity
打开了工程,此时执行下面这个命令是会报错的,
"D:\software\Unity\2021.1.7f1c1\Editor\Unity.exe" ^
-quit ^
-batchmode ^
-projectPath "E:\UnityProject\UnityDemo" ^
-executeMethod BuildTools.BuildApk ^
报错如下:
Aborting batchmode due to fatal error:
It looks like another Unity instance is running with this project open.
Multiple Unity instances cannot open the same project.
我们需要先判断Unity
是否在运行中,如果是,则先将旧的Unity
实例进程杀掉,对应的bat
代码如下:
::判断Unity是否运行中
TASKLIST /V /S localhost /U %username%>tmp_process_list.txt
TYPE tmp_process_list.txt |FIND "Unity.exe"
IF ERRORLEVEL 0 (GOTO UNITY_IS_RUNNING)
ELSE (GOTO START_UNITY)
:UNITY_IS_RUNNING
::杀掉Unity
TASKKILL /F /IM Unity.exe
::停1秒
PING 127.0.0.1 -n 1 >NUL
GOTO START_UNITY
:START_UNITY
:: 此处执行Unity打包
另外,我们想要在执行打包时传入一些参数,比如APP名字
、版本号
等,可以在命令中加上,格式可以自定义,我们只需在后面的C#
代码中进行相应的解析即可,例:
--productName:%1 --version:%2
其中%1
表示参数1
,%2
表示参数2
,
完整命令如下:
"D:\software\Unity\2021.1.7f1c1\Editor\Unity.exe" ^
-quit ^
-batchmode ^
-projectPath "E:\UnityProject\UnityDemo" ^
-executeMethod BuildTools.BuildApk ^
--productName:%1 ^
--version:%2
整合上面的Unity
进程判断,最终完整的bat
代码如下:
::判断Unity是否运行中
TASKLIST /V /S localhost /U %username%>tmp_process_list.txt
TYPE tmp_process_list.txt |FIND "Unity.exe"
IF ERRORLEVEL 0 (GOTO UNITY_IS_RUNNING)
ELSE (GOTO START_UNITY)
:UNITY_IS_RUNNING
::杀掉Unity
TASKKILL /F /IM Unity.exe
::停1秒
PING 127.0.0.1 -n 1 >NUL
GOTO START_UNITY
:START_UNITY
:: 此处执行Unity打包
"D:\software\Unity\2021.1.7f1c1\Editor\Unity.exe" ^
-quit ^
-batchmode ^
-projectPath "E:\UnityProject\UnityDemo" ^
-executeMethod BuildTools.BuildApk ^
-logFile "E:\UnityProject\UnityDemo\output.log" ^
--productName:%1 ^
--version:%2
将上面的bat
代码保存为build_app.bat
,我们通过命令行去执行这个build_app.bat
,如下:
可以看到此时能打出APK
,
在输出的日志文件中我们也可以看到我们Debug.Log
输出的日志,
虽然我们上面的bat
脚本传递了包名和版本号两个参数,但是我们在Unity
的打包工具中并没有对这两个参数进行解析,现在,我们补上解析参数的逻辑吧。
// 解析命令行参数
string[] args = System.Environment.GetCommandLineArgs();
foreach (var s in args)
{
if (s.Contains("--productName:"))
{
string productName= s.Split(':')[1];
// 设置app名字
PlayerSettings.productName = productName;
}
if (s.Contains("--version:"))
{
string version = s.Split(':')[1];
// 设置版本号
PlayerSettings.bundleVersion = version;
}
}
打包工具完整代码如下:
// BuildTools.cs
using UnityEngine;
using UnityEditor;
public class BuildTools
{
[MenuItem("Build/Build APK")]
public static void BuildApk()
{
// 解析命令行参数
string[] args = System.Environment.GetCommandLineArgs();
foreach (var s in args)
{
if (s.Contains("--productName:"))
{
string productName= s.Split(':')[1];
// 设置app名字
PlayerSettings.productName = productName;
}
if (s.Contains("--version:"))
{
string version = s.Split(':')[1];
// 设置版本号
PlayerSettings.bundleVersion = version;
}
}
// 执行打包
BuildPlayerOptions opt = new BuildPlayerOptions();
opt.scenes = new string[] { "Assets/Scenes/SampleScene.unity" };
opt.locationPathName = Application.dataPath + "/../Bin/test.apk";
opt.target = BuildTarget.Android;
opt.options = BuildOptions.None;
BuildPipeline.BuildPlayer(opt);
Debug.Log("Build App Done!");
}
}
重新执行命令:
然后我们安装一下APK
,看看APP
名字是不是哈哈哈
,
在应用信息里可以看到版本号也是我们命令行中设置的1.2.0.0
,
我们回到Jenkins
页面中,创建一个带参数的任务,
appName
参数:
version
参数:
命令行:
E:\UnityProject\UnityDemo\bat\build_app.bat %appName% %version%
,如下:
执行Jenkins
任务,如下:
等等运行结果:
执行完毕,我们看下输出的日志,
可以看到我们的bat
脚本被正确执行了,参数也传递正确,
APK
也可以正常生成,
安装到模拟器上,可以看到名字正确,
版本号也正确,
流程走通了,剩下的就是根据自己的需求进行扩展啦,比如打包前先执行一下svn
更新之类的,需要额外参数,就在Jenkins
中添加,传递到bat
脚本中,再传递到Unity
中,最后根据参数进行打包。
我个人其实不是特别喜欢写bat
脚本,我更喜欢写pytho
,于是,我就写了个python
版的脚本,脚本中我加了监控Unity
日志输出的逻辑,方便进行一些判断,画个图:
python
完整代码如下:
import os
import sys
import time
# 设置你本地的Unity安装目录
unity_exe = 'D:/software/Unity/2021.1.7f1c1/Editor/Unity.exe'
# unity工程目录,当前脚本放在unity工程根目录中
project_path = 'E:/UnityProject/UnityDemo'
# 日志
log_file = os.getcwd() + '/unity_log.log'
static_func = 'BuildTools.BuildApk'
# 杀掉unity进程
def kill_unity():
os.system('taskkill /IM Unity.exe /F')
def clear_log():
if os.path.exists(log_file):
os.remove(log_file)
# 调用unity中我们封装的静态函数
def call_unity_static_func(func):
kill_unity()
time.sleep(1)
clear_log()
time.sleep(1)
cmd = 'start %s -quit -batchmode -projectPath %s -logFile %s -executeMethod %s --productName:%s --version:%s'%(unity_exe,project_path,log_file,func, sys.argv[1], sys.argv[2])
print('run cmd: ' + cmd)
os.system(cmd)
# 实时监测unity的log, 参数target_log是我们要监测的目标log, 如果检测到了, 则跳出while循环
def monitor_unity_log(target_log):
pos = 0
while True:
if os.path.exists(log_file):
break
else:
time.sleep(0.1)
while True:
fd = open(log_file, 'r', encoding='utf-8')
if 0 != pos:
fd.seek(pos, 0)
while True:
line = fd.readline()
pos = pos + len(line)
if target_log in line:
print(u'监测到unity输出了目标log: ' + target_log)
fd.close()
return
if line.strip():
print(line)
else:
break
fd.close()
if __name__ == '__main__':
call_unity_static_func(static_func)
monitor_unity_log('Build App Done!')
print('done')
我们把脚本保存为build_app.py
,
把Jenkins
中的命令改为执行python
脚本:
call python E:\UnityProject\UnityDemo\bat\build_app.py %appName% %version%
如下:
执行一下任务,
耐心等待执行结果,
执行完毕可以看到监控到的日志,
我们在python
中输出的日志都可以在Jenkins
的Console Output
中看到,
APK
顺利生成,Very Good
,完美~
好了,就写这么多吧,我是新发,喜欢我的可以点赞、关注,有任何技术上的疑问欢迎评论或留言~