资源管理:重塑 UE 的包拆分方案
之前我写了一篇文章,介绍 UE 默认的拆包方式 (UE 热更新:拆分基础包),但 UE 默认的拆包方式在大规模资源项目中不够灵活,无法满足对超大规模资源项目的精细化管理需求。
为了解决 UE 默认打包和资源拆分的痛点,我基于 HotPatcherCore 开发了一个扩展,能够能够解决默认拆包流程的缺点,足够地灵活与强大。博客中有篇文章,介绍它的整体实现机制: 一种灵活与非侵入式的基础包拆分方案。
HotChunker 能够非侵入式地集成到 UE 默认的打包流程中,无需手动做任何的处理,只要 UE 默认打包,就会自动拉起 HotChunker 的拆包流程,实现资源的 Cook、打包,以及自动 Copy 到 StagedBuilds 目录中。
本篇文章会着重介绍,使用 HotChunker 在进行资源拆分的配置方式和自动化构建、最大限度地降低资源冗余、以及进行并行打包的实现。
UE 默认打包的问题
UE 默认使用的 CookerOnTheFly
-UnrealPak
的模式本身存在一些问题:
- 不能够精确地打包和灵活控制自定义拆包的行为
- 没有全平台通用的 pak 进包过滤机制(只有安卓的
ObbFilter
) - 强依赖 Cook 流程,无法单独打包某一个 Chunk
- 无法方便地进行程序化的拆包
- 无法配置 Chunk 包含 Not-uasset 文件
- 所有 Not-uasset 文件均在 0 号 Chunk
- 无法基于 chunk 拆分 Metadatas
HotChunker 的实现能够比较好的解决上面的问题,每个 Chunk 都是自包含的:
最终效果,UE 打包的资源配置,只有那些最基础的资源由 UE 自己管理打包,其余的都可以通过 HotChunker 的配置来拆分。
文章的后续部分会介绍如何配置做到这一点。
Chunk 配置
Chunk 的配置,是进行包拆分的最小单元,在 UE 中,每个 Chunk 会打包为一个独立的 pak 文件。
对于 HotChunker 的实现,单个 Chunk 的配置可以包含以下这些属性:
Chunk 的资源的配置提供了两种模式:Rule/Getter,用于指定打包的资源路径或从代码中通过程序化的方式获取。
每个 Chunk 都可以添加的外部文件,以及控制 Shader 的序列化方式、AssetRegistry 的序列化等等。并且 Chunk 的 metadta 是自包含的,最显著的就是降低安装包内 ShaderLirary 的大小。
默认情况下,UE 打包项目的 Shader 都被序列化到了同一个文件内,并且这个文件是被打包到 pakchunk0 之内的,也就是整个项目完整的 Shader 都在安装包之内。资源规模上来之后 ShaderLibrary 大小非常恐怖,可能会达到数百 M。
让每个 Chunk 的 Shader 自包含,能够比较好地解决这个问题。并且配合上我之前的一种高效的 ZSTD Shader 字典训练方案,组合起来能够最大限度地降低 ShaderLibrary 的大小,实现 Shader 的按需下载。
Rule
Rule 模式的配置和 HotPatcher 的配置类似,可以指定包含路径、资源、忽略的资源。
这种模式适合配置比较固定的资源,如打包某个目录、某个地图等。
Getter
除了直接指定资源之外,我还提供了一种 Getter 的配置模式,可以指定一个继承自 UHPLGetter
的类,从代码中动态地获取 Chunk 管理的资源。
之所以会有这样一种模式,是期望与策划的配置建立起直接的对应关系。基于策划的配置来控制打包。
比如,当前版本包含了哪几个地图、哪些职业的角色等等,这些配置是会随着开发进程不断变化的,而数据源头在策划,在 UE 默认的方式中,还需要在工程中进行修改打包配置。
但,HotChunker 通过 Getter 这种配置方式建立起打包与策划配置的对应关系,就能够策划来直接控制打包哪些资源。
Chunk 管理
HPL 资源
与 PrimaryLabel 类似,我给 Chunk 的配置增加了一个等价物,我将其称之为 HPL
(HotPatcherPrimaryLabel)。
可以创建一个 HPL 资源用于编辑要打包的资源,每个 HPL 是一个独立的 Chunk。
在打包中,需要在 Project Settings
-Plugins
-HotChunker
中添加扫描路径:
在 HotChunker 的打包中,会自动扫描所有配置的目录下的 HPL 资源,进行打包。
DataTable
如果想要创建多个 Chunk,可以创建多个 HPL 的资源,但是为了方便管理,我还支持了 DataTable 配置。
选择创建 DataTable,RoleStructure
选择 HPLChunkSetting
:
打开编辑,每个元素就是一个独立的 HPL 的 Chunk 配置:
并且我为 HPL 的 DataTable 添加了一个专门的打包按钮,可以直接在表格界面中选择打包哪个 Chunk:
它会拉起一个单独的进程执行,不会阻塞当前编辑器。
同样的,当创建了一个 HPL 的 DataTable,想要将其参与打包,需要在 Project Settings
-Plugins
-HotChunker
中添加 ImportRuleTables
配置:
动态注册
为了实现程序化自动进行分包的需要,我为拆分配置增加了动态注册的机制,只需要绑定该 Delegate:
1 | FHotChunkerDelegates::OnNotyfyChunkRegister.AddStatic(&FGameEditorModule::OnNotyfyChunkRegister); |
在该函数中将 Chunk 注册进去:
通过动态注册或者前面提到的 Getter 机制,可以把策划配置与打包建立起一种对应关系,可以完全按照策划的配置,来自动控制要打包哪些资源,解放程序的配置任务。
编辑器支持
我为 Chunk 的 DT 配置提供了编辑器选项,能够比较方便地选择打包某个 Chunk,在开发过程中,可以让美术同学自己方便地打包自己所需要的资源,避免出包的等待。
- 打包单个 Chunk
- 打包 DT 中所有的 Chunk
- 导入 / 导出 Chunk 的配置
- 不阻塞编辑器
全局配置与流程扩展
在 Project Settings
-Plugins
-HotChunker
中可以进行全局配置,和逻辑替换(ChunkerManager)。
- HPL 扫描路径
- Chunk 规则 DT 配置
- Pak Setting
- 功能开关
- 替换 ChunkerManager
- 全局重载
介入 Chunker 的各个阶段
为了实现足够的灵活性,在实现过程中,我为 Chunk 的打包留了很多可以介入到各个阶段的口子。
- 按需对资源资源分析、处理
- 获取打包状态、结果
- 获取完整的 Manifest 信息
- 进行包体审计
在框架之内,可以非侵入式地实现项目的各种分析需求。
冗余控制
我在 UE 热更新:拆分基础包 #Priority 打包时失效中介绍了通过 UE 默认的拆包形式,优先级不生效的情况,并提供了解决方案。
HotChunker 在实现时,就已经规避了这些问题,Chunk 的配置也支持 Priority
。
如果 Chunk 的 Priority 是平级的,则它们之间就没有依赖关系,统计资源会冗余一些。
打进安装包内的 Chunk 之间是没有资源冗余的,在安装包之外的 Chunk,可以优先级来进行冗余控制。
Commandlet 支持
我为 HotChunker 添加了多种 Commandlet 支持,能够比较方便地使用命令行打包。
- 打包工程里所有配置的 Chunk
- 打包一组 Chunks
- 打包单个 Chunk
- 打包某个命名的 Chunk
1 2 3 4 | -run=HotChunker -TargetPlatform=IOS -run=HotMultiChunker -config=G:/HPL/HPL_ChunkerSettings.json -TargetPlatform=IOS -run=HotSingleChunker -config=G:/HPL/ChunkerSetting.json -TargetPlatform=IOS -run=HotNamedChunk -ChunkName=XXXX -TargetPlatform=IOS |
打包控制与动态开关
Chunk 的打包控制,分为三种类型:
- bUseHPL:是否启用 HotChunker 的配置打包 chunk
- ALL:所有的 chunk
- BASEBUILDS:安装包内的 Chunk
- EXTERNAL:安装包之外的 Chunk
在项目设置中可以控制默认打包的类型
除了固定配置之外,还可以在 commandlet 传递命令来覆盖引擎中的配置,能够比较方便地在 CI/CD 上进行控制。
目前支持了两个动态开关:
1 2 3 4 | # true/false -bUseHPL= # ALL/BASEBUILDS/EXTERNAL -GenerateType= |
在拉起 UE 的 CookCommandlet 时传递,就会自动地控制 Chunker 的打包开关和生成的 Chunk 类型。
如果是自己在 CI/CD 中调用 HotChunkerCommandlet
,也可以传递这两个动态开关。
如,只打包非安装包内的 Chunk:
1 | -run=HotChunker -TargetPlatform=IOS -GenerateType=EXTERNAL |
能够比较方便地集成到 CI/CD 系统中:
通过这种机制,可以实现打包时的精确控制。在一些测试场景中,可以实现快速出包,避免每次打包都要处理完整资源。
Chunk 的加密
使用 HotChunker 方式管理并打包的 Chunk,默认复用项目配置中里 DefaultCrypto.ini
的配置。
在 Project Settings
-Plugins
-HotChunker
-HPLPakSettings
-EnceyptSettings
中配置:
bUseDefaultCryptoIni
:使用项目设置中的配置。如果不复用,可以通过 CryptoKeys
来指定 crypto.json 进行加密。
通用的包过滤方案
在 UE 热更新:拆分基础包这篇文章里,我介绍了 Android 和 IOS 包过滤的方法。
引擎默认只为 Android 提供了 ObbFilter
,但没有为 IOS 提供等价物,需要自己实现。
在 HotChunker 的实现中,我对包过滤的机制做了补充,可以在创建每个 Chunk 时,直接指定它是否进安装包。
并且,可以针对某个平台特殊处理:如,默认情况下进基础包,但在 WindowsNoEditor 平台不进。
如果不想要对每个 Chunk 都单独配置,可以在项目设置中配置全局重载:
这个配置意味着,在打包 WindowsNoEditor 平台时,HotChunker 所有管理的 chunk 都会归档到安装包之内。
流程优化
默认情况下,UE 使用 CookerOnTheFly 的过程,是要把当前工程中所有参与打包的资源都执行 Cook 之后,才进入到 Pak 打包过程。
本质上是一个串行的过程:
但是当资源规模庞大之后,这个过程非常之久。
但实际上,我们并不一定需要把所有的资源处理完,才能打安装包。因为在手游中,安装包内的资源只是一少部分,其余的资源都是热更动态下载下来的。
在 HotChunker 的实现中,可以把安装包内 Chunk 的打包与动态下载的 Chunk 进行并行执行:
这样就能够节省大量的时间,用最快的速度打出安装包,再用一个独立的打包任务并行执行动态下载的 Chunk 的打包。
结语
基于 HotChunker 的包管理方案,是一种强大和精确地包管理机制。能够在精确拆分资源的同时,保证配置的灵活性,并且支持强大的 Commandlet 能力。能够方便地非侵入式扩展流程和进行资源审计。
而且,该方案是非侵入式集成,无需对默认的打包过程做任何处理也不需要修改引擎,整个实现都是插件化的,只要启用插件后拉起 UE 默认的打包就会自动执行。