UE5.3 里 Cook 的入口收敛在 UCookOnTheFlyServer(Editor/UnrealEd/Private/CookOnTheFlyServer.cpp)——名字带 OnTheFly,但 CookByTheBook(一次性批量 Cook,CookCommandlet 驱动)和 CookOnTheFly(按请求边玩边 Cook,主要给迭代开发用)共用同一套内核,区别只在队列怎么喂:ByTheBook 启动时把初始集合全算出来塞进去,OnTheFly 是运行中动态追加。这一篇走 ByTheBook 的完整流程。
初始包集:Cook 范围的定义
依赖遍历得有个起点。ByTheBook 的初始队列来自三个来源的并集:
- 显式地图:
-map=参数和 Project Settings 里的地图清单; - 启动包:引擎启动必需的包(game singleton、默认材质这类,配置在 ini 的 startup 段);
- AssetManager 的主资产清单:所有 PrimaryAsset 按 CookRule 展开——
AlwaysCook的直接进队列,其余等被引用。
用图论的话说,Cook 集合是可达性闭包:以初始集为源点、以"硬依赖 + 生效的 CookRule"为边做遍历。硬依赖就是第 9030 篇讲的 Import 表——包 A import 了包 B 的对象,B 必须在 Cook 结果里,否则加载时 CreateImport 找不到符号。软依赖(FSoftObjectPath)不强制 Cook,只在运行时解析,解析不到就是"打包后缺资源"的经典事故。
遍历本身没什么好神秘的,广度优先加去重,代价 $O(V + E)$。真正的工作量在"边从哪来":每条边要查 AssetRegistry 的依赖缓存(GetDependencies,UE5 起依赖按类型分硬/软/可选),几万资产的项目的 Cook 预热时间大头在这里,而不是在遍历循环本身。
队列消费:分层保存
初始集合进队列后进入保存阶段。两个关键机制:
- SCC 分组:包之间的循环依赖(A 引 B、B 引 A,蓝图互引很常见)构成强连通分量,同 SCC 的包必须一起保存——分批存会出现"先存的引用了后存的还没生成的导出"。Cook 用 Tarjan 之类做 SCC 缩点,缩点后的 DAG 才有拓扑序;
- 分层并行:拓扑序把 SCC 分层,同层之间无依赖,可以并行保存——这是 MPCook(多进程 Cook,第 9020 篇)的并行粒度来源。每层的串行时间才是加速比的上限约束:$T \ge \max_{\text{层}} T_{\text{layer}}$,所以依赖图"扁而宽"的项目并行收益大,"深链"项目(一串蓝图继承到底)收益小,瓶颈在最长链。
保存动作本身走 UPackage::Save 的目标平台路径(FSavePackageArgs),序列化上下文挂上 Cook 标志(ArIsCooking 一族),后面所有"剥离"决策都读这个标志。
EditorOnly 剥离发生在写盘路径上
很多人以为剥离是"Cook 前删掉编辑器对象",实际是保存时跳过:属性序列化入口(FProperty::SerializeItem 路径)检查 CPF_EditorOnly 标志,Cook 上下文直接不写;对象层面,编辑器专用的 UObject(缩略图、Ubergraph 节点)根本不进 cooked 包的 Export 表。所以:
- 剥离是保存的副作用,不是独立的清理 pass——不存在"剥离前的中间产物"可以给你抓;
- Blueprint 的节点图属于 EditorOnly 数据,Cook 后只剩编译出的字节码,这也是为什么 cooked 包不能当编辑器资产用(第 9030 篇的不可逆性);
- 有例外开关:
SAVE_KeepEditorOnlyCookedPackages一类标志保留 EditorOnly 数据(调试和某些平台需要),体积代价自负。
对应的数学事实:Cook 是有损变换 $f: \text{EditorData} \to \text{CookData}$,$f$ 不满射——编辑器数据空间远大于 Cook 数据空间,不存在逆映射。所有"拿 cooked 包反推编辑器资产"的工具都是在猜,不是在还原。
增量:对上一次的 diff
全量 Cook 每次重算所有派生数据,增量 Cook 靠"和上一次 Cook 的输出比":Cook 完成后会把每个包的产出元数据(内容哈希一类)落盘,下次 Cook 时逐包对账,没变的包跳过保存、直接从缓存(Zen,第 9050 篇)取产物。这里有个细节:对账的粒度是包,不是对象——包里任何一处改动,整个包重存。所以 Blueprint 拆分要按改动频率切,一个天天改的数据结构挂在几百 MB 的蓝图包里,等于每次 Cook 都重存几百 MB。
排查 Cook 异常的入口
- Cook 集不对(该 Cook 的没进):查 AssetRegistry 依赖缓存是不是过期(编辑器里强制全量刷新资产注册表再 Cook),查软引用是否标了强制 Cook;
- 保存阶段卡死或爆内存:大概率是巨型 SCC——两个互相引用的"万包大户"会把整层变成单点,Insights 里看保存线程的并行度,一层只有一个任务就是这个症状;
- cooked 包体积异常大:开包对比编辑器版,看是否有 EditorOnly 数据漏剥(某个自定义
UStruct忘了标 EditorOnly,或者走了 KeepEditorOnly 标志)。
Cook 流程本身不复杂,复杂度全在依赖图的形状上。把"初始集、硬依赖、SCC、分层"四个概念钉死,看 MPCook 日志和 Cook 报告时就不再是玄学。