1. Cook失败这种事,为什么我劝你先冷静下来
做UE4项目的人,多少都在某个版本节点上栽过Cook这道坎。明明编辑器里跑得飞起,PIE也正常,一到打包就给你甩一脸错误日志,而且往往是项目临交付或者要做QA构建的时候,错误来得最密集。这篇我记录的是一次完整的Cook资源失败排查过程,从崩溃开始,到定位、拆解、逐个击破,最后总结出一套可复用的处理流程。
先说个基本认知:UE4的Cook本质是把工程里的资产打包成目标平台可读取的中间格式,再结合关卡、代码、Shader编译结果,最终生成Pak包。这个环节里跑的是编辑器逻辑的子集,所以Cook失败并不等于你的项目“坏了”,更多时候是资源配置不合理、引用不规范、或者是构建环境出了一些隐藏问题。
适合读这篇内容的人,我猜是这些:
- 已经能正常启动UE4项目,但一执行Build就报错,日志看不懂
- 发版前频繁被Cook错误打断,每天盯着输出窗口怀疑人生
- 想给团队整理一套查Cook报错的SOP,但手头缺少一份系统性的排查链路参考
如果你符合其中任何一条,这篇记录应该能帮你少折腾一两天。这里面没有什么玄学,所有报错背后都有对应的原因,排查链路是固定的,关键在于你有没有一套自己的方法。
2. 先搞清楚Cook失败的本质,再动手删资源
2.1 编辑器里没事,为什么Cook就炸了
这是很多人问的第一个问题。UE4编辑器加载资源,用的是开发态格式,贴图是原图,模型是原始网格,蓝图有完整的节点图。Cook出来之后要落地到某个具体平台,比如Windows、Android或iOS,资源格式要转换、压缩、序列化,Shader要在编译后编进资源包里,蓝图节点要生成可执行的字节码。
所以你会发现Cook失败的报错,很多根本不是“资源缺失”,而是:
- Shader编译不过去:某个材质在特定平台下编译出非法指令
- 纹理格式不被目标平台支持:比如Android上用了RGBA16F且没有Fallback
- 蓝图字节码生成失败:往往是某个节点在Cook阶段执行了EditorOnly逻辑
- 资源引用链断裂:Cook到一半,某个引用关系找不到对应对象
2.2 Cook失败的几种类型,决定你该怎么查
我习惯把Cook失败分成三类,每一类的排查方式完全不一样。
| 失败类型 | 典型现象 | 排查方向 |
|---|---|---|
| 单个资产失败 | 日志里明确指向某张贴图、某个模型、某个关卡 | 优先在编辑器里单资产验证Cook |
| 依赖链失败 | 报错对象是A,但根因在B,B是A引用的 | 查引用关系,打断依赖链 |
| 环境性失败 | 同一份代码,别人机器能过,你机器不能过 | 查缓存、共享内存、磁盘剩余、路径深度 |
第一类最简单,直接在编辑器里对报错资产执行单独的Cook或重存,基本能复现。第二类最隐蔽,经常让你误判。第三类最气人,因为代码和资源都没问题,纯粹是环境问题。
3. 从日志里挖出真正的元凶:我的完整排查链路
3.1 日志要看哪几行,怎么看
遇到Cook失败,第一件事不是去翻Content目录,而是先拿一份完整日志。Windows下UE4的Cook日志默认在项目目录/Saved/Logs/目录里,文件名一般是烤制时的时间戳加Cook前缀。如果你在Build任务里跑了Cook,那日志会嵌入整个构建日志里。
我习惯用编辑器里的Output Log配合命令行同时跑,因为Output Log有实时过滤,而命令行日志保留了最原始的堆栈信息。拿到日志之后,我只看三类东西:
- Error级别的行:这是直接告诉你什么失败了的。
- Warning配合Error附近的行:很多失败是Warning触发出来的连锁反应。
- Cook命令的任务编号:UE4的Cook日志会显示当前正在处理哪个包、第几个资产,顺着这个编号往前翻,能定位到是哪个批次崩的。
3.2 一次真实崩溃的日志定位过程
这次我遇到的情况,日志最后几行是这个样子:
LogCook: Warning: Unable to find package for cooking 'CharacterBase' requested by 'Map_Test01' LogCook: Error: Cook failed. LogCook: Display: 0 errors, 12 warnings.很多人看到这行就开始慌,心想“CharacterBase是不是被删了?”但注意看,警告说的是“Unable to find package for cooking”,意思是Cook系统在序列化某些对象的时候,找不到CharacterBase这个包。这不代表它在磁盘上不存在,而是它没有被加入这次Cook的依赖收集范围。
我接下来执行了命令行Cook单跑:
UE4Editor-Cmd.exe 项目.uproject -run=cook -targetplatform=Windows -map=Map_Test01 -iterate -unversioned -log这里有几个参数解释一下:
-run=cook是固定调用Cook命令-targetplatform=Windows指定目标平台-iterate只补Cook有变动的资产,别全量跑-unversioned不生成版本号附加信息,调试更快-log强制输出完整日志
单跑之后立刻复现,输出的信息比合入Build的日志多了几十行,最终锁定的SQLite报告里显示,问题出在CharacterBase蓝图里挂了一个继承自ActorComponent的C++类,但这个类在目标平台下没有实现。
3.3 日志看不懂时,直接开时间戳
UE4的Cook日志有个让人恼火的问题,就是它的输出顺序不一定和实际执行顺序完全一致。尤其多线程Cook开启后(默认开启了),同一时间有多个资产在处理,日志会交叉打印,你根本排不出先后。这时候别硬看,直接在命令行里加:
-logtimes它会在每行日志前打上[时间戳][帧号]格式的前缀,用记事本打开后按时间排序,整个逻辑链就清晰了。我这次排查花了很长时间,后来发现有一张4096x4096的贴图在Android平台下压缩,耗时远高于其他资产,导致整个Cook进程看起来像卡死。如果不加这个参数,你很难判断“慢”和“挂掉”的区别。
4. 材质与贴图类失败:最常见也最容易被误判
4.1 Shader编译失败的一个隐蔽原因
Cook过程中材质编译是重头戏。如果某个材质在编辑器里看起来一切正常,但是Cook时Shader编译报错,十有八九是材质表达式里用了某个平台不支持的节点或其组合方式不合理。
我这次遇到的是Landscape图层材质,里面用了一个World Position Offset节点去做顶点动画,但目标平台是Android。这个节点本身在Android也能用,问题是我在同一个材质里混用了Pixel Depth Offset和Vertex Color,两个节点在移动端组合会产生不可预期的编译行为。
排查方法是把一个复杂材质里的节点逐个禁用,每次只保留一个变量,然后重新Cook,直到找出哪两个节点组合会报错。这个过程很枯燥,但有效。
4.2 纹理压缩失败的识别信号
日志里如果出现Unable to compress texture ...之类的提示,首先去检查纹理的压缩设置。UE4里长宽不是2的幂次时,某些压缩格式(比如BC7)无法处理,会回退到RGBA8,在某些平台上是合法回退,在另一些平台会直接报错。
还有一点容易忽略:贴图的Source纹理尺寸超过4096后,Android的OpenGL ES 3.0部分驱动不支持4096的Cubemap,或有最大纹理尺寸限制。日志里如果只显示某一帧纹理压缩失败,但实际上Cook流程因为这个中断了,可能就是这种。解决方式是在纹理导入设置里勾选“Power Of Two Mode”为PadToPowerOfTwo或设置MaxTextureSize为2048,对性能影响很小,但Cook稳定性大幅提升。
4.3 UV错误引发的烘焙问题
有些贴图类失败不是压缩的事,而是模型UV超出0-1范围。Cook阶段虽然不会执行渲染,但会把模型的静态光照贴图信息烘焙出来,UV超出范围而且没设置Wrap模式时,光照贴图通道会写入非预期数据,最终反映为Cook报错。
这种问题往往在编辑器里不提示,只有Cook时才炸。处理方法是选中对应StaticMesh,在Mesh Settings里检查UV Channel 1是否在0-1范围内,配合UV Channel Editing工具修正。
5. 依赖链断裂:最让人头秃的一类Cook失败
5.1 蓝图里的硬引用,Cook收集不到的三种场景
这次排查还连带修了两个依赖链问题,都属于“编辑器里正常,Cook时找不到对象”的典型。整理一下:
- 动态加载用字符串路径:
LoadObject或StaticLoadObject运行时传入的资源路径,Cook收集器不一定能识别出来,因为它默认通过硬引用关系收集资产。字符串路径属于软引用,必须有对应的AssetRegistry扫描支撑。 - 蓝图里通过变量暴露的对象引用被标记为Transient:某些变量在Details面板中勾了Transient后,Cook序列化时会被跳过,但运行时又需要它,于是出现“Cook成功但运行时崩溃”,和严格意义上的Cook失败不同,但排查思路类似。
- C++里使用
ConstructorHelpers::FObjectFinder引用的资源只存在于某个插件,但插件未启用:Cook收集器扫描依赖时,插件未启用会导致整个包都不进入收集范围。
5.2 用Reference Viewer批量排查依赖链
排查依赖问题时,我习惯优先用Content Browser的Reference Viewer(在资源上右键 → Reference Viewer),勾上“Show Soft References”和“Show Hard References”,能直观看到资源间的引用图。
但这东西有个局限:它只展示编辑器已经加载进来的引用。如果你想在无UI环境下精确知道一个资产的引用链,用命令行更靠谱:
UE4Editor-Cmd.exe 项目.uproject -run=AssetRegistry -mode=query -object=/Game/Characters/CharacterBase -property=Dependencies这个命令会直接输出CharacterBase的所有依赖包路径,拿它和你Cook失败时的报错路径做对比,缺失的依赖项一目了然。比在编辑器里一个个点高效很多。
5.3 修复依赖链之后的“假成功”坑
修复完成后别忘了验证一下Cook是否真正通过。有些时候日志显示“0 errors”,但Pak包是残缺的。我的验证方法是用命令行执行一次加载测试:
UE4Editor-Cmd.exe 项目.uproject -run=LoadPackage -map=/Game/Maps/Map_Test01 -log如果这个命令没有任何Error,才说明依赖链真的补全了。否则哪怕Cook报告成功,也只是把缺失的引用跳过了,运行时大概率会崩。
6. 构建机环境的隐性炸弹:内存、路径、缓存与并发
6.1 路径超长问题,Win10下你防不胜防
如果你的项目放在D:\GameProject\Client\Main\UE4\ProjectName\Content\...这种层级比较深的目录里,Cook到一半很有可能突然报一堆“Cannot find file”或“Create directory failed”的错误。这不是文件丢了,而是**Windows的MAX_PATH限制(260个字符)**撞上了UE4的Cook临时目录。
UE4的Cook会往项目Saved目录写入大量中间文件,路径一长就爆。解决方式很粗暴但有效:把项目移到浅目录,比如D:\UProj\你的项目名\,或者开启Windows长路径支持(注册表里设置LongPathsEnabled为1,重启)。两种方式我都在项目里用过,前一种更稳妥,因为即使你开启了长路径,某些三方库底层还是调用的旧API,照样会炸。
6.2 缓存损坏:同一个锅,背了两次黑锅
Cook缓存有多坑,我觉得值得单独讲一下。UE4的Cook缓存分为两级:一是Saved/Cooked目录的成品缓存,二是Saved/DerivedDataCache(DDC)的中间数据缓存。这次项目里我遇到的诡异现象是:第一次Cook失败,清理了资源后重新跑,还是同样的错误,而且错误路径指向一个已经不存在的资产。
最后我把Saved/DerivedDataCache整个目录删了,再Cook一次,居然就过了。原因在于DDC里保留了旧的资产输出记录,Cook系统在做增量更新时,以为这些旧资产还是最新的,就直接复用了缓存结果,而那个缓存本身是坏的。这个坑非常隐蔽,因为你修了资源、修了引用,但DDC还在用失效数据。
提示:遇到“删了资产还在依赖它”的诡异Cook错误,先删除
Saved/DerivedDataCache目录再试一次,成本极低,不要一上来就动项目配置。
6.3 并发Cook的线程数调整
某些资源在Cook时有状态冲突,特别是一些自定义C++类里的静态变量,如果多个线程同时访问同一个全局数据,就会产生竞态条件,表现出来的就是随机性很强的Cook失败——你重跑一次,崩的资源都不一样。
遇到这种情况,先别急着改代码,试试限制Cook线程数:
UE4Editor-Cmd.exe 项目.uproject -run=cook -targetplatform=Windows -CookWorkerCount=1 -unversioned -log默认情况下UE4会按CPU核心数启动多个CookWorker。调成1以后,很多因为并发导致的随机失败直接消失。如果你的项目是在验证阶段,用单线程跑一次确认问题根因,之后再有针对性地修复代码里的静态数据访问问题。
7. 提升Cook效率,减少无谓失败的三个习惯
7.1 每次交互前,先做一次无UI的Cook验证
很多项目是美术、策划、程序共用一套工程。美术在编辑器里操作,改了很多资源,但他不知道某个改动会让Cook失败。如果等到正式构建时才暴露,后端工程师就要在一个巨大日志里捞针。
我自己的习惯是,每天至少跑一次无UI的Cook验证,命令就是前面提到的基础命令,不需要打最终包,只要Cook能出Pak或中间产物就算过。这套验证可以在本地跑,也可以在构建机上定时跑。尽早发现问题,排查成本最低。
7.2 善用Cook命令的迭代模式
平时开发中,全量Cook非常浪费时间。除非你要出最终验证包,否则我都用-iterate模式。它根据资源时间戳和DDC缓存来决定哪些资产需要重新Cook,其余直接跳过。实测下来,一个Asset中等的项目,全量Cook可能要30分钟,-iterate模式常常5分钟以内就完成了。
用它的前提是确保你的工程版本的资源时间戳是准确的。如果某个环节统一修改了所有资源的时间戳(比如从版本控制里批量checkout),那-iterate会退化成全量Cook,但即便如此,也不会出错,只是慢。
7.3 给构建机设置独立的构建账户
构建机和开发机应该分开,这是一条相对更实用的建议。在开发机上跑Cook,你背后还开着IDE、浏览器、通讯软件,内存占用大了以后,Cook会莫名资源不足,然后报一些“Out of memory”或“Unable to allocate”的错误。构建机用独立账户,跑构建任务时其他服务全停,Cook的稳定性大幅提升。
另外,构建机上的杀毒软件和索引服务也建议把项目目录加入排除列表。我遇到过不少次,Cook失败是因为某个dll或中间产物正在被杀毒软件扫描锁定,导致文件写入失败。
8. 个人体会:Cook排障,本质上是资源意识的较量
Cook失败的排查没有太多捷径,但如果你能做到下面三点,大部分问题都能在半小时内定位:
- 拿到日志先分类型,不盲猜
- 善用命令行工具,把问题缩小到最小复现范围
- 遇到诡异情况,先清DDC缓存再做其他尝试
我自己的项目组从建立这份排查流程之后,Cook失败的平均定位时间从一整天缩短到了两三个小时。现在新来的同事遇到打包报错,第一反应不是去群里喊人,而是自己拉日志、查类型、跑单测,效率提升非常明显。
最后再分享一个小工具吧,UE4自带的命令行里有个-logtimes参数,我前面提过一次,这里再重复一遍是因为它真的救命。项目组的所有打包脚本里我都加了这一项,出了问题直接把日志按时间轴展开,谁先谁后一目了然。没有它,我大概还会在那些交错的Cook日志里多挣扎很多次。