上个月周五晚上十点,主程老周在群里发了张截图:他本地那张 L_AbandonedCity.umap 和服务器版本冲突,Resolve 窗口里只有两个按钮——Accept Source 和 Accept Target。他改了三天的光照烘焙,美术小林同一时间在同一个关卡里摆了新的广告牌。没有合并,只能二选一。老周把键盘一推,说了一句话:我们得聊聊版本控制了。
这正是 UE5.8 团队每天都在面对的命题。引擎越来越强,资产越来越重,协作模式却还停留在 UE4 甚至 UE3 的惯性里。下面我把这几年在几个项目里踩过的坑整理出来,希望能让你在周五晚上少砸一次键盘。
二进制资产的真相:是冰块,不是代码
Git 是给程序员写的。它默认你提交的是文本,diff 是一行一行看,merge 是三路合并。可 UE 里的 .uasset、.umap、.fbx、.wav 全是二进制。你用文本 diff 打开一个 Blueprint,看到的是乱码和十六进制。
二进制资产没法像 C++ 那样自动合并。两个人同时改同一个材质球,结局就是两份文件,必须选一个。这个选择的成本,老周那晚已经付过了。
工程上有个简单的估算:假设一个关卡每天有 个人会动,每个人平均每天提交 次,关卡被同时改到的概率大约可以写成
其中 是活跃系数, 是单次编辑时长, 是一天的工作窗口。人数一多,指数项很快就会让冲突几乎必然发生。这不是危言耸听,是我们在 8 人关卡组里统计过两周的数据。
所以二进制资产的管理核心就两件事:锁,或者拆。
Git LFS:让 Git 也能扛住大文件
很多独立团队和小型工作室从 Git 起家。Git 本身对大文件不友好,但 Git LFS 把它变成了可选项。原理很简单:仓库里只存一个文本指针,真正的文件放在 LFS 服务器上。
指针大概长这样:
version https://git-lfs.github.com/spec/v1
oid sha256:abc123...
size 123456789实际使用时,我会在项目根目录放一段 .gitattributes:
*.uasset filter=lfs diff=lfs merge=lfs -text
*.umap filter=lfs diff=lfs merge=lfs -text
*.fbx filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text这样 Git 知道这些文件归 LFS 管。第一次 clone 时,只有当前 checkout 的文件会被下载,而不是把整个仓库历史全拖下来。
Git LFS 也支持文件锁:
git lfs lock Content/Levels/L_AbandonedCity.umap
git lfs locks
git lfs unlock Content/Levels/L_AbandonedCity.umap锁的本质是告诉别人:这个文件我在改,你别动。但它的体验比 Perforce 差一截。锁状态不会自动出现在编辑器内容浏览器里,美术经常忘记锁,提交时才发现冲突。而且 Git LFS 锁在跨分支时行为也比较弱,branch 一多,锁会乱。
我们项目当时的妥协是:.uasset 走普通 Git(文件不大,数量多),.fbx、.wav、.umap 走 LFS,同时强制要求美术在改大文件前手动锁。这个规矩写在 CI 脚本里,未锁的 .umap 提交会被打回。
Git 团队的工作流
flowchart TD
A[美术打开一个 .umap] --> B{文件是否被锁}
B -->|是| C[看到锁拥有者,去喝咖啡]
B -->|否| D[git lfs lock 文件]
D --> E[编辑并保存]
E --> F[提交并 push]
F --> G[git lfs unlock]
G --> H[其他人可继续编辑]这个流程看起来不复杂,实际执行起来最大的敌人是忘了锁。所以后来我们给每个美术工位贴了张便利贴:改 .umap 前,先 git lfs lock。
Perforce:锁就是默认
Perforce 在游戏行业能屹立这么多年,不是因为它时髦,而是因为它对二进制资产的假设从根上就是独占编辑。你 p4 edit 一个文件,服务器就给你锁上;别人再想 edit,会收到一个清晰的提示。
Epic 官方的 UE5 Perforce 集成做得相当好。内容浏览器右下角会显示文件状态:未签出、已签出、被谁锁了。保存时如果文件已被别人锁定,编辑器会直接弹窗阻止。
要让它自动锁,需要在 Perforce typemap 里把 .uasset 和 .umap 设为独占锁:
p4 typemap在弹出的编辑器里加:
binary+l //depot/.../*.uasset
binary+l //depot/.../*.umap
binary+l //depot/.../*.fbx
binary+l //depot/.../*.wav
binary //depot/.../*.png
binary //depot/.../*.jpgbinary+l 的 +l 就是 lock,表示文件被签出时自动独占。这是 Perforce 比 Git LFS 锁更省心的地方:锁是工作流的一部分,而不是一个额外命令。
下面是一段常见的 Perforce 日常:
sequenceDiagram
autonumber
participant A as 美术小林
participant P as Perforce Server
participant B as 程序老周
A->>P: p4 edit Content/Materials/M_Road.uasset
P-->>A: 文件已锁定
B->>P: p4 edit Content/Materials/M_Road.uasset
P-->>B: 文件正被小林锁定
A->>P: p4 submit
P-->>A: 提交成功,锁释放
B->>P: p4 edit Content/Materials/M_Road.uasset
P-->>B: 现在可以编辑这段对话每天在我们项目里发生几百次。它让冲突在保存之前就被挡住,而不是在周五晚上合并时才暴露。
.gitignore 与 .p4ignore:别让垃圾进仓库
无论用 Git 还是 Perforce,忽略规则都是第一道防线。最忌讳的是把 Saved/、Intermediate/、DerivedDataCache/ 提交进去。这些东西又大又没用,还会让新人的第一次 sync 变成灾难。
一个我们项目实际在用的 .gitignore 大概长这样:
# UE 工程临时目录
*/Saved/
*/Intermediate/
*/DerivedDataCache/
*/Binaries/
*/Build/
# 用户本地配置
*.suo
*.user
*.vs/
.idea/
.vscode/
# 平台产物
*.apk
*.ipa
*.exe
*.dll
*.pdb
# 日志与崩溃
*.log
crashinfo--*
# 素材源文件走 LFS 或 DCC 管理
*.psd
*.max
*.ma
*.blend1Perforce 的 .p4ignore 语法和 Git 略有不同,核心思路一致:
# P4IGNORE for UE5
*/Saved/
*/Intermediate/
*/DerivedDataCache/
*.suo
*.user
*.tmp
*.pdb
*.log
.DS_Store有个细节很多人会踩:.p4ignore 只对还没 add 的文件生效。如果已经提交过 Saved/,再写规则是没用的,需要先 p4 delete 掉。我们项目初期就因为这个原因多同步了 30GB 的缓存文件,后来花了两个晚上清理。
OFPA:把关卡拆成乐高
UE5 的 World Partition 带了一个叫 One File Per Actor(OFPA)的机制。开启后,关卡里的每个 Actor 会存成独立的 .uasset 或 .uaactor 文件,放在 Content/__ExternalActors__/MapName/ 下面。原本一个 800MB 的 .umap,被拆成几万个几十 KB 到几 MB 的小文件。
这对协作是质变。小林改路灯,老周改光照,只要他俩动的不是同一个 Actor,就不需要锁,也不会冲突。只有在真正改到同一个 Actor 时,才会回到锁或二选一的老问题。
我用一个架构图说明:
architecture-beta
group level[关卡 L_AbandonedCity]
service umap(database)[L_AbandonedCity.umap]
service actors(database)[__ExternalActors__]
service actor1[路灯 Actor]
service actor2[广告牌 Actor]
service actor3[触发器 Actor]
umap:R -- L:actors
actors:T -- B:actor1
actors:T -- B:actor2
actors:T -- B:actor3umap 文件本身还在,但它只保留关卡元数据:World Partition 网格、关卡设置、Data Layer 配置。真正的 Actor 数据都散落在 __ExternalActors__ 里。
OFPA 和冲突的关系
OFPA 把冲突的粒度从整个关卡降到了单个 Actor。我用一张关系图表示:
graph LR
A[传统 .umap] -->|任何改动| B[整关冲突]
C[OFPA 模式] -->|改 Actor 1| D[仅 Actor 1 冲突]
C -->|改 Actor 2| E[无冲突]
C -->|改 Actor 3| F[无冲突]注意 OFPA 不是魔法。如果两个人同时改同一个路灯,依然要锁。只是日常工作中,这种概率比两个人同时改同一个 800MB 关卡低得多。
我们项目迁移到 OFPA 后,关卡相关的冲突从每周 5-8 次降到每月 1-2 次。这个数字够直观了吧?
冲突真的发生了怎么办
即使有了 OFPA 和锁,冲突仍会出现。常见场景:
- 有人强行 unlock 了别人锁住的文件,然后保存。
- 两个人在不同分支上改了同一个 Blueprint,最后合并。
- Git 团队忘了锁
.umap,同时提交。
二进制冲突没有自动合并。你的选择通常是:
- 保留服务器版本,自己的改动手动重做。
- 强制用自己的版本覆盖服务器。
- 如果改的是 Blueprint,用 UE 的 Blueprint Diff 工具对比,手动把差异抄过去。
Blueprint Diff 能显示两个版本里哪些节点被增删改,哪些变量默认值变了。它不能一键 merge,但能让你看清对方到底改了什么。对于材质、关卡、动画这些没有 diff 工具的类型,只能靠锁和沟通。
DDC:别让缓存毁了仓库
Derived Data Cache(DDC)是 UE 编译和导入时生成的缓存:贴图烘焙、材质 Shader、Niagara 模拟数据。这些文件动辄几十 GB,而且每个人本地生成的一模一样。提交进版本控制等于给仓库灌铅。
我们的 .gitignore 和 .p4ignore 都会排除:
*/DerivedDataCache/
**/DerivedDataCache/Boot.ddc
**/DerivedDataCache/**/*.udd但 DDC 还有一个进阶玩法:共享 DDC。在团队里搭一台 DDC 服务器,或者把 DDC 放到 NAS / 对象存储上,所有人共享。这样新人第一次打开项目不用等半天烘焙,CI 打包也能复用缓存。
UE5 的 BaseEngine.ini 里可以配置共享 DDC:
[DerivedDataBackendGraph]
Shared=(Type=FileSystem, ReadOnly=false, Clean=false, Flush=false, PurgeTransient=true, DeleteUnused=true, UnusedFileAge=10, FoldersToClean=-1, Path=\\\\nas\\ddc\\shared)路径换成你们自己的 NAS 地址。共享 DDC 是我们项目里最划算的一笔优化:每人每天省下的等待时间,换算成工资,几天就回本。
蓝图 Diff 与二进制合并的边界
UE 编辑器里自带 Blueprint Diff。右键一个 Blueprint 的历史版本,选 Diff Against Depot 或 Diff Against Local,能看到节点级别的差异。这个工具对程序看逻辑变更非常有用。
但它的边界很清楚:
- 只能对比两个版本的节点图、变量、默认值。
- 不能合并两个版本的 Blueprint。你看到差异后,只能手动把改动搬到自己版本。
- 对纯数据资产(Data Asset),UE 提供了 Data Asset Merge,可以在一定程度上自动合并。
所以 Blueprint Diff 是看清战场的工具,不是一键结束战斗的武器。真正解决冲突,还得靠锁和沟通。
Plastic SCM:被低估的第三选项
除了 Git 和 Perforce,Plastic SCM 也是 UE 团队可以考虑的方案。它支持二进制文件锁、分支可视化、与 Unity 和 UE 都有集成。对于中小型团队,Plastic 的许可证成本有时比 Perforce 更友好,界面也更现代。
它的工作流介于 Git 和 Perforce 之间:可以像 Git 一样分布式分支,也可以像 Perforce 一样对 .uasset 加锁。如果你的团队既想要 Git 的分支自由,又想要 Perforce 的锁,Plastic 是个折中选项。
我们项目没深度用过 Plastic,但我认识的一个 12 人团队用了两年,反馈是锁比 Git LFS 稳,分支比 Perforce 活。这只是个参考,具体还得看你们的预算和托管方式。
一些能直接抄的作业
说了这么多,最后列一份我们项目验证过的 checklist。你可以直接贴到团队 wiki 里:
- 所有
.uasset、.umap、.fbx、.wav默认走独占锁或 LFS 锁。 - 开启 OFPA,把关卡拆成 Actor 级文件。
.gitignore/.p4ignore必须排除Saved/、Intermediate/、DerivedDataCache/、Binaries/。- 搭建共享 DDC,别让每个人本地重复烘焙。
- 禁止 force unlock,除非锁的主人已经休假且改动已确认无效。
- 每个周五下午做一次锁状态检查,清理幽灵锁。
- 合并分支前,先用编辑器里的 diff 看一遍 Blueprint 变更。
- 大版本升级或重构关卡前,先打标签或快照。
一个 Blueprint 冲突的实例
去年我们有个战斗 Blueprint:BP_PlayerAttack。程序阿杰加了一个新的 Combo 输入,策划小王调整了一段 Montage 播放逻辑。两个人都在自己的分支上改了两周,合并时冲突。
UE 的 Blueprint Diff 打开后,左边是服务器版本,右边是本地版本。阿杰看到小王把 PlayMontage 节点换到了 Branch 后面,而他自己新增了一个 InputAction 事件。两个改动都合理,但没法自动合并。
最后我们的处理办法:阿杰接受服务器版本,然后手工把自己的 InputAction 事件和变量重新连进去。花了四十分钟。如果这是 .umap 冲突,四十分钟根本不够,可能得重做一整天。
从那以后,我们规定:凡是 Blueprint,合并前必须先 diff;凡是 .umap,必须锁。
分支策略:别把 UE 当普通软件项目
很多程序出身的人喜欢 Git Flow:feature 分支、develop 分支、release 分支切得飞起。这在纯代码项目里没问题,但在 UE 项目里会撞墙。
因为二进制资产没法合并,feature 分支一旦涉及 .uasset 或 .umap,合并到主分支时几乎必然冲突。我们团队的实践经验是:
- 主分支只放稳定资产。美术每天的小改动直接提交到主分支,并用锁保证互斥。
- 程序写 C++ 或改 Blueprint 逻辑时,可以切 feature 分支,但分支周期不要超过三天。
- 任何 feature 分支合回主分支前,必须先同步主分支最新资产,确认没有未锁的
.umap。 - 大型重构关卡前,全员停下手头关卡相关改动,统一快照。
这套规矩听着死板,但它把冲突从合并时爆发转移到了编辑前就预防。
CI 和自动检查:用脚本守规矩
口头规矩总会有人忘。我们后来在 CI 里加了两条检查:
- 每次 PR 如果包含
.umap修改,必须同时存在对应的 LFS 锁记录或 PR 描述里有锁释放说明。 - 提交内容不能包含
Saved/、Intermediate/、DerivedDataCache/里的文件。
Git 团队可以用一段简单的 pre-commit hook 做本地拦截:
#!/bin/sh
files=$(git diff --cached --name-only | grep -E '\.(umap|uasset)$')
for f in $files; do
if ! git lfs locks | grep -q "$f"; then
echo "未锁定文件: $f"
exit 1
fi
donePerforce 团队可以在提交时要求 changelist 描述里写明锁状态。把检查交给机器,比靠人记靠谱得多。
迁移 OFPA 的注意事项
如果你是从 UE4 项目迁过来,开启 OFPA 不是勾个选项就完事。
- 老关卡的 Actor 会重新序列化到
__ExternalActors__,第一次保存时会产生大量文件变更。不要在周五下午做这件事。 - 文件夹数量会暴增。Windows 默认路径长度可能不够用,建议开启长路径支持。
- 某些老插件或自定义序列化逻辑可能不兼容 External Actor,迁移前先在副本工程里跑一遍。
- OFPA 文件很小,不建议全部走 Git LFS。我们的做法是把
.uaactor和小的.uasset留在普通 Git,只把贴图、模型、音频走 LFS。
迁移后最直观的感受是:关卡设计师终于可以同时工作了。
再聊聊 DDC 的账
以我们项目为例,DDC 目录最高峰时本地有 47GB。8 个人就是 376GB 的重复数据。放到 NAS 共享后,新人第一次打开项目从 2 小时压缩到 15 分钟。这 15 分钟里大部分还是在读项目设置和加载插件。
这笔钱怎么算都值。一台普通的 NAS 加万兆交换机,成本比一个人两周工资还低。共享 DDC 是我们项目里最划算的一笔优化。
给不同规模团队的选型建议
我常被问:小团队该用 Git LFS 还是 Perforce?我的回答一贯很实际:看人头和看预算。
- 1-3 人独立团队:Git 免费、周边工具多,配合 Git LFS 完全够用。只要养成锁
.umap的习惯,冲突不会太多。 - 5-15 人中小团队:如果有预算,直接上 Perforce。省下的沟通成本和冲突处理时间,够付好几年的授权费。
- 20 人以上或有多地办公:Perforce 几乎是标准答案。配合 UnrealGameSync 和共享 DDC,能让整个团队保持在同一节奏。
Plastic SCM 适合那些卡在 Git 和 Perforce 之间的团队。它的锁比 Git LFS 稳,分支又比 Perforce 灵活,只是国内资料和托管方案少一些。
选型没有绝对的对错,只有合不合适。最怕的是项目做到一半发现工具撑不住了,再迁移一次,那才是真的伤筋动骨。
还有一点常被忽略:托管位置。Git LFS 的存储和带宽费用在资产膨胀后会变得很可观。Perforce 的授权费虽然 upfront,但大文件管理和锁的体验是算在总成本里的。不要只看第一年的账单,要算三年内的总拥有成本。
老周那晚的冲突,最后是靠 OFPA 迁移和强制锁规则解决的。三个月后,我们再也没在周五晚上因为 .umap 冲突吵过架。
版本控制工具不会替你沟通。再完美的锁也挡不住两个人商量好一起改同一个文件。真正让团队顺畅的,是每天早上的十分钟站会里,谁动过哪个关卡、哪个 Blueprint 一句话带过。工具负责兜底,沟通负责 Prevention。
版本控制这件事,技术选型只占三成,规矩和习惯占七成。选一个你们团队能坚持下去的方案,把锁、忽略规则和 OFPA 这三板斧用好,周五晚上就能准时下班。
最后多说一句:别把工具当成信仰。我见过用 Git LFS 跑得很顺的 AAA 项目,也见过用 Perforce 还天天冲突的团队。真正起作用的是人对规则的尊重。
规则执行得再死板,也比周五晚上删档重来强。选一个能睡得着的方案,才是正经事。写到这里,我又想起老周那晚的截图。希望你的群里再也不会出现那样的画面。