每个游戏都是由种类繁多的资源构成,例如:网格、材质、纹理、着色器、动画、音频等。导入并管理这些资源文件,是游戏引擎必备的能力。资源管理包含两部分:离线管理和运行时管理。
在 Unity 2019 LTS 中,对应的解决方案分别是 Asset Import Pipeline v2(下文简称 AIP v2) 和 Addressable Asset System。本文主要讲解 AIP v2 部分, 包含 v2 相对于 v1 的改进之处,和完整的资源导入流程。
关键词:Asset Import Pipeline v2,Unity Accelerator,Asset Revisions,Asset Database
1. Asset Import Pipeline v2
资源导入是 Unity 中最耗时的工作流程之一,是 AIP v1 的遗留问题。针对其中最耗时的两个阶段——新资源的导入和平台切换,AIP v2 分别提供了对应的解决方案。
1.1 新资源的导入
第一次拉取项目时,项目中没有 Library 文件夹,这意味着 Assets 文件夹和 Packages 文件夹中的所有资源都需要经过 AIP 的导入处理,这步非常耗时。相较而言,直接拉取导入结果,耗时就要少几个数量级。
Unity Accelerator
AIP v2 给出的方案是 Unity Accelerator,一种基于 Unity Collaborate 的局域网代理和缓存服务。原理是:团队中有一人导入完资源,导入结果被自动缓存到 Accelerator,团队里的其他成员在导入相同版本的资源时,Unity 首先检测 Accelerator 上是否已经有对应的导入结果,如果有就下载。因为是本地网络,带宽不是问题,所以在资源较多的项目中,下载时间要远少于导入时间。
在 Unity 给出的测试中,一个大小100MB、数量12000个的项目资源,Untiy 的导入时间为6分钟,Accelerator 的导入时间为30秒,节省了90%以上的资源导入时间。然而实际项目中,12000个资源的空间占用一般要大于100MB,因此该测试仅供参考。
Unity Accelerator 基于 Unity Teams Advaned,按团队人数收费。目前提供30天免费试用,25GB云存储,3人团队的价格是60元/月,每增加一名团队成员的价格是48元/月。
1.2 平台切换
在 AIP v1 中,Library 文件夹中缓存的导入结果以资源的GUID作为文件名。这天然的导致了一个资源只存在一个导入结果,因此每次切换平台都需要重新导入所有资源。这步非常耗时,在大型项目中甚至需要数个小时。
在 AIP v2 出现前,临时的解决方案是每个平台分别对应一个项目。缺点是要占用多份项目的硬盘空间,以及每个项目需要单独拉取更新。
Asset Revisions
AIP v2 给出的解决方案是:不使用资源的GUID作为导入结果的文件名,而是使用资源所有依赖的哈希值作为文件名,而平台正是资源的依赖之一(资源依赖的说明见下文4.3)。于是每个资源因为依赖的不同,而拥有多个版本的导入结果,它们被称为资源的修订版(Asset Revisions)。因此 Asset Database 中可以同时缓存多个平台的导入结果,当切换至已经有导入结果的平台时,就不需要再经历耗时的导入处理。
AIP v2 同样存在占用硬盘空间大的缺点,但是相较于每个平台分别对应一个项目,占用空间要小很多。对此 AIP v2 也做了优化处理:只保存每个资源仅平台依赖不同的修订版,其他依赖导致的不同修订版,在 Unity 每次重启时会被删除。
2. 源文件 Source File
Source File 是指由 Unity 之外的软件创建并导出的资源文件。常见的有:图片(png、jpg、psd)、音频(wav、ogg、mp3)、视频(avi、wmv、mp4)、模型(fbx、obj、c4d、blend)、字体(ttf、otf、ttc)、着色器(cg、hlsl)、文本文件(txt、xml、json、csv)、代码文件(dll、jar、cpp、c、mm、dylib)等。
仅仅将 Source File 放入项目文件夹中,并不代表它成为了项目的一部分。只有经过 AIP 的导入处理,Soure File 转变成 Asset, 才能为项目所用。
导入处理将 Source File 转换成 Unity 能够处理的格式。这些格式往往硬件友好,能够让CPU、图形、音频等硬件立即处理。例如:Unity 在运行时不会直接使用 .png 格式的图片,而是使用经过导入处理后的某种具体格式的 texture 文件。
Pipeline 确保了导入结果的确定性,即:相同依赖的 Sourece File 经过导入处理,在任意时刻、任意设备得到的 Asset 是相同的。
3. 资源数据库 Asset Database
Asset Database 包含了两层含义。一层指 Source Files 经过 AIP 导入处理后的数据文件集合,即项目 Library 文件夹中的内容,又称为 Asset Cache。Unity 在项目运行和打包时,使用的正是这里面的数据文件。
另一层指 Unity 封装的 AssetDatabase 类。该类提供了对于 AIP 最重要的一个方法 Refresh,以及一系列方便安全的项目文件操作方法,例如:Contains、CreateAsset、CreateFolder、RenameAsset、CopyAsset、MoveAsset、DeleteAsset、LoadAssetAtPath等。
4. 刷新 Asset Database
将 Source File 放入项目 Assets 文件夹中时,或通过 Package Manager 导入 Package 时,AIP 检测到新资源,并执行导入处理。这一步的本质是触发了 AssetDatabase 的 Refresh 方法。
以下三种情况会触发 AssetDatabase 的 Refresh 方法:
- Unity Editor 重新获得焦点(如果开启了 Auto-Refresh)。
- 点击 Assets > Refresh 菜单。
- 在 C# 代码中直接调用 Refresh 方法;或通过调用 CreateAsset、ImportAsset 等方法,间接触发 Refresh 方法。
AssetDatabase 的 Refresh 方法将按顺序执行以下步骤:
- 寻找变更的 Assets,并更新 source Asset Database(Library 文件夹中)
- 导入并编译代码相关文件
- 重载脚本域
- 导入非代码文件
- 热重载
- 更新 Artifact Database(Library 文件夹中)
接下来将对上述步骤做较详细介绍,其中某些步骤会被拆分为几个小步骤。
4.1 寻找变更的 Assets
当触发了 AssetDatabase.Refresh() 方法后,Unity 开始扫描项目中 Assets 文件夹和 Packages 文件夹中的每个资源。与上次扫描相比,如果资源出现了新增、变更或删除,则将它添加到一个资源变更列表中。
4.2 更新 source Asset Database
计算资源变更列表中的每个资源的哈希值,并且根据它们的GUID,更新 source Asset Database,并从列表中删除对应资源。
4.3 依赖追踪(Dependency tracking)
资源的依赖(Dependencies)是指所有能够影响导入结果的数据。资源的 Source File 是最原始的依赖,资源的导入设置、目标平台等也都是依赖。
资源变更的本质,是资源依赖的变更。AIP 追踪了每个资源的所有依赖。资源的任意依赖发生变更,都将引起资源的重新导入,生成新的导入结果。
资源变更的结果,是不同版本的资源缓存。AIP 维护了项目所有资源的导入结果,这些导入结果缓存在 Library 文件夹中,又叫做资源缓存(Asset Cache)。同一个资源的不同导入结果,是不同版本的资源缓存,又称为资源的修订版(Asset Revisions)。
4.4 导入并编译代码相关文件
Unity 从资源变更列表中收集与代码相关的文件,递送给脚本编译管线(script compilation pipeline)。然后编译器根据脚本文件和 assembly definition 文件生成程序集(Assemblies)。
4.5 重载脚本域(Reload the domain)
域重载(Domain Reloading)处理会完全重置脚本的状态,包括重置所有静态字段和静态事件句柄。任意脚本的变更,都会重载脚本域。为了确保自定义导入器(Scripted Importer)起作用,这步将重启 AssetDatabase.Refresh() 方法。
4.6 导入非代码资源
AIP 基于资源文件名的扩展名识别文件类型,再基于文件类型使用对应的导入器(Asset Importer)处理资源。例如:TextureImporter 负责导入 jpg、png 和 psd 等图片资源。导入器分为两种:原生导入器(Native Importers)和自定义导入器(Scripted Importers)。
4.6.1 原生导入器(Native Importers)
Unity 内置了很多原生导入器,支持大多数资源类型,例如:图片、音频、视频、模型、材质、着色器、字体、文本文件、代码文件等。
4.6.2 自定义导入器(Scripted Importers)
对于新的资源类型,我们可以编写自己的导入器。对于 Unity 已经提供了导入器的资源类型,也可以使用自定义导入器覆盖原生的导入处理。
部分 Unity 内置的导入器也属于自定义导入器,因此这些导入器的处理,发生在自定义导入器的处理阶段。例如:StyleSheetImporter(uss 文件)、UIElementsViewImporter(uxml 文件)。
4.7 预处理(Preprocess)和后处理(Postprocess)
在导入阶段,有许多回调方法,可用于资源导入的预处理和后处理。
预处理方法有:
- OnpreprocessAsset
- OnpreprocessAnimation
- OnpreprocessAudio
- OnpreprocessModel
- OnpreprocessSpeedTree
- OnpreprocessTexture
后处理方法有:
- OnAssignMaterialModel
- OnPostprocessAnimation
- OnPostprocessAssetbundleNameChanged
- OnPostprocessAudio
- OnPostprocessCubemap
- OnPostprocessGameobjectWithAnimatedUserProties
- OnPostprocessWithUserProperties
- OnPostprocessMaterial
- OnPostprocessMeshHierarchy
- OnPostprocessModel
- OnPostprocessSpeedTree
- OnPostprocessSprites
- OnPostprocessTexture
- OnPostprocessAllAssets
4.8 重启 AssetDatabase 的 Refresh 处理
在上文中提到,重载脚本域将重启 AssetDatabase.Refresh() 方法。除此以外,还有其他情况会重启 AssetData base 的 Refresh 处理。例如:
- 如果某个资源导入失败
- 如果资源在 Refresh 中的导入阶段被更改
- 如果资源在导入时生成了其他资源
- 如果在预处理/后处理回调方法中强制重新导入某个文件,例如:在 OnPostProcessAllAssets 方法中使用 AssetDatabase.ForceReserializeAssets 方法或 AssetImport.SaveAndReimport 方法。注意避免无限重导入。
- 如果在编译完脚本后,某个程序集需要重新加载。
- 如果以“Text only”保存某个资源,但是该资源必须被序列化为二进制格式,就会触发 Refresh 处理。例如:带有地形的场景必须被序列化为二进制格式,因为文本格式太笨重。
4.9 热重载 Hot reloading
热重载是指在 Editor 不重启的情况下,导入并应用脚本和资源的变更。热重载在 Editor 的 Play Mode 和非 Play Mode 都有可能发生。
当你更改并保存一个脚本文件时,Unity 首先保存所有已加载脚本的序列化变量,等脚本加载完成再恢复这些数据。在热重载完成后,所有非序列化的数据都将丢失。
4.10 更新 Artifact Database
这是 Refrech 的最后一步,Library 文件夹中的 Artifact Database 被更新,导入结果被保存在硬盘上。
引用参考
[1] The new Asset Import Pipeline: Solid foundation for speeding up asset imports
[2] The Asset Database
[3] Refreshing the Asset Database
【三思Unity】系列是一个野心颇大的坑,旨在用精炼而准确的文字,阐述Unity的最新技术。