本篇文章是我关于Unity资源导入的相关笔记。

内容基于Asset Import Pipelline V2介绍,即Unity 2019.3或以上版本。

资源导入缓存

Unity的资源导入缓存位于Library目录内。

SourceAssetDB
包含资源的元数据,例如最后修改日期、文件内容哈希、GUID等。Unity用这些信息来判断文件是否被修改,从而决定是否应该重新导入。

ArtifactDB
记录资源导入结果。包含依赖项、元数据和文件列表。

metadata
Asset Import Pipelline V1的导入结果存放于Library/metadata,以资源GUID作为文件名。

img

Artifacts
Asset Import Pipelline V2的导入结果存放于Library/Artifacts,以资源的所有依赖的哈希值作为文件名。

img

V2切换平台所需时间比V1少。这是因为以往的资源导入结果都是用GUID来存储的,而GUID不会变,所以切换平台时只能通过改变内容使资源适应不同平台。V2通过引入依赖项来解决,并且缓存不同平台的导入结果,切换平台就不用再重新导入了。

Unity的Refresh

Refresh执行顺序

  1. 查找资源文件的更改,然后更新源资源数据库。
  2. 导入并编译与代码相关的文件,例如 .dll、.asmdef、.asmref、.rsp 和 .cs 文件。
  3. 如果Refresh不是从脚本调用的,会重新加载域。
  4. 对所有与代码相关的资源进行导入后处理。
  5. 导入与代码无关的资源,并对剩余资源进行导入后处理。
  6. 最后热重载资源。

触发重新Refresh的情况

  • 某个资源导入失败。
  • 资源在 Refresh 中的导入阶段被更改。
  • 资源在导入时生成了其他资源。
  • 在预处理/后处理回调方法中强制重导某个文件,例如:在 OnPostProcessAllAssets 方法中使用 。AssetDatabase.ForceReserializeAssets 方法或 AssetImport.SaveAndReimport 方法。需要注意避免无限重导入。
  • 编译脚本后,需要重新加载程序集。
  • 以“Text only”保存资源,但某些资源必须被序列化为二进制格式,此时就会触发 Refresh 处理。例如:带有地形的场景必须被序列化为二进制格式,因为文本格式太笨重。

资源依赖

当某个资源A使用了另一个资源B,就可以称B为A的依赖,称A为B的引用。资源的导入设置、目标平台等也是依赖。资源的依赖能够影响导入结果。当资源的任一依赖发生变更后,都会使资源的导入缓存过时,从而引起资源重新导入。

使用AssetDatabase.GetDependencies可以获取资源的直接依赖。如果需要频繁获取资源的依赖,可以将获取的资源依赖缓存下来,并使用AssetDatabase.GetAssetDependencyHash来判断是否需要更新缓存。

另外也可以借助工具查找资源的引用,比如 Maintainer

资源规范方案

AssetPostprocessor

AssetImporter

Presets

AssetGraph

提示:在用AssetPostprocessor或AssetImporter做资源规范处理时,可以用AssetImporter.importSettingsMissing判断是否第一次导入,如果导入资源时未附带meta文件,则此值为 true。或者在AssetImporter.userdata加入标记,比如assetImporter.userData = “v1”;,userdata可以在meta文件中看到。

资源导入调试

Unity Docs - Import Activity Window
界面入口:Window->Analysis->Import Activity。诸如导入时间、时长、原因,以及资源本身信息如GUID、资源依赖和Library缓存位置应有尽有。

img

导入日志记录

当出现导入资源异常时,可以根据以下两个日志排查:

  • Editor.log:Unity编辑器几乎所有的日志都在这里,存放路径为”C:\Users\用户名\AppData\Local\Unity\Editor\Editor.log”。
  • Logs/AssetImportWorkerxxx.log:导入日志

也可以在代码中用这个接口来获取导入日志:AssetImporter.GetImportLog

导入加速

编译加速

分析编译时长

以下代码可以在编译后打印编译时长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class AssemblyCompilationReporter
{
[InitializeOnLoadMethod]
private static void Init()
{
CompilationPipeline.assemblyCompilationStarted += CompilationPipelineOnAssemblyCompilationStarted;
CompilationPipeline.assemblyCompilationFinished += CompilationPipelineOnAssemblyCompilationFinished;
}

private static void CompilationPipelineOnAssemblyCompilationFinished(string s, CompilerMessage[] compilerMessages)
{
var startTime = new DateTime(Convert.ToInt64(EditorPrefs.GetString($"CompileStartTime{s}")));
Debug.Log($"=== CompilationPipeline Assembly Finished {s} ({(DateTime.Now - startTime).ToString("s\\.fff")}s)");
}

private static void CompilationPipelineOnAssemblyCompilationStarted(string s)
{
EditorPrefs.SetString($"CompileStartTime{s}", Convert.ToString(DateTime.Now.Ticks));
}
}

另外也可以借助工具来分析

划分程序集

当代码变动时,只会重新编译当前程序集和引用程序集,因此可以通过划分程序集减少编译时间。
以下是Unity的预定义程序集及编译顺序:

阶段 程序集名称 脚本文件
1 Assembly-CSharp-firstpass 名为 Standard Assets、Pro Standard Assets 和 Plugins 的文件夹中的运行时脚本。
2 Assembly-CSharp-Editor-firstpass 名为 Editor 的文件夹(位于名为 Standard Assets、Pro Standard Assets 和 Plugins 的顶级文件夹中的任意位置)中的 Editor 脚本。
3 Assembly-CSharp 不在名为 Editor 的文件夹中的所有其他脚本。
4 Assembly-CSharp-Editor 其余所有脚本(位于名为 Editor 的文件夹中的脚本)

一般运行时的代码都放在Assembly-CSharp,可以将不常变动的代码移至Assembly-CSharp-firstpass,例如将插件放到Plugins目录。

除了Unity预定义的程序集以外,可以自定义程序集,有以下两种做法:

  1. 将C#代码编译为DLL
  2. 使用AssemblyDefine来划分,具体使用方式参考Unity Docs - assembly-definitions-creating

增量编译

后台编译

通常我们在VS里改完代码后,需要回到Unity编辑器重新激活窗口才会重新编译。按照以下步骤可以改成每次保存文件时立刻执行编译:

  1. Unity PlayerSetting里,Windows平台勾选Run In Background
  2. 添加 unity-compile-in-background

除了上述方法以外,也可以尝试优化代码里各种在编译前后执行的逻辑,比如InitializeOnLoad

编辑器运行时C#热重载

当编辑器正在运行时修改代码,需要修改选项Editor->Preferences->General->Script Changes While Playing为Recompile And Continue Playing。重新编译并继续运行。重新编译会重置域,因此容易出现异常。

借助一些插件可以一定程度上解决重置域带来的问题:Fast Script ReloadHot Reload

另外,禁用Play时Reload Domain 可以减少Unity编辑器从点击Play到运行的时间。

Shader异步编译

如果发现每次改动Shader后编译时间非常长,可以尝试比较下开/关Shader异步编译后的编译时长。因为Shader异步编译实际是将部分Shader数据传给Unity的境外服务器并等待返回,因此可能受网络影响。

批量处理资源变动

当处理资源时,调用以下资源变动接口会触发Unity资源导入,如果不做调整,每次调用都会触发一次导入,会耗费大量时间。

  • AssetDatabase.ImportAsset
  • AssetDatabase.MoveAsset
  • AssetDatabase.CopyAsset
  • AddObjectToAsset

可以让Unity批量导入这期间操作的所有资源,通过以下方式:

  1. 在大量执行以上操作前先执行 AssetDatabase.StartAssetEditing();
  2. 在执行完所有资源操作后再执行 AssetDatabase.StopAssetEditing();

资源并行导入

img

设置资源占用内核数量

img

忽略文件夹

在导入过程中,Unity 忽略 Assets 文件夹(或其子文件夹)中的以下文件和文件夹:

  • 隐藏的文件夹。
  • 以“.”开头的文件和文件夹。
  • 以“~”结尾的文件和文件夹。
  • 名为 cvs 的文件和文件夹。
  • 扩展名为 .tmp 的文件。

Cache Server/Unity Accelerator

Unity Accelerator

大致原理:团队其中一个人导入完资源后,会上传至缓存服务器,团队其他人在导入资源时从缓存服务器获取,无需再重新导入这些资源。

Preferences和ProjectSetting都可以配置。

Cache Server对应Asset Import Pipelline V1,Unity Accelerator对应Asset Import Pipelline V2。

img

注意:使用Cache Server疑似会导致错误导入资源的概率增加,怀疑可能是因为当某个工程错误导入某个资源后会将错误数据上传到Server。如果使用时发现这种情况增加,可以考虑是不是缓存服务器导致的。

Unity启动过程

  1. 证书验证
  2. Package Manager
  3. 编译
  4. 资源导入

当你发现Unity启动崩溃了,可以尝试根据Unity在哪一步崩溃来判断问题。

Library文件含义

编辑器出问题不要急着把整个Library删掉,可以尝试先删除可能有问题的文件。Library的文件大部分看名字就能判断出大概用途:

  • Artifacts、ArtifactDB、SourceAssetDB:资源缓存、资源数据库相关
  • AtlasCache、SpriteAtlasDatabase.asset:图集缓存
  • Bee:增量构建缓存,打包时用的
  • ShaderCache、ShaderCache.db:Shader缓存
  • ScriptAssemblies、ScriptMapper:代码、编译相关、
  • PackageCache、PackageManager:插件包相关

举几个例子:

  • 打包报错,删除Bee文件夹解决

img

  • 莫名奇妙的编译报错,或者是某个没改过的插件或Unity内部代码提示编译报错。可以试试以下步骤:

    • 关闭untiy
    • 删除工程下所有.csproject、.sln文件
    • 删除Library/ScriptAssemblies文件夹
    • 重新打开untiy
  • 在编辑器里运行游戏使用了图集,发现图集异常比如变白块,可以把Library下图集缓存删了。


参考文档

Unity Docs - Asset Workflow

Unity Docs - ImportActivityWindow

Unity Docs - AssetDatabaseRefreshing

Youtube - AssetDatabase.Refresh() refresher - Unite Copenhagen

The new Asset Import Pipeline: Solid foundation for speeding up asset imports