Unity项目CI/CD自动化构建实战:基于Jenkins的流水线搭建指南

📅 2026/7/23 6:52:00 👁️ 阅读次数 📝 编程学习
Unity项目CI/CD自动化构建实战:基于Jenkins的流水线搭建指南

1. 项目概述:为什么我们需要为Unity工程搭建CI/CD流水线?

如果你是一个Unity开发者,或者是一个小型游戏工作室的技术负责人,你大概率经历过这样的场景:美术同学更新了一个模型,程序同学修复了一个Bug,策划同学调整了一个数值表,然后大家围在一台“构建机”旁边,等着某位同事手动点击Unity编辑器上的“Build”按钮。这个过程可能持续十几分钟到几个小时,期间不能断电、不能断网、不能有任何意外,否则就得重来。更头疼的是,当项目需要同时构建Android、iOS、Windows、WebGL等多个平台时,手动操作的复杂度和出错率会呈指数级上升。这不仅仅是效率问题,更是团队协作和项目质量的巨大隐患。

“持续化编译部署”,或者说CI/CD(持续集成/持续交付),就是为了根治这个痛点。它的核心思想是,将代码提交到版本库(如Git)这个动作,作为触发一系列自动化流程的开关。这个流程会自动拉取最新代码、解决依赖、执行编译、运行测试、打包成品,并最终部署到测试环境或分发渠道。对于Unity项目而言,这意味着美术、程序、策划的任何提交,都能在几分钟到几小时内得到一个可运行的、跨平台的测试包,供团队快速验证。Jenkins,作为一款开源的、功能强大的自动化服务器,正是搭建这条流水线的绝佳工具。它就像一个不知疲倦的、严格按照指令行事的构建机器人,7x24小时待命。

我经历过从纯手动构建到搭建自动化流水线的全过程,实话说,初期搭建会花一些功夫,但一旦跑通,带来的解放感和质量提升是颠覆性的。你再也不用担心构建环境不一致导致的“在我机器上是好的”这种问题,因为构建环境被固化在了脚本和配置里。你也可以在每天凌晨自动为项目生成一个“每日构建”(Nightly Build),让测试同学一早就能拿到最新版本。接下来,我就把搭建这套系统的核心思路、实操步骤以及我踩过的那些坑,毫无保留地分享给你。

2. 核心架构与工具选型解析

在动手之前,我们需要理清整个流水线的核心组件和它们之间的协作关系。一个典型的Unity+Jenkins CI/CD流水线通常包含以下几个部分:

  1. 版本控制系统:这是一切的源头,通常是Git(如GitLab、GitHub、Gitee或自建Git服务器)。所有代码、资源、配置的变更都通过提交(Commit)和推送(Push)到这里。
  2. CI/CD服务器:也就是Jenkins。它负责监听版本库的变更(如Git的Webhook),触发构建任务,并在指定的“构建代理”上执行我们编写好的构建脚本。
  3. 构建代理:实际执行Unity编译命令的机器。它可以是Jenkins服务器本身,也可以是另一台专门用于构建的、性能更强的机器(Windows、macOS或Linux)。关键点在于,这台机器上必须安装好指定版本的Unity Editor(无图形界面模式)以及必要的SDK(如Android SDK/NDK、Xcode)。
  4. 构建脚本:自动化流程的灵魂。这是一系列用命令行驱动Unity和后续处理步骤的脚本,可以用C#、Shell、Batch或PowerShell编写。Unity官方提供的Unity.exe -batchmode -quit -projectPath ... -executeMethod ...命令行接口是核心。
  5. 制品仓库:存放构建产物的地方,比如打包好的APK、IPA、EXE文件。Jenkins本身可以暂存,但更专业的做法是上传到像Nexus、Artifactory这样的制品库,或者简单的网络共享目录、云存储。

2.1 为什么选择Jenkins?

市面上CI/CD工具很多,GitLab CI、GitHub Actions、TeamCity等都很优秀。选择Jenkins的主要原因在于其无与伦比的灵活性和强大的生态系统。它是开源的,拥有上千个插件,几乎可以和任何工具集成。对于Unity这种构建过程相对复杂、定制化需求高的场景,Jenkins通过编写Pipeline脚本(一种基于Groovy的DSL)可以实现极其精细的控制。你可以轻松地串并联构建步骤,在不同的操作系统代理上执行任务,并且它的历史记录、控制台输出查看、构建趋势图等功能都非常成熟。对于中小团队或需要高度自定义流程的项目,Jenkins的学习成本和可控性平衡得最好。

2.2 Unity侧的准备工作:Editor脚本与项目设置

自动化构建的核心,是让Unity在无人工干预的情况下执行编译。这依赖于我们提前编写好的Editor脚本。你需要在项目的Assets/Editor目录下(如果没有就创建一个)创建一个C#脚本,例如BuildScript.cs

这个脚本里需要包含一个静态方法,供命令行调用。这个方法里要完成所有构建准备工作:场景列表配置、定义输出路径、设置应用标识和版本号、处理不同平台的构建设置(如Android的Keystore、iOS的Team ID)等。

一个最基础的构建方法骨架如下:

using UnityEditor; using System.Collections.Generic; public static class BuildScript { public static void PerformBuild() { // 1. 定义要打包的场景 List<string> scenes = new List<string>(); foreach (var scene in EditorBuildSettings.scenes) { if (scene.enabled) scenes.Add(scene.path); } // 2. 定义输出目录(可以从命令行参数获取,更灵活) string outputPath = "./Builds/" + EditorUserBuildSettings.activeBuildTarget; System.IO.Directory.CreateDirectory(outputPath); // 3. 执行构建 BuildPipeline.BuildPlayer(scenes.ToArray(), outputPath + "/MyGame.exe", EditorUserBuildSettings.activeBuildTarget, BuildOptions.None); } }

注意:在实际项目中,版本号管理、Keystore密码等敏感信息绝对不要硬编码在脚本里。应该通过命令行参数、环境变量或配置文件传入,Jenkins的“Credentials”功能可以安全地管理这些密钥。

3. Jenkins服务部署与环境准备

Jenkins的安装方式很多,这里我推荐使用Docker方式安装,这是最干净、最易于管理和迁移的方式。假设我们的构建服务器是一台Linux机器(如Ubuntu 20.04)。

3.1 使用Docker安装和运行Jenkins

首先,确保服务器上已经安装了Docker和Docker Compose。

  1. 创建一个用于持久化Jenkins数据的目录:

    sudo mkdir -p /var/jenkins_home sudo chown 1000:1000 /var/jenkins_home # Jenkins容器内用户UID通常是1000
  2. 使用Docker命令直接运行(最简单的方式):

    docker run -d \ --name jenkins \ -p 8080:8080 -p 50000:50000 \ -v /var/jenkins_home:/var/jenkins_home \ -v /var/run/docker.sock:/var/run/docker.sock \ jenkins/jenkins:lts-jdk11

    这条命令做了几件事:后台运行、命名容器、将宿主机的8080和50000端口映射给Jenkins、将宿主机目录挂载为Jenkins数据卷(这样数据不会随容器消失)、挂载Docker套接字(方便Jenkins在容器内调用宿主机的Docker引擎,用于运行其他容器化构建环境)。

  3. 查看初始密码并登录:

    docker logs jenkins

    在日志中寻找类似Please use the following password to proceed to installation:的信息,复制密码。然后在浏览器访问http://你的服务器IP:8080,输入密码,完成初始插件安装向导。我建议在向导中选择“安装推荐的插件”。

3.2 初始配置与必要插件安装

安装完推荐插件后,进入Jenkins主界面,我们还需要安装几个对Unity构建至关重要的插件:

  1. Git plugin: 通常已默认安装,用于从Git仓库拉取代码。
  2. Pipeline: 用于支持最强大的“Pipeline”任务类型,我们将用编写Jenkinsfile的方式定义流水线。
  3. Credentials Binding Plugin: 安全地绑定密码、密钥等凭证到构建环境变量中。
  4. Workspace Cleanup Plugin: 构建前后清理工作空间,避免残留文件干扰。

进入“系统管理” -> “插件管理” -> “可选插件”,搜索并安装上述插件。

接下来,配置全局工具。进入“系统管理” -> “全局工具配置”:

  • Git: 如果你的服务器上已安装Git,可以指定Path to Git executable(如/usr/bin/git)。也可以选择让Jenkins自动安装。
  • JDK: Jenkins本身需要Java,LTS镜像已自带,通常无需额外配置。

3.3 构建代理节点配置(关键)

如果你的Jenkins服务器本身性能足够,且与Unity构建环境一致(比如都是Windows),可以直接在Jenkins服务器(即Built-In Node)上执行构建。但更常见的做法是,使用专门的、安装了Unity的机器作为构建代理。

配置Windows构建代理(物理机/虚拟机):

  1. 在Jenkins主界面,进入“系统管理” -> “节点管理” -> “新建节点”。
  2. 输入节点名称(如Unity-Windows-Builder),选择“固定节点”。
  3. 配置节点:
    • 执行器数量:根据CPU核心数设置,例如4。
    • 远程工作目录:指定一个代理机上的路径,如C:\Jenkins\Workspace
    • 标签:非常重要!填写windows unity。这样我们可以在Pipeline脚本中指定agent { label 'windows unity' }来让任务在这个节点运行。
    • 用法:选择“只允许运行绑定到这台机器的Job”。
    • 启动方式:对于Windows,通常选择“Launch agent via Java Web Start”。你需要下载agent.jar,并在代理机上运行一个命令。更稳定的一种方式是在Windows代理机上以服务方式运行Jenkins代理,这需要额外的配置步骤。

实操心得:在Windows上配置Jenkins代理,权限和网络问题是最常见的坑。确保代理机防火墙允许与Jenkins主机的通信,并且运行代理服务的账户有足够权限访问Unity安装目录和工作目录。我强烈建议先在代理机上用命令行测试Unity的批处理模式是否能正常工作,例如"C:\Program Files\Unity\Hub\Editor\2022.3.20f1\Editor\Unity.exe" -batchmode -quit -logFile - -projectPath C:\TestProject -executeMethod BuildScript.PerformBuild

4. 编写Jenkins Pipeline脚本实现自动化构建

Pipeline是Jenkins的灵魂。我们将构建流程定义在一个名为Jenkinsfile的文本文件中,并提交到项目Git仓库的根目录。这样,构建流程就和代码一样被版本管理起来。

下面是一个针对Unity多平台构建的Jenkinsfile示例,它包含了构建Android和Windows平台的核心步骤,并演示了如何处理版本号和敏感信息。

pipeline { agent { // 使用标签选择在配置了Unity的Windows代理上运行 label 'windows unity' } environment { // 从Jenkins凭证库中读取Unity的激活许可证文件内容 UNITY_LICENSE = credentials('unity-license-file') // 定义项目路径(相对于工作空间) UNITY_PROJECT_PATH = './MyUnityProject' // 从参数或环境变量获取版本号,默认使用构建号 BUILD_VERSION = "${env.BUILD_NUMBER}" } parameters { // 构建时可以选择平台 choice(name: 'BUILD_TARGET', choices: ['Android', 'Windows', 'iOS'], description: '选择构建目标平台') string(name: 'CUSTOM_VERSION', defaultValue: '', description: '自定义版本号(可选)') } stages { stage('检出代码') { steps { checkout scm // 拉取触发本次构建的Git代码 } } stage('写入Unity许可证') { steps { // 将许可证文件写入到Unity通用的许可目录 bat ''' set UNITY_LICENSE_PATH=%LOCALAPPDATA%\Unity if not exist "%UNITY_LICENSE_PATH%" mkdir "%UNITY_LICENSE_PATH%" echo %UNITY_LICENSE% > "%UNITY_LICENSE_PATH%\Unity_lic.ulf" ''' } } stage('设置构建版本') { steps { script { // 如果提供了自定义版本号,则优先使用 if (params.CUSTOM_VERSION?.trim()) { env.BUILD_VERSION = params.CUSTOM_VERSION } echo "当前构建版本号: ${env.BUILD_VERSION}" } } } stage('Unity构建') { steps { script { // 根据选择的平台,调用不同的构建方法 def unityExe = "C:\\Program Files\\Unity\\Hub\\Editor\\2022.3.20f1\\Editor\\Unity.exe" def buildMethod = "" def outputSubDir = "" switch(params.BUILD_TARGET) { case 'Android': buildMethod = "BuildScript.PerformAndroidBuild" outputSubDir = "Android" break case 'Windows': buildMethod = "BuildScript.PerformWindowsBuild" outputSubDir = "Windows" break // iOS构建通常需要在macOS代理上完成,这里只是示例结构 case 'iOS': buildMethod = "BuildScript.PerformiOSBuild" outputSubDir = "iOS" echo "iOS构建需要macOS代理,此Pipeline仅作示例。" currentBuild.result = 'ABORTED' return } // 执行Unity批处理模式构建 bat """ "${unityExe}" -batchmode -quit -nographics ^ -projectPath "${WORKSPACE}\\${UNITY_PROJECT_PATH}" ^ -executeMethod ${buildMethod} ^ -buildVersion ${env.BUILD_VERSION} ^ -logFile "${WORKSPACE}\\unity_build.log" """ } } post { // 无论成功失败,都存档Unity的详细日志,便于排查 always { archiveArtifacts artifacts: 'unity_build.log', allowEmptyArchive: true } } } stage('处理构建产物') { steps { script { def outputDir = "${UNITY_PROJECT_PATH}/Builds/${params.BUILD_TARGET}" // 将构建产物(如APK)复制到工作空间根目录,方便Jenkins存档 bat """ if exist "${outputDir}" ( xcopy /E /I /Y "${outputDir}\\*.*" "${WORKSPACE}\\artifacts\\" ) """ } } } stage('存档与通知') { steps { // 存档构建产物 archiveArtifacts artifacts: 'artifacts/**/*', fingerprint: true // 这里可以集成邮件、钉钉、企业微信等通知插件,发送构建结果 } } } post { // 构建后操作,例如失败时发送警报 failure { echo '构建失败!请检查Unity日志。' // emailext ... (发送邮件通知的配置) } success { echo '构建成功!' } } }

对应的Unity构建脚本(BuildScript.cs)需要增强,以接收命令行参数并处理不同平台:

using UnityEditor; using System.Collections.Generic; using System.Linq; public static class BuildScript { // 从命令行参数获取版本号 private static string GetBuildVersion() { var args = System.Environment.GetCommandLineArgs(); for (int i = 0; i < args.Length; i++) { if (args[i] == "-buildVersion" && i + 1 < args.Length) { return args[i + 1]; } } return "1.0.0"; // 默认版本 } [MenuItem("Build/Windows")] public static void PerformWindowsBuild() { BuildPlayer(BuildTarget.StandaloneWindows64, ".exe"); } [MenuItem("Build/Android")] public static void PerformAndroidBuild() { // 在构建前进行Android特定设置 PlayerSettings.Android.keystoreName = "./keystore/user.keystore"; // 路径应从安全渠道获取 PlayerSettings.Android.keystorePass = "你的密码"; // 警告:应从环境变量或命令行传入! PlayerSettings.Android.keyaliasName = "你的别名"; PlayerSettings.Android.keyaliasPass = "你的别名密码"; PlayerSettings.Android.bundleVersionCode += 1; // 自动递增版本号 BuildPlayer(BuildTarget.Android, ".apk"); } private static void BuildPlayer(BuildTarget target, string extension) { string version = GetBuildVersion(); PlayerSettings.bundleVersion = version; // 设置包版本 List<string> scenes = EditorBuildSettings.scenes.Where(s => s.enabled).Select(s => s.path).ToList(); string outputPath = $"Builds/{target}/{PlayerSettings.productName}_{version}{extension}"; BuildPipeline.BuildPlayer(scenes.ToArray(), outputPath, target, BuildOptions.None); } }

5. 高级配置与优化实践

基础流水线跑通后,我们可以考虑以下优化来提升效率、安全性和可靠性。

5.1 使用Docker容器作为统一的构建环境

手动在代理机上安装和配置Unity非常繁琐,且难以保证环境一致性。使用Docker镜像可以完美解决这个问题。Unity官方提供了用于CI的Docker镜像(如unityci/editor),它包含了指定版本的Unity Editor和基础组件。

你可以在Jenkins Pipeline中,使用docker代理,指定一个包含Unity的镜像来运行构建步骤。这样,构建环境完全由镜像定义,与宿主机无关,实现了真正的环境标准化。

pipeline { agent none // 不在全局指定代理 stages { stage('构建Windows版本') { agent { docker { image 'unityci/editor:2022.3.20f1-windows-mono-1' args '-v /path/to/unity/license:/root/.local/share/unity3d/Unity/ -v /path/to/cache:/root/.cache/unity3d' } } steps { // 在容器内执行Unity构建命令 bat 'Unity.exe -batchmode -quit -projectPath ... -executeMethod ...' } } } }

注意事项:使用Docker构建Unity项目,尤其是需要图形编译(如烘焙光照图)或访问特定硬件时,可能会遇到权限和驱动问题。对于纯代码编译和简单资源打包,这是最佳实践。此外,镜像体积很大(几十GB),需要良好的网络和存储空间。

5.2 实现增量构建与缓存优化

Unity项目动辄几十GB,每次全量拉取和构建非常耗时。可以通过以下策略优化:

  • Git浅克隆:在Jenkins的checkout步骤中,可以配置只拉取最近几次提交,减少数据量。
    checkout([ $class: 'GitSCM', branches: [[name: '*/main']], extensions: [[$class: 'CloneOption', depth: 1, shallow: true]], userRemoteConfigs: [[url: 'https://your.git.repo']] ])
  • Library缓存:Unity项目的Library文件夹是编译缓存,占空间最大。可以尝试在构建完成后,将Library文件夹压缩并上传到服务器存储。下次构建时,先下载并解压缓存,再进行构建。这可以借助Jenkins的stash/unstash步骤或外部存储(如S3)实现。但要注意,不同Unity版本或项目重大变更后,缓存可能需要清理。
  • 分包构建:如果项目使用了Addressables或AssetBundles,可以将资源打包与代码编译分离,实现真正的增量资源更新。

5.3 集成自动化测试与质量门禁

CI不仅仅是构建,更重要的是集成。可以在构建流水线中加入自动化测试阶段。

  1. 单元测试:Unity支持通过命令行运行在Edit Mode和Play Mode下的单元测试(使用NUnit)。在Pipeline中添加一个阶段:
    stage('运行单元测试') { steps { bat """ Unity.exe -batchmode -quit -projectPath ... -runTests -testResults "${WORKSPACE}\\test-results.xml" -testPlatform editmode """ } post { always { // 解析并发布测试报告,例如使用JUnit插件 junit '**/test-results.xml' } } }
  2. 静态代码分析:集成Roslyn Analyzers或Unity的Microsoft.CodeAnalysis进行代码规范检查。
  3. 构建后测试:对于打出的包,可以编写简单的自动化脚本(如使用Appium、AltTester等)进行安装和冒烟测试,确保基本功能可运行。

6. 常见问题排查与实战心得

搭建过程中,你肯定会遇到各种问题。这里记录了几个最典型的问题和解决方法。

6.1 Unity批处理模式构建失败

这是最常见的问题。请按以下顺序排查:

  1. 检查日志:这是最重要的!构建命令中一定要加上-logFile参数(如-logFile build.log),构建失败后第一时间查看日志文件。错误信息通常非常明确。
  2. 许可证问题:无图形界面模式下,Unity需要一个有效的许可证。确保已通过-manualLicenseFile参数指定了许可证文件,或者已将许可证文件放置在正确的位置(Windows:%LOCALAPPDATA%\Unity\Unity_lic.ulf; macOS:~/Library/Unity/Unity_lic.ulf; Linux:~/.local/share/unity3d/Unity/Unity_lic.ulf)。可以通过Unity.exe -batchmode -quit -logFile - -manualLicenseFile license.ulf来激活。
  3. 编译错误:如果项目代码有错误,批处理模式会直接失败。确保在提交触发自动构建前,在本地编辑器里编译通过。
  4. 路径或权限问题:确保Unity执行路径、项目路径、输出路径都存在且Jenkins代理进程有读写权限。路径中避免使用中文和特殊字符。

6.2 Jenkins代理连接不稳定或构建卡住

  1. 检查网络和防火墙:确保Jenkins主机与代理机之间的TCP端口(默认50000)通信畅通。
  2. 检查代理机资源:构建Unity项目非常消耗CPU和内存。监控代理机资源使用情况,避免因资源耗尽导致进程假死。可以在Jenkins节点配置中减少“执行器数量”。
  3. 查看代理日志:在代理机的Jenkins代理程序运行窗口中,或查看其日志文件(通常在代理工作目录下),寻找错误信息。
  4. 使用“流水线步骤查看器”:Jenkins的Pipeline任务提供了图形化的步骤查看器,可以清晰看到流水线执行到哪一步卡住了,方便定位。

6.3 构建产物版本号管理混乱

手动管理版本号容易出错。我推荐的实践是:

  • 主版本号:在项目配置或单独的文件中定义。
  • 次版本号和修订号:使用Jenkins的BUILD_NUMBER环境变量自动生成。例如,可以在Pipeline中组合成1.0.${env.BUILD_NUMBER}
  • 提交哈希:将Git的短提交哈希(${env.GIT_COMMIT.substring(0, 7)})作为版本信息的一部分,打包到应用内(如设置界面中),便于精准定位代码版本。

6.4 安全地管理签名密钥与密码

绝对不要将Keystore密码、API密钥等硬编码在脚本或项目文件中。Jenkins的“凭证管理”功能是为此而生的。

  1. 在Jenkins后台,“管理Jenkins” -> “管理凭证” -> “全局凭证”中,添加一个类型为“Secret file”或“Secret text”的凭证。
  2. 在Pipeline中,使用withCredentials绑定凭证到环境变量或文件。
    stage('Android签名') { environment { KEYSTORE_PASSWORD = credentials('android-keystore-password') } steps { // 此时 KEYSTORE_PASSWORD 变量就是安全的密码 bat """ // 在Unity构建脚本中,通过环境变量读取这个密码 set UNITY_KEYSTORE_PASS=%KEYSTORE_PASSWORD% ... """ } }
    或者在构建脚本中,通过System.Environment.GetEnvironmentVariable("KEYSTORE_PASSWORD")来获取。

最后,我想说的是,搭建CI/CD流水线是一个“磨刀不误砍柴工”的过程。初期投入一两天时间,换来的是日后每天数小时甚至数天的团队时间节省,以及构建质量的显著提升。从最简单的自动打包开始,逐步加入自动化测试、自动部署到测试服、甚至自动化商店提审流程,你会发现团队的开发节奏和交付信心有了质的飞跃。我的经验是,先让最核心的编译打包流程跑起来,看到那个绿色的“构建成功”标志,你和你的团队就会迫不及待地想把它做得更好了。