
1. 项目概述为什么我们需要为Unity工程搭建CI/CD流水线如果你是一个Unity开发者或者是一个小型游戏工作室的技术负责人你大概率经历过这样的场景美术同学更新了一个模型程序同学修复了一个Bug策划同学调整了一个数值表然后大家围在一台“构建机”旁边等着某位同事手动点击Unity编辑器上的“Build”按钮。这个过程可能持续十几分钟到几个小时期间不能断电、不能断网、不能有任何意外否则就得重来。更头疼的是当项目需要同时构建Android、iOS、Windows、WebGL等多个平台时手动操作的复杂度和出错率会呈指数级上升。这不仅仅是效率问题更是团队协作和项目质量的巨大隐患。“持续化编译部署”或者说CI/CD持续集成/持续交付就是为了根治这个痛点。它的核心思想是将代码提交到版本库如Git这个动作作为触发一系列自动化流程的开关。这个流程会自动拉取最新代码、解决依赖、执行编译、运行测试、打包成品并最终部署到测试环境或分发渠道。对于Unity项目而言这意味着美术、程序、策划的任何提交都能在几分钟到几小时内得到一个可运行的、跨平台的测试包供团队快速验证。Jenkins作为一款开源的、功能强大的自动化服务器正是搭建这条流水线的绝佳工具。它就像一个不知疲倦的、严格按照指令行事的构建机器人7x24小时待命。我经历过从纯手动构建到搭建自动化流水线的全过程实话说初期搭建会花一些功夫但一旦跑通带来的解放感和质量提升是颠覆性的。你再也不用担心构建环境不一致导致的“在我机器上是好的”这种问题因为构建环境被固化在了脚本和配置里。你也可以在每天凌晨自动为项目生成一个“每日构建”Nightly Build让测试同学一早就能拿到最新版本。接下来我就把搭建这套系统的核心思路、实操步骤以及我踩过的那些坑毫无保留地分享给你。2. 核心架构与工具选型解析在动手之前我们需要理清整个流水线的核心组件和它们之间的协作关系。一个典型的UnityJenkins CI/CD流水线通常包含以下几个部分版本控制系统这是一切的源头通常是Git如GitLab、GitHub、Gitee或自建Git服务器。所有代码、资源、配置的变更都通过提交Commit和推送Push到这里。CI/CD服务器也就是Jenkins。它负责监听版本库的变更如Git的Webhook触发构建任务并在指定的“构建代理”上执行我们编写好的构建脚本。构建代理实际执行Unity编译命令的机器。它可以是Jenkins服务器本身也可以是另一台专门用于构建的、性能更强的机器Windows、macOS或Linux。关键点在于这台机器上必须安装好指定版本的Unity Editor无图形界面模式以及必要的SDK如Android SDK/NDK、Xcode。构建脚本自动化流程的灵魂。这是一系列用命令行驱动Unity和后续处理步骤的脚本可以用C#、Shell、Batch或PowerShell编写。Unity官方提供的Unity.exe -batchmode -quit -projectPath ... -executeMethod ...命令行接口是核心。制品仓库存放构建产物的地方比如打包好的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. 定义要打包的场景 Liststring scenes new Liststring(); 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。创建一个用于持久化Jenkins数据的目录sudo mkdir -p /var/jenkins_home sudo chown 1000:1000 /var/jenkins_home # Jenkins容器内用户UID通常是1000使用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引擎用于运行其他容器化构建环境。查看初始密码并登录docker logs jenkins在日志中寻找类似Please use the following password to proceed to installation:的信息复制密码。然后在浏览器访问http://你的服务器IP:8080输入密码完成初始插件安装向导。我建议在向导中选择“安装推荐的插件”。3.2 初始配置与必要插件安装安装完推荐插件后进入Jenkins主界面我们还需要安装几个对Unity构建至关重要的插件Git plugin 通常已默认安装用于从Git仓库拉取代码。Pipeline 用于支持最强大的“Pipeline”任务类型我们将用编写Jenkinsfile的方式定义流水线。Credentials Binding Plugin 安全地绑定密码、密钥等凭证到构建环境变量中。Workspace Cleanup Plugin 构建前后清理工作空间避免残留文件干扰。进入“系统管理” - “插件管理” - “可选插件”搜索并安装上述插件。接下来配置全局工具。进入“系统管理” - “全局工具配置”Git 如果你的服务器上已安装Git可以指定Path to Git executable如/usr/bin/git。也可以选择让Jenkins自动安装。JDK Jenkins本身需要JavaLTS镜像已自带通常无需额外配置。3.3 构建代理节点配置关键如果你的Jenkins服务器本身性能足够且与Unity构建环境一致比如都是Windows可以直接在Jenkins服务器即Built-In Node上执行构建。但更常见的做法是使用专门的、安装了Unity的机器作为构建代理。配置Windows构建代理物理机/虚拟机在Jenkins主界面进入“系统管理” - “节点管理” - “新建节点”。输入节点名称如Unity-Windows-Builder选择“固定节点”。配置节点执行器数量根据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; // 设置包版本 Liststring 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不仅仅是构建更重要的是集成。可以在构建流水线中加入自动化测试阶段。单元测试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 } } }静态代码分析集成Roslyn Analyzers或Unity的Microsoft.CodeAnalysis进行代码规范检查。构建后测试对于打出的包可以编写简单的自动化脚本如使用Appium、AltTester等进行安装和冒烟测试确保基本功能可运行。6. 常见问题排查与实战心得搭建过程中你肯定会遇到各种问题。这里记录了几个最典型的问题和解决方法。6.1 Unity批处理模式构建失败这是最常见的问题。请按以下顺序排查检查日志这是最重要的构建命令中一定要加上-logFile参数如-logFile build.log构建失败后第一时间查看日志文件。错误信息通常非常明确。许可证问题无图形界面模式下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来激活。编译错误如果项目代码有错误批处理模式会直接失败。确保在提交触发自动构建前在本地编辑器里编译通过。路径或权限问题确保Unity执行路径、项目路径、输出路径都存在且Jenkins代理进程有读写权限。路径中避免使用中文和特殊字符。6.2 Jenkins代理连接不稳定或构建卡住检查网络和防火墙确保Jenkins主机与代理机之间的TCP端口默认50000通信畅通。检查代理机资源构建Unity项目非常消耗CPU和内存。监控代理机资源使用情况避免因资源耗尽导致进程假死。可以在Jenkins节点配置中减少“执行器数量”。查看代理日志在代理机的Jenkins代理程序运行窗口中或查看其日志文件通常在代理工作目录下寻找错误信息。使用“流水线步骤查看器”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的“凭证管理”功能是为此而生的。在Jenkins后台“管理Jenkins” - “管理凭证” - “全局凭证”中添加一个类型为“Secret file”或“Secret text”的凭证。在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流水线是一个“磨刀不误砍柴工”的过程。初期投入一两天时间换来的是日后每天数小时甚至数天的团队时间节省以及构建质量的显著提升。从最简单的自动打包开始逐步加入自动化测试、自动部署到测试服、甚至自动化商店提审流程你会发现团队的开发节奏和交付信心有了质的飞跃。我的经验是先让最核心的编译打包流程跑起来看到那个绿色的“构建成功”标志你和你的团队就会迫不及待地想把它做得更好了。