1. 项目概述:为什么我们需要uet?
如果你是一名虚幻引擎(Unreal Engine)开发者,尤其是参与过中大型项目,那么下面这个场景你一定不陌生:项目组新来了一位同事,你让他把项目从源码库拉下来,然后告诉他“用引擎版本5.3.2,编译一下,再运行一下自动化测试看看”。接下来,你可能会听到一连串的问题:“编译选项怎么配?用Development还是Shipping?测试怎么跑?是直接在编辑器里点那个按钮吗?测试报告在哪看?我本地没装那个Python包怎么办?” 光是搭建一个能构建、能测试的本地环境,可能就要花掉半天甚至更久的时间。
这还只是个人开发。到了持续集成(CI)环节,问题会指数级放大。Jenkins、GitLab CI或者GitHub Actions上的构建脚本,往往是一堆错综复杂的批处理、PowerShell和Python脚本的缝合怪。它们硬编码了引擎路径、平台工具链的调用方式,以及各种脆弱的文件路径。一旦引擎版本升级、项目结构变动,或者需要在新的平台上(比如从Windows换到Linux)进行构建,这套脚本就可能直接崩溃,排查起来如同大海捞针。
uet的出现,正是为了解决这些痛点。它不是一个全新的构建系统,而是一个基于Python的、面向虚幻引擎项目的统一命令行接口和自动化工作流工具。你可以把它想象成虚幻引擎领域的make或cmake,但它更懂UE项目的“规矩”。它的核心目标是标准化和简化从项目生成、编译、打包、测试到部署的整个生命周期,让开发者能用一个简单、一致的命令,在不同的机器、不同的平台上,获得可重复、可靠的结果。
简单来说,uet试图把那些隐藏在编辑器UI背后、散落在各种文档角落、以及存在于资深开发者“肌肉记忆”里的构建与测试知识,封装成一套清晰的、可脚本化的工具链。无论你是想快速验证一次代码提交,还是为团队搭建一套企业级的CI/CD流水线,uet都试图提供一个更优雅的起点。
2.uet的核心设计理念与架构拆解
2.1 从混沌到秩序:uet解决的问题域
在深入uet之前,我们先看看传统的UE项目工作流有多“混沌”。一个典型的UE C++项目,其构建和测试涉及多个层次和工具:
- 项目文件生成:需要调用
UnrealBuildTool来生成.sln或.xcodeproj文件。 - 源码编译:调用
UnrealBuildTool或平台原生编译器(如MSBuild、clang)来编译引擎和游戏模块。 - 内容构建:调用
UnrealEditor-Cmd.exe进行Cook(内容烹饪)、Pak(打包)等。 - 测试执行:单元测试、功能测试、自动化测试可能分散在多个地方——有基于
Google Test的C++单元测试,有在编辑器内运行的Functional Testing,还有使用Gauntlet或自定义Python脚本的端到端测试。 - 环境管理:需要正确设置引擎路径、
.uproject文件路径、各种SDK路径(如Android NDK, iOS Provisioning Profile)。
这些步骤每一个都有自己的一套参数、调用方式和潜在的坑。uet的设计理念,就是用一个“动词-名词”结构的命令行接口,将这些步骤统一起来。例如:
uet build: 处理所有编译相关的事情。uet test: 处理所有测试相关的事情。uet cook: 处理内容烹饪。uet package: 处理最终的游戏打包。
2.2 架构分层:插件化与可扩展性
uet没有试图重新发明轮子,而是扮演了一个“胶水”和“调度器”的角色。它的架构大致可以分为三层:
- 核心层(Core):提供基础框架,包括命令行解析、配置管理、日志系统、插件加载机制。它定义了一套抽象的“命令”接口,任何具体功能都以插件形式实现。
- 内置插件层(Built-in Plugins):
uet自带了一系列实现核心功能的插件。例如:build插件:封装了对UnrealBuildTool的调用,处理不同配置(Debug, Development, Shipping)和平台(Win64, Linux, Android)的编译。test插件:整合了运行各种测试的流程,可能是调用引擎的自动化测试工具,也可能是执行项目自定义的测试脚本。project插件:用于创建、初始化和管理.uproject文件。
- 自定义/第三方插件层:这是
uet强大扩展性的体现。团队或个人可以编写自己的uet插件,来封装项目特有的构建步骤。比如,你的项目在打包后需要自动上传到内部分发平台,或者需要执行一套特定的资源后处理流程,都可以写成一个独立的uet插件命令。
这种插件化架构意味着uet本身是轻量的,它的功能边界可以由使用者自由定义。官方提供“开箱即用”的基础能力,复杂项目则可以通过自定义插件来满足特定需求,而无需污染或魔改核心工具链。
注意:评估一个构建工具,不仅要看它现在能做什么,更要看它是否易于扩展来适配你未来的需求。
uet的插件化设计在这方面提供了很好的基础。
2.3 配置即代码:.uet.toml的威力
uet强烈推荐使用配置文件来管理项目设置。通常,你会在项目根目录创建一个名为.uet.toml的文件。这个文件是uet的“唯一真相源”,它定义了:
- 项目使用的引擎版本和路径(或引擎标识符)。
- 项目本身的路径。
- 为不同命令预设的选项和参数。
- 自定义插件的位置和配置。
# .uet.toml 示例 [project] name = "MyAwesomeGame" path = "D:/Projects/MyAwesomeGame/MyAwesomeGame.uproject" [engine] version = "5.3" # 可以是本地路径 location = "D:/UE_5.3" # 或者使用 Epic 的版本标识符 # identifier = “5.3” [commands.build] configuration = "Development" platform = "Win64" target = "MyAwesomeGameEditor" [commands.test] type = "functional" # 可以是 unit, functional, gauntlet 等 report_format = "junit" # 输出 JUnit 格式的报告,便于 CI 集成通过配置文件,你将原本需要每次在命令行输入的一长串参数固化下来。在CI环境中,这尤其重要——你只需要确保CI机器上存在这个配置文件,或者通过环境变量动态生成它,就能保证每次构建的环境和参数完全一致,实现了真正的“配置即代码”。
3. 核心功能深度解析与实操要点
3.1 构建(Build):不止是编译
uet build命令是使用频率最高的命令之一。它的工作远不止调用编译器那么简单。
典型工作流:
# 1. 生成项目文件(如果不存在或引擎版本变更) uet generate # 2. 编译项目(使用 .uet.toml 中的默认配置) uet build # 3. 编译特定配置和平台 uet build --configuration Shipping --platform Android # 4. 编译多个目标(如同时编译Game和Editor) uet build --target MyGame MyGameEditor背后原理与细节:
- 环境探测:
uet首先会解析.uet.toml和系统环境变量,确定要使用的UnrealBuildTool的准确路径。它知道不同引擎版本下UBT的位置可能不同。 - 参数转换与传递:
uet会将你提供的友好参数(如--configuration Shipping)转换为UBT能识别的参数(如-configuration=Shipping)。同时,它会帮你处理一些繁琐的细节,比如自动添加-project=参数指向你的.uproject文件。 - 并行与日志:
uet可以控制编译的并行度(-j参数),并重定向UBT的输出。一个重要的功能是解析编译错误和警告。原生的UBT输出信息量巨大,uet可以对其进行过滤和格式化,高亮显示错误行,让开发者更快定位问题。 - 增量构建支持:
uet尊重UBT的增量构建系统。它会检查时间戳和依赖关系,只编译发生变化的模块,大幅提升日常开发中的编译速度。
实操心得:在团队中,建议将
--configuration Development --platform Win64这类常用组合写入.uet.toml的预设中。这样新成员只需uet build即可开始,避免了参数记忆负担。对于CI,则显式指定Shipping等配置,确保产出一致。
3.2 测试(Test):统一测试入口的挑战与实现
测试是uet的另一个核心,也是传统上最分散的环节。uet test命令的目标是提供一个统一的入口来运行所有类型的测试。
支持的测试类型:
- 单元测试:基于
Google Test的C++单元测试。uet会定位到编译生成的.exe测试运行器(通常位于Binaries/[Platform]下),执行它并收集结果。 - 功能测试:在编辑器内运行的自动化测试。这是最复杂的一种,因为
uet需要以“命令行模式”启动UnrealEditor-Cmd.exe,加载指定的测试地图或测试套件,执行测试,然后安全退出并返回结果。 - Gauntlet 测试:用于多客户端/服务器端到端测试的框架。
uet可以调用Gauntlet的Python脚本,设置测试会话(如1个服务器+2个客户端),运行测试并生成报告。
一个运行功能测试的示例:
# 运行项目中所有的功能测试 uet test --type functional --all # 运行特定的测试地图 uet test --type functional --map “/Game/Tests/MyFunctionalTestMap” # 指定报告输出路径和格式 uet test --type functional --all --report-dir “./TestResults” --report-format junit实现难点与uet的解决方案:
- 编辑器进程管理:运行功能测试需要启动一个无界面的编辑器进程。
uet必须妥善处理这个进程的生命周期——启动、监控其输出(包括测试日志和可能的崩溃信息)、在测试完成后终止它。这里涉及到超时设置、信号处理等复杂逻辑。 - 结果解析:编辑器测试的输出是结构化的日志(通常包含
LogFunctionalTest: Warning/Error等标签)。uet需要从海量日志中精准提取测试通过/失败的信息,并将其转换为标准的测试报告格式(如JUnit XML),以便被Jenkins、GitLab等CI系统识别。 - 资源清理:测试过程中可能会产生临时文件或修改配置。
uet需要在测试前后确保环境的一致性,或者在测试后执行清理操作。
注意事项:功能测试非常依赖项目内容。确保你的测试地图和相关的资产(蓝图、材质等)已经正确烹饪(Cook)并随项目打包。在CI中,通常需要先执行
uet cook和uet package(或至少是-cook阶段),然后再运行uet test。
3.3 项目与引擎管理
除了构建和测试,uet还提供了一些提升开发体验的辅助命令。
uet project:可以快速创建新的UE项目模板,或者将现有文件夹初始化为一个uet管理的项目(创建.uet.toml)。uet engine:如果你通过Epic Games Launcher安装了多个版本的引擎,这个命令可以帮助你列出、切换或链接到特定的引擎版本。这对于需要同时维护多个使用不同UE版本的项目非常有用。
4. 集成到CI/CD流水线:从理论到实践
uet的真正威力在于自动化。下面我们以 GitHub Actions 为例,展示如何构建一个完整的UE项目CI流水线。
4.1 环境准备与缓存策略
UE项目构建是资源密集型任务,尤其是引擎源码的编译。在CI中每次都从头编译引擎是不现实的。常见的策略是:
- 使用预编译的引擎版本:从Epic的服务器下载特定版本的已编译引擎。
- 缓存引擎和中间文件:利用CI系统的缓存功能,将编译好的引擎和项目的中间文件(
Intermediate,Saved下的部分目录)缓存起来,下次构建时复用。
uet本身不提供下载引擎的功能,但可以很好地与现有的引擎管理工具(如自己编写的脚本或第三方工具)协同工作。在CI脚本中,我们通常先准备好引擎,再调用uet。
# .github/workflows/ci.yml 片段 name: UE Project CI on: [push, pull_request] jobs: build-and-test: runs-on: windows-latest # 或 macos-latest, ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 with: submodules: recursive # 如果引擎是git子模块 - name: Cache UE Engine uses: actions/cache@v3 id: cache-engine with: path: D:/UE_5.3 # 假设引擎安装在此路径 key: ${{ runner.os }}-ue-5.3-${{ hashFiles('**/.ueversion') }} # 用文件内容哈希作为缓存键 - name: Setup UE Engine (if not cached) if: steps.cache-engine.outputs.cache-hit != 'true' run: | # 这里调用你自己的引擎安装脚本 # 例如,使用 PowerShell 调用 Epic 的安装程序 API,或解压预下载的引擎包 ./scripts/setup_engine.ps1 -Version 5.3 -InstallPath “D:/UE_5.3” - name: Install Python and uet run: | python -m pip install --upgrade pip pip install uet # 从 PyPI 安装 uet - name: Generate Project Files run: uet generate env: UE_PATH: “D:/UE_5.3” # uet 可以通过环境变量识别引擎路径 - name: Build Project run: uet build --configuration Development --platform Win64 - name: Run Unit Tests run: uet test --type unit --report-dir ./TestResults --report-format junit continue-on-error: true # 测试失败不应立即终止,以便上传报告 - name: Upload Test Results if: always() # 无论成功失败都上传报告 uses: actions/upload-artifact@v3 with: name: test-results path: ./TestResults/4.2 多平台构建矩阵
对于需要发布到多个平台(Windows, Linux, Consoles, Mobile)的项目,可以利用CI的矩阵构建功能。
jobs: build-matrix: strategy: matrix: platform: [Win64, Linux, Android] include: - platform: Android sdk-setup: true runs-on: ${{ matrix.platform == 'Win64' && 'windows-latest' || matrix.platform == 'Linux' && 'ubuntu-latest' || 'macos-latest' }} # Android 构建通常也在 Windows 或 Linux 上进行 steps: - ... - name: Setup Android SDK (if needed) if: matrix.sdk-setup run: ./scripts/setup_android_sdk.ps1 - name: Build run: uet build --platform ${{ matrix.platform }}4.3 制品管理与发布
构建和测试通过后,最后一步通常是生成可发布的包(Package)。
- name: Package for Distribution run: | uet cook --platform Win64 uet package --configuration Shipping --platform Win64 env: # 打包可能需要额外的签名证书等环境变量 SIGNING_CERTIFICATE: ${{ secrets.SIGNING_CERTIFICATE }} - name: Upload Build Artifact uses: actions/upload-artifact@v3 with: name: MyGame-Win64-Shipping path: | ./Build/Win64/Shipping/MyGame/ !**/*.pdb # 排除符号文件 !**/Intermediate/5. 常见问题、排查技巧与进阶用法
5.1 安装与配置问题
问题1:pip install uet失败或安装后uet命令找不到。
- 排查:首先确认Python(建议3.8+)已正确安装并已添加到系统PATH。使用
python --version和pip --version检查。在Windows上,安装后可能需要重启终端,或者手动将Python的Scripts目录(如C:\Users\用户名\AppData\Local\Programs\Python\Python39\Scripts)添加到PATH。 - 解决:尝试使用
python -m uet来代替uet命令。如果还是不行,检查是否有多个Python版本冲突。
问题2:uet找不到Unreal Engine。
- 排查:
uet会按以下顺序查找引擎:1).uet.toml中的engine.location;2) 环境变量UE_PATH;3) 常见的默认安装路径;4) Epic Games Launcher的清单文件。 - 解决:最可靠的方法是在项目根目录的
.uet.toml中明确指定引擎路径。或者,在运行uet命令前设置UE_PATH环境变量。
5.2 构建与编译问题
问题3:编译失败,报错Missing UnrealBuildTool或UnrealBuildTool版本不匹配。
- 排查:这通常意味着
uet找到了引擎目录,但目录结构不对(例如,指向了包含多个版本引擎的根目录,而非具体的UE_5.3目录),或者该引擎版本未完整安装(缺少源码或开发文件)。 - 解决:检查
.uet.toml中的路径是否精确指向了类似D:/UE_5.3/Engine这样的具体引擎版本目录。对于从Epic Games Launcher安装的引擎,确保安装了对应版本的“引擎源码”。
问题4:增量构建失效,每次都全量编译。
- 排查:检查
Intermediate和Saved目录的权限,确保构建进程有写入权限。有时杀毒软件或云同步软件(如OneDrive)会锁定这些目录下的文件,导致时间戳更新异常。 - 解决:尝试执行一次
uet clean(如果uet提供了该命令或插件)来清理中间文件,然后重新构建。在CI环境中,为了绝对干净,通常建议每次构建前都清理Binaries和Intermediate目录。
5.3 测试执行问题
问题5:功能测试超时或无响应。
- 排查:功能测试启动编辑器进程,可能因为加载大型地图、执行复杂蓝图逻辑而耗时过长。也可能是测试本身存在死循环或崩溃,导致进程无法正常退出。
- 解决:为
uet test命令增加--timeout 300(单位秒)参数,设置一个合理的超时时间。同时,检查编辑器的日志输出(uet通常会将其重定向到控制台或文件),看是否有加载错误或断言失败。对于不稳定的测试,考虑将其标记为“不稳定”或拆分。
问题6:测试报告为空或格式错误。
- 排查:
uet的测试报告生成依赖于正确解析编辑器或测试运行器的输出。如果测试本身没有按照预期格式输出日志,报告就会为空。 - 解决:首先确保测试本身能正常运行并输出日志。可以手动运行一次测试,观察控制台输出。对于自定义的测试类型,可能需要编写或调整
uet的测试结果解析器(这涉及到自定义插件开发)。
5.4 进阶用法:编写自定义uet插件
当内置命令无法满足需求时,编写自定义插件是终极解决方案。一个最简单的插件只需要定义一个Python类并注册一个命令。
# 文件:my_project/plugins/my_custom_plugin.py from uet.core.commands import command, BaseCommand @command(“upload”, “将打包好的游戏上传到内部服务器”) class UploadCommand(BaseCommand): def setup_arguments(self, parser): parser.add_argument(“--version”, required=True, help=“版本号”) parser.add_argument(“--platform”, default=“Win64”, help=“目标平台”) def execute(self, args, unknown_args): # 你的业务逻辑 build_path = f“./Build/{args.platform}/Shipping” version = args.version print(f“准备上传 {build_path} 的版本 {version}...”) # 调用上传脚本或API # ... print(“上传完成!”) # 在 .uet.toml 中注册插件 # [plugins] # paths = [“./plugins”]然后,你就可以像使用内置命令一样使用它:uet upload --version 1.2.3 --platform Win64。这让你能将项目特有的部署、后处理、资源管理等流程无缝集成到统一的uet工作流中。
5.5 性能调优与最佳实践
- 善用缓存:在CI中,缓存
DerivedDataCache可以极大加速材质的Shader编译和资源的派生数据生成。将Engine/DerivedDataCache和项目下的Saved/DerivedDataCache加入缓存策略。 - 分布式构建:对于超大型项目,可以考虑使用
Distributed Compilation工具,如 Incredibuild 或 SN-DBS。uet build命令可以通过传递-j参数来利用这些工具,但具体的集成需要在构建机器上预先配置好环境。 - 配置文件分环境:可以创建多个
.uet.toml文件,如.uet.ci.toml、.uet.dev.toml,通过环境变量UET_CONFIG_FILE来指定使用哪个。这样可以为开发、测试、生产环境配置不同的默认参数。 - 日志分级:使用
--verbosity参数控制uet的输出详细程度。在CI中,通常使用verbose或veryverbose以便排查问题;在本地快速构建时,使用quiet或minimal减少干扰。
uet的价值在于它通过约定大于配置的方式,将虚幻引擎项目复杂的构建与测试流程标准化、自动化。它降低了新成员的上手成本,提升了团队协作的效率,并为稳健的CI/CD实践打下了坚实的基础。虽然初期需要一些学习和配置投入,但长远来看,这份投资对于任何严肃的虚幻引擎项目团队都是值得的。