导语:周五傍晚六点,美术总监老周把最后一张场景贴图推进仓库,在群里发了一句“今晚能出包吗”。我点开编辑器,开始手动 Cook、打包、上传。七点,打包到 87% 报了一个 Shader 编译错误;八点重跑,发现忘关 Use Pak File;九点出的包启动后黑屏,日志里躺着一句
LogOutputDevice: Error: === Handled ensure: ===。等我把修复版上传到内部下载站,已经晚上十一点半。老周回了一句“辛苦了”,我只觉得下周不能再这样。
这不是某个项目的特例。任何使用 Unreal Engine 的团队,只要成员超过三个人、版本更新超过两周,都会撞上同一个问题:打包不应该是一个人守在机器前按按钮。UE5.8 的命令行工具链和 BuildGraph 已经相当成熟,配合 Jenkins 或 GitHub Actions,可以把 Cook、打包、测试、分发串成一条自动管线。本文把这条管线的每个关节拆开,讲清楚我们是怎么从“周五噩梦”走出来的。
1. 为什么自动构建不是奢侈品
手动打包的问题不只是累。它会引入大量不可复现的中间状态:本地缓存有没有清、插件版本对不对、编辑器是不是开着另一个项目、命令行参数有没有写错。同一份代码,在我的机器上能出包,在你的机器上可能就因为 DDC 过期而失败。更隐蔽的是,手动流程把“构建知识”锁在个人电脑里,老成员请假或离职,整个团队的发布能力会瞬间打折。
自动构建的核心价值是把发布变成一条可重复、可审计、可回滚的流水线。任何人提交代码后,系统都能在固定环境里跑一遍完整流程,产物被统一命名、统一签名、统一分发。出现问题时,我们可以回到某一次成功的构建编号,而不是翻聊天记录找“昨晚那个包”。
从成本角度看,搭建 CI/CD 需要一两天,但省下来的时间很快会回本。假设团队每周出两次测试包,每次手动打包平均消耗两小时,一年就是两百多小时。把这两百小时花在修 Bug 或做特性上,回报远高于维护一台构建机。
整个流程可以抽象成四个阶段:触发、构建、验证、分发。下面这张图展示了我们在 UE5.8 项目里实际使用的整体架构。
flowchart TD
subgraph source ["源代码仓库"]
git_server["Git Server"]
end
subgraph ci ["CI 服务"]
jenkins["Jenkins / GitHub Actions"]
build_agent["构建代理 Windows/Linux/Mac"]
end
subgraph artifact ["产物存储"]
s3["对象存储 / NAS"]
cdn["CDN / 内网下载站"]
end
subgraph users ["用户侧"]
tester["测试人员"]
developer["开发人员"]
end
git_server --> jenkins
jenkins --> build_agent
build_agent --> s3
s3 --> cdn
cdn --> tester
cdn --> developer2. 命令行 Cook 与打包
UE5.8 的编辑器界面再华丽,CI 环境里也只有命令行。理解命令行参数是搭管线的第一步。
2.1 核心工具链
引擎目录下的 Engine/Build/BatchFiles 里有三个最常用的脚本:
RunUAT.bat:通用构建入口,能跑 BuildCookRun、BuildGraph 等任务。RunUBT.bat:Unreal Build Tool,负责编译 C++ 模块。UE5Editor-Cmd.exe:无界面编辑器,用于执行蓝图/C++ 自动化测试、Cook 等。
一次完整的打包通常调用 RunUAT BuildCookRun。下面这条命令把 Game.uproject cooked 并打包成 Windows 64 位 Shipping 版本:
RunUAT.bat BuildCookRun ^
-project="D:\Work\MyGame\MyGame.uproject" ^
-noP4 ^
-platform=Win64 ^
-clientconfig=Shipping ^
-serverconfig=Shipping ^
-cook ^
-allmaps ^
-stage ^
-pak ^
-archive ^
-archivedirectory="D:\Builds\MyGame"参数含义:
-noP4:不连接 Perforce,纯 Git 项目必须加。-clientconfig=Shipping:客户端用 Shipping 配置。-cook:执行 Cook,把资源转成目标平台格式。-allmaps:Cook 项目里的所有地图。-stage:把 Cook 结果复制到 Staging 目录。-pak:把文件打进 Pak 文件。-archive:把最终结果复制到-archivedirectory。
如果只想 Cook 不打包,可以去掉 -stage、-pak、-archive,单独跑 -cook。这在快速验证资源改动时很有用。
2.2 命令行打包的常见坑
第一个坑是 DDC(Derived Data Cache)。CI 机器上的 DDC 默认写在用户目录,每次构建机重建都会清空,导致 Cook 时间爆炸。解决方案是在构建命令里显式指定共享 DDC 路径:
-ddc=DerivedDataBackendGraph并在 DerivedDataBackendGraph.xml 里把 Shared 节点指向 NAS 或 S3 兼容存储。
第二个坑是插件版本。UE5.8 的项目常常依赖 Marketplace 插件或内部插件,如果这些插件以子模块方式管理,构建前必须执行 git submodule update --init --recursive。
第三个坑是长路径。Windows 默认 MAX_PATH 是 260 字符,引擎中间文件很容易突破这个限制。构建机要开启长路径支持,并在命令行里避免把项目放在过深的目录。
3. BuildGraph 简介
BuildGraph 是 UE 内置的一套声明式构建脚本系统,用 XML 描述任务依赖图。它比直接在命令行里拼 BuildCookRun 更灵活,适合多平台、多配置、多阶段的复杂管线。
3.1 基本结构
一个 BuildGraph 脚本由 BuildGraph 根节点、Agent 代理节点、Node 任务节点和 Job 作业节点组成。节点之间通过 Requires 声明依赖。下面是一个最小示例,只做客户端 Cook:
<?xml version="1.0" encoding="utf-8"?>
<BuildGraph xmlns="http://www.epicgames.com/BuildGraph"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.epicgames.com/BuildGraph ../Schema/BuildGraph.xsd"
Version="1.0">
<Property Name="Project" Value="D:\Work\MyGame\MyGame.uproject"/>
<Property Name="ArchiveDir" Value="D:\Builds\MyGame"/>
<Agent Name="WindowsBuilder" Type="Windows">
<Node Name="Cook Client">
<Job Name="Cook Win64" Command="RunUAT.bat BuildCookRun -project=$(Project) -platform=Win64 -clientconfig=Development -cook -allmaps -stage -pak -archive -archivedirectory=$(ArchiveDir)"/>
</Node>
</Agent>
</BuildGraph>3.2 为什么用 BuildGraph
当项目需要同时出 Windows、Linux 和 Android 三个包时,手动写三条命令也能跑,但依赖关系和错误处理会变得非常脆弱。BuildGraph 可以把公共步骤拆成节点,例如 Build Editor、Cook Content、Package Win64、Package Linux、Package Android,然后让平台打包节点都依赖 Cook Content。一旦 Cook 失败,所有平台打包都不会启动,避免浪费算力。
BuildGraph 还支持条件节点和参数化,方便做版本构建。比如通过命令行传入 -set:BuildConfig=Shipping,脚本内部根据变量决定用 Development 还是 Shipping。
下面这张图展示了一个典型的多平台 BuildGraph 依赖关系:
flowchart TD
A["Setup: 拉取代码 + 更新子模块"] --> B["Build Editor"]
B --> C["Cook Content"]
C --> D["Package Win64"]
C --> E["Package Linux Server"]
C --> F["Package Android"]
D --> G["Run Smoke Tests"]
E --> G
F --> G
G --> H["Archive to Storage"]
H --> I["Notify Team"]4. Jenkins 与 GitHub Actions 配置
CI 服务负责监听仓库事件、调度构建代理、收集日志和产物。Jenkins 和 GitHub Actions 是 UE 团队最常见的两种选择,核心思路相通,只是配置语法不同。
4.1 Jenkins 配置要点
我们在构建机上装了一个 Jenkins Agent,Label 设为 ue5-builder-win。Jenkins Master 收到 Push 事件后,把任务派给这个 Agent。关键配置如下:
- 源码管理:Git,仓库地址填项目仓库,分支规范用
*/develop、*/release/*。 - 触发器:Webhook 触发,Git 服务器 Push 时自动拉起构建。
- 构建环境:清理工作区里的
Saved/Logs、Intermediate、DerivedDataCache中过期的临时目录,但保留.git以加速拉取。 - 构建步骤:执行 PowerShell 或 Batch 脚本,先更新子模块,再调用 RunUAT 或 BuildGraph。
- 产物归档:把
D:\Builds\MyGame\*.zip或*.exe归档到 Jenkins,并同步到 S3。
Jenkins 的 Blue Ocean 视图对多阶段管线很友好,能把 Cook、打包、测试、分发拆开显示。哪一步红了,一眼就能看到。
4.2 GitHub Actions 配置要点
如果项目托管在 GitHub,GitHub Actions 省去了维护 Jenkins Master 的成本。Runner 可以用 GitHub 托管的 Windows Runner,也可以自托管。UE5.8 项目建议自托管,因为默认 GitHub Runner 的磁盘空间和内存对完整打包来说比较紧张。
一个典型的工作流文件结构:
name: UE5.8 Build
on:
push:
branches: [develop, release/*]
pull_request:
branches: [develop]
jobs:
build:
runs-on: [self-hosted, windows, ue5]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
lfs: true
- name: Build Game
shell: powershell
run: |
& "C:\UE_5.8\Engine\Build\BatchFiles\RunUAT.bat" `
BuildCookRun `
-project="$env:GITHUB_WORKSPACE\MyGame.uproject" `
-platform=Win64 `
-clientconfig=Development `
-cook -allmaps -stage -pak -archive `
-archivedirectory="D:\Builds\MyGame"
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: MyGame-Win64-Development
path: D:\Builds\MyGame\**注意 submodules: recursive 和 lfs: true。很多 UE 项目把美术资源放 Git LFS,如果漏开,构建时会出现大量资源缺失。
4.3 CI 里的权限与凭证
构建脚本需要访问代码仓库、S3、证书、版本服务器等。不要把密码或密钥直接写在仓库里。Jenkins 用 Credentials Binding,GitHub Actions 用 Repository Secrets。上传代码到 Steam、Epic Online Services 或私有 CDN 时,尤其要注意密钥的最小权限原则。
下面这张图展示了代码提交之后直到构建完成的完整时序:
sequenceDiagram
participant D as "开发者"
participant R as "Git 仓库"
participant C as "CI 服务"
participant A as "构建代理"
participant S as "对象存储"
D->>R: git push origin develop
R->>C: 触发 Webhook
C->>A: 分配 ue5-builder-win
A->>R: git fetch + submodule update
A->>A: RunUAT BuildCookRun
A->>A: 执行自动化测试
A->>S: 上传 Pak / 安装包
C->>D: 发送构建结果通知5. 版本构建与分支策略
CI/CD 不只是自动跑命令,还要和版本管理配合。没有清晰的分支策略,自动构建很容易变成“每次 Push 都出包,但没人知道哪个包能用”。
5.1 分支模型
我们的项目采用以下分支模型:
main:永远可发布,只接受从release/*合并进来的代码。release/1.x:版本分支,出候选包和正式包。develop:日常集成,自动出开发包给策划和测试试玩。feature/*:特性分支,只做编译检查,不出完整包。hotfix/*:紧急修复,可以直接合入main并触发补丁构建。
CI 对不同分支做不同的事:
feature/*:编译检查 + Cook 检查,验证资源引用是否破坏。develop:完整 Development 包 + 自动化 Smoke 测试。release/*:Shipping 包 + 完整测试 + 上传到内测分发站。main:与最后一次release构建结果一致,只补标签。
5.2 构建编号与版本号
每个正式包都要有唯一版本号。我们采用 主版本.次版本.补丁.构建号 的格式,例如 1.3.0.4821。构建号由 CI 自动递增,写入 DefaultGame.ini 或代码里的版本宏。
版本号要回写到两个地方:
- 游戏内显示:通过
FApp::GetBuildVersion()读取,方便测试人员报 Bug 时确认版本。 - 产物文件名:例如
MyGame-Win64-Shipping-1.3.0.4821.exe,避免下载站里一堆MyGame.zip分不清。
5.3 多配置构建
Development 配置编译快、调试信息完整,适合日常验证;Shipping 配置做了大量优化和剥离,适合对外发布。Test 配置介于两者之间,保留部分调试能力又接近发布性能。CI 管线应该同时覆盖这三种配置,而不是只出 Development。
从时间成本看,三种配置的构建时间并不一样。设 Development 构建时间为 ,Test 为 ,Shipping 为 。通常有 ,经验关系接近 ,。如果某次提交只想验证编译,只跑 就够了;如果要做发布候选,再跑完整矩阵。
6. 自动化测试
打包成功不等于游戏能玩。CI 里必须嵌入自动化测试,否则只是把有 Bug 的包自动发出去。
6.1 测试金字塔
UE 的自动化测试分为三层:
- 单元测试:针对纯 C++ 函数,使用
IMPLEMENT_SIMPLE_AUTOMATION_TEST宏。 - 功能测试:针对蓝图和 Gameplay 逻辑,使用 Gauntlet 或编辑器自动化测试框架。
- 端到端测试:启动打包后的游戏,跑一段固定流程,检查是否崩溃、帧率是否达标。
底层测试跑得快、定位准,应该尽量多写;顶层测试搭建成本高,但覆盖真实玩家路径,必须保留关键用例。
6.2 命令行执行测试
UE5.8 可以通过命令行跑自动化测试:
UE5Editor-Cmd.exe "D:\Work\MyGame\MyGame.uproject" -ExecCmds="Automation RunTests MyGame" -testexit="Automation Test Queue Empty" -log -unattended这条命令会启动无界面编辑器,跑 MyGame 测试组,等测试队列清空后退出。CI 通过检查退出码判断测试是否通过。
对于打包后的端到端测试,Gauntlet 是更专业的选择。它可以在 CI 里启动打包客户端,连接到专用测试服务器,跑完指定地图后收集日志和截图。Gauntlet 的配置相对复杂,但它是主机平台认证的常用工具。
6.3 性能回归测试
除了功能正确性,还要防止性能回退。我们在 CI 里固定一台机器跑同一段关卡录制,记录平均帧时间 和 P99 帧时间 。如果某次构建让 比基线高出超过 ,就标记为性能回归失败。
性能测试对环境一致性要求很高。同一台机器必须固定驱动版本、关闭 Windows 更新、禁止休眠、插电源、使用有线网络。否则数据的噪声会掩盖真实的性能变化。
7. 产物分发
构建出来的 Pak 文件、安装包、符号文件需要被正确分发到不同角色手里。
7.1 产物类型
一次完整构建通常产出:
- 客户端安装包或绿色版压缩包。
- 专用服务器二进制(如果是多人游戏)。
.pak文件,用于增量更新或补丁。- 调试符号文件
.pdb/.sym,用于崩溃分析。 - Cooked 日志和 BuildGraph 报告。
符号文件往往很大,但绝不能丢。没有符号,线上崩溃的 minidump 就解不出堆栈。可以把符号单独压缩上传到符号服务器或对象存储,客户端包本身不带符号。
7.2 分发渠道
内部团队常用的分发方式:
- 对象存储 + 预签名 URL:适合中小团队,按构建号组织目录。
- 自研下载站:前端展示版本说明、下载按钮、构建状态。
- Steam / Epic:对外发布时直接走官方渠道,但需要额外的上传步骤。
- CDN:大规模分发时把产物推到 CDN,减轻源站压力。
我们目前的做法是:构建成功后把产物上传到 MinIO 桶,按 MyGame/1.3.0.4821/Win64/ 这样的路径组织。下载站通过 API 读取桶内列表,测试人员按版本号下载。
7.3 增量更新
完整包动辄几十 GB,每次测试都重新下载很浪费。UE 的 Pak 文件适合做增量更新。对比两个版本的 Pak,只把变化的 Pak 或块推送到更新服务器。CI 里可以用 PakTools 或自研差分工具生成增量补丁。
增量大小的上限可以用信息熵角度理解。设版本 A 和版本 B 的资源集合差异为 ,补丁大小 满足:
其中 是差异块的出现概率。再好的差分算法也无法突破信息论下界。如果某次更新把大量贴图从 2K 换成 4K,补丁就不可能小。这个认知有助于我们和策划、美术沟通包体预期。
8. 失败排查
自动构建每天都会失败,关键是怎么快速定位。
8.1 日志分层
CI 失败时,先看三层日志:
- CI 控制台输出:告诉你哪个步骤红了,退出码是多少。
- UAT 日志
Saved/Logs/UBT-*.txt:编译和链接阶段的详细错误。 - 引擎日志
Saved/Logs/MyGame.log:Cook、打包、运行阶段的错误。
如果是 Cook 阶段失败,常见原因包括:资源引用丢失、蓝图编译错误、材质节点版本不兼容。如果是打包阶段失败,常见原因包括:插件未正确引用、目标平台 SDK 缺失、代码签名失败。
8.2 复现失败
CI 环境失败但本地正常,是最头痛的情况。排查思路:
- 对比环境差异:CI 是否用了和你一样的引擎版本、SDK、Windows SDK 版本。
- 检查缓存:CI 是否清空了 DDC 或 Intermediate,本地是否依赖了旧缓存。
- 路径差异:CI 的项目路径是否包含中文或空格,某些旧插件对此敏感。
- 并发问题:CI 是否并行执行了多个构建,导致文件锁冲突。
8.3 重试与熔断
不是所有失败都需要人工处理。网络波动导致的子模块拉取失败、临时性的着色器编译超时,可以设置自动重试。但重试必须有上限,否则一个真正的编译错误会被无限重试,浪费资源。
我们对不同类型失败设置不同策略:
- 环境类失败(子模块超时、DDC 网络抖动):重试 2 次。
- 编译类失败:立即失败,不自动重试。
- 测试类失败:保留现场日志和截图,供人工分析。
下面这张图总结了失败排查的决策树:
graph TD
A["构建失败"] --> B{失败发生在哪一阶段}
B -->|Setup| C["检查子模块 / LFS / 凭证"]
B -->|Compile| D["检查 UBT 日志与代码变更"]
B -->|Cook| E["检查资源引用与蓝图错误"]
B -->|Package| F["检查插件引用与 SDK"]
B -->|Test| G["检查测试日志与崩溃转储"]
C --> H{本地能否复现}
D --> H
E --> H
F --> H
G --> H
H -->|"能"| I["修复后重新提交"]
H -->|"不能"| J["对比 CI 与本地环境差异"]
J --> K["更新 Docker/Agent 镜像或文档"]9. 最佳实践
最后整理一些踩过坑后沉淀下来的原则。
9.1 把构建环境当代码管
构建机的环境不应该靠口口相传维护。用 Infrastructure as Code 的方式记录:Windows 版本、Visual Studio 工作负载、Windows SDK 版本、引擎安装路径、环境变量。我们维护了一份 PowerShell 脚本 setup-build-agent.ps1,新机器半小时内就能加入构建池。
9.2 每日构建必须绿
develop 分支的每日构建是团队的血压计。如果连续两天红着,测试和策划就拿不到新包,特性验证会全面滞后。我们的规则是: develop 红的时候,全团队优先修构建,再写新功能。
9.3 产物保留策略
构建产物会无限增长。我们保留最近 30 天的 Development 包、最近 10 个 Release 候选包、所有正式版本的包。符号文件保留时间与对应版本一致。过期产物自动清理,避免存储账单失控。
9.4 让通知有信息量
构建成功或失败的通知不要只发一句“构建失败”。通知里应该包含:构建号、分支、提交人、失败阶段、日志链接、产物链接。这样收到通知的人能立刻判断是否需要处理。
9.5 文档化失败案例
把每次 CI 故障和修复方法写成内部 Wiki。半年后你会得到一本很有价值的《构建故障手册》。新成员遇到同样的问题时,不用再问老员工。
10. 写在最后
回到周五晚上的那个故事。现在我们团队的构建流程是这样的:开发者 Push 代码到 develop,Jenkins Agent 自动拉取、Cook、打包、跑 Smoke 测试,把包上传到下载站,然后在企业微信发一条消息。老周再也不用问我“今晚能出包吗”,他自己会点开链接看构建状态。
自动构建不是把人的工作交给机器,而是把重复的、容易出错的、容易依赖个人的工作交给机器。省下来的时间和精力,用来打磨玩法、修真正的 Bug、陪家人吃顿晚饭。这套管线的搭建没有银弹,但 UE5.8 的命令行工具、BuildGraph 和现代 CI 服务已经提供了所有必要的积木。剩下的,就是一块一块把它们搭起来。