技术博客

Pak 与 Chunk:PrimaryAssetLabel 的分包规则

第 84 / 100 篇 · 2025-10-22 · ue5 · 资产管线打包资产分发

Cook 完只是得到一堆"熟"资产,打包(Staging)阶段还要回答一个问题:这些资产分几个包装?装错了,DLC 更新会把基础包重新下发一遍,补丁体积直接爆炸。这一篇讲 UE5.3 的 Chunk 划分规则和 pak 的挂载模型,入口在 Programs/AutomationTool 的 staging 代码和 FPakPlatformFile(Runtime/PakFile)。

Pak 的文件格式与挂载模型

一个 .pak 是"文件系统快照":FPakInfo(魔数、版本号、索引偏移)固定在文件尾部,索引(文件名 → 偏移/大小的映射表)可以整体加密。挂载用 FPakPlatformFile::Mount(Path, PakOrder),关键是 PakOrder——它不是加载顺序,是优先级:

  • 文件查找按挂载优先级从高到低逐个 pak 问"你有没有这个路径",第一个答有的赢;
  • 同一路径可以在多个 pak 里各存一份,高优先级的覆盖低优先级的,这就是补丁机制的运行时形态(第 9080 篇展开)。

所以"分几个 pak"在运行时语义上等价于"分几层命名空间":层与层之间用路径重叠做覆盖,层内部是纯粹的空间划分。

Chunk:分层的来源

打包时资产 → Chunk 的映射由 AssetManager 的规则体系产出,核心工具是 PrimaryAssetLabel(一种资产类型,放在 Content 里就是一个分包规则)。一个 Label 声明三件事:管哪些资产(ExplicitAssets / ExplicitAssetsDirectories / AssetCollection)、分到哪个 ChunkId、规则强度(CookRule)。

聚合规则按"主资产"(PrimaryAsset,如地图、游戏功能块)展开:一个主资产被 Cook 时,它的依赖闭包默认跟着进同一个 Chunk。于是自然的划法是:

  • Chunk 0:主游戏(启动地图 + 未被任何 Label 接管的资产);
  • 高编号 Chunk:DLC、语言包、按需下载内容,各自配一个 Label 和一组目录;
  • 引擎内容默认并入 Chunk 0/1 一带,补丁敏感的项目会把引擎内容也单独拆 Chunk,避免引擎升级牵动主包。

CookRule 里值得记住的是 AlwaysCook 一族:有些资产不被任何主资产引用(比如软引用才能触达的)但必须在包里,用 AlwaysCook 强制进 Cook 集合并钉进指定 Chunk。漏配的典型症状是"编辑器里好使、打包后资源缺失",排查方向就是 CookRule 和软引用闭包。

UE5 的现实:容器化的 Chunk

UE4 时代一个 Chunk 输出一个 .pak;UE5 默认走 IoStore,一个 Chunk 输出一对 .utoc/.ucas(第 9020 篇的格式)。概念不变——ChunkId 还是分配单位——只是容器内部从"文件树"换成了"内容块"。补丁 diff 也在这一层做:基线与补丁两个 Cook 的产物按 Chunk 对账,变化的容器才进补丁。

体积账可以这样算:设一次内容更新改了资产集合 $\Delta$,这些资产落在 Chunk 集合 $C(\Delta)$ 里,下发体积约为

$$S_{\text{patch}} \approx \sum_{c \in C(\Delta)} S_c$$

注意求和是对Chunk而不是对变化的资产——同一个 Chunk 里任何一点改动,整个 Chunk 的容器都得重发(IoStore 内部按 block 复用能省一部分,但容器头部的变化不可避免)。所以"补丁小"的充分条件是改动被隔离在尽量少的 Chunk 里,这正是 Label 划法的优化目标:把"高频改动的内容"(活动资源、平衡性数据)和"稳定大内容"(地图、美术主体)分开放。

从资产到 Chunk:规则聚合 → 容器产出 → 运行时按优先级叠加 PrimaryAssetLabel 目录 / 集合 + ChunkId 主资产依赖闭包 被引用 → 跟随同一 Chunk AlwaysCook 强制集 软引用可达但未 Cook Chunk 分配表 Chunk 0:主游戏 Chunk 10:DLC-A Chunk 20:语言包 一资产一 Chunk,先到先占 chunk0.utoc/ucas chunk10.utoc/ucas chunk20.utoc/ucas 运行时:FPakPlatformFile 按 PakOrder 从高到低查路径 同路径高优先级覆盖低 → 补丁挂高 Order 即完成热更 补丁体积 ≈ 改动触及的 Chunk 容器体积之和

规则冲突:先到先占

一个资产被多个 Label 瞄准时按什么分?UE 的策略是先到先占:遍历顺序里先处理到的规则赢,资产只进一个 Chunk(依赖闭包跟随其主资产)。这意味着划 Chunk 时要把"大归属"想清楚再动手:DLC 地图依赖了主游戏的某个材质,那个材质归 Chunk 0(先被主游戏认领),DLC 的容器里不会重复它——运行时靠挂载所有 Chunk 的容器拼出完整视图。反过来,如果主游戏 Label 写得太宽(比如把整个 Content 目录圈进去),DLC 想隔离都隔离不了。

实操清单

  • 划 Chunk 前先列出"更新频率谱":每次热更都动的东西(配置、活动)→ 单独低编号之外的 Chunk;基本不动的大头(地图、角色)→ 稳定 Chunk;引擎内容视补丁策略决定要不要拆;
  • Label 里用目录而不是逐资产枚举,否则资产搬家就掉出 Chunk;
  • 打包后验证:UnrealPak -List 逐个容器看内容归属,和预期分配表对账;-fileopenlog 跑一次完整流程,看运行时实际打开了哪些包,反向检查有没有东西落错 Chunk;
  • 补丁体积回归:CI 里固定一条"基线 Cook + 改一个资产 + 补丁 Cook"的流水线,输出补丁体积,涨了就是划法退化,比发布后才发现便宜得多。

Chunk 划法在项目早期看起来无关紧要,等第一个 DLC 或第一次热更上线再改,成本就是全量重打包加全量下发。把"改动隔离"当成一条设计约束,和架构设计同步做掉。