Unity引擎资源管理机制介绍

介绍

在开发时有时会对Unity引擎产生一些疑惑。

  • 为什么新创建或者通过版本管理工具拉取的项目,在首打开的时候会加载很久?
  • Library文件夹为什么这么大,可不可以删掉?
  • 项目中的 .meta 文件又是什么,有什么用,是否可以删除?
  • Unity引擎对于项目中的Asset有时如何处理,Asset之间的引用又是如何维系的?

诸如以上的等等疑问,本篇文章将注意解答。

哪些人适合本篇文章

  • 刚刚从事Unity开发的人员。
  • 从事Unity资产以及版本管理的人。
  • 想要对Unity Asset、序列化深入理解的开发人员。

从本篇文章中会学到什么

  • 外部资源导入Unity后,引擎所做的幕后工作。
  • Library文件夹的内容及作用
  • .meta 文件的作用
  • Unity.Object 和 Asset 之间的关系
  • File GUID、Local ID、Instance ID的概念及作用
  • Resources 和 StreamingAssets 特殊文件夹介绍
  • AssetBundle的简单介绍

本文主要讲解下图中的分发部分

点击看大图

新资源导入后引擎会主要做什么?

当新资源导入工程中时,Unity会读取并处理添加到Assets文件夹中的任何文件,并将文件内容转换为Unity引擎可用的内部数据,而导入的源资源,引擎并不会做改动。


引擎为什么这么做?

对于大多数类型的资产(Asset),Unity需要将资产源文件中的数据转换为其可在游戏或实时应用程序中使用的格式。这些转换后的文件及其相关数据会存储在 资产数据库(Asset Database) 中。也因为这些被处理后的Asset,开发者才可以通过Unity内的API对Asset进行加载、卸载、设置等操作。

转换过程是必要的,因为大多数文件格式的设计目标是优化存储空间,而在游戏或实时应用程序中,资产数据需要以一种硬件(如 CPU、GPU 或音频硬件)能够立即使用的格式存在。例如,当 Unity 将一个 .png 图像文件作为纹理导入时,运行时并不会直接使用原始的 .png 格式数据。相反,在导入纹理时,Unity 会创建该图像的另一种格式的表示,并将其存储在项目的 Library 文件夹 中。Unity 引擎中的 Texture 类 使用导入的版本,Unity 会将其上传到 GPU 以实现实时显示。

如果您随后修改了已导入的资产源文件(或修改了其任何依赖项),Unity 会重新导入该文件并更新导入版本的数据。有关此过程的更多信息,请参阅 刷新资产数据库(Refreshing the Asset Database)

资产数据库还提供了一个 AssetDatabase API,您可以使用它来访问资产,并控制或自定义导入过程。


平台切换时会重新导入?

切换平台时,Unity可能会重新导入资源。这通常发生在不同平台的资源导入方式有所差异的情况下。例如,不同平台支持的纹理格式不同,因此纹理会针对各个平台进行不同方式的导入。

file

当使用 Asset Database V2 时,平台信息是 Asset Database 用于存储 Unity 内置导入器导入结果的哈希值的一部分。这意味着针对不同平台导入资源的结果会作为独立的缓存数据存储起来。

当首次将项目切换到一个新的平台,并且项目中包含未为该平台导入过的新资源时,这些资源会重新导入,需要等待完成。不过新平台重新导入的数据并不会覆盖之前平台的缓存导入数据。

当切换回已经为其导入过资源的平台时,那些资源的导入结果已经被缓存,可以直接使用,从而大幅加快切换速度。

这也是为什么在项目工程比较庞大时,会根据开发平台的数量,让项目工程保持多份的原因之一。


有什么办法能够加快导入速度吗?

使用 Unity Accelerator 是 Unity 提供的一种缓存工具,旨在加速团队协作中的某些关键工作流。它通过本地缓存和共享网络缓存的方式,减少资产的导入、构建和其他资源处理过程的时间。Unity Accelerator 能够在开发团队成员之间共享缓存数据,从而提高团队整体的开发效率。


Library文件夹的内容

Source Assets 和 Artifacts

Unity在 Library 文件夹 中维护了两个数据库文件,它们统称为 资产数据库(Asset Database)。这两个数据库分别记录了有关 Source Assets 的信息,以及 Artifacts 的信息(即导入结果的相关数据)。

Source Asset 数据库(Source Asset Database)

Source Asset 数据库包含 Source Assets 的元信息。Unity 使用这些信息来判断文件是否发生了修改,从而决定是否需要重新导入文件。这些信息包括以下内容:

  • 文件的最后修改日期
  • 文件内容的哈希值
  • 全局唯一标识符(GUID)
  • 其他元信息

file

Artifact 数据库(Artifact Database)

Artifacts 是导入过程的结果。Artifact 数据库记录了每个 Source Asset 的导入结果信息。每个 Artifact 包含以下内容:

  • 导入的依赖关系信息
  • Artifact 的元信息
  • Artifact 文件的列表

file

  • 注意事项
    数据库文件位于项目的 Library 文件夹 中,因此应将其排除在版本控制系统之外。这些文件的具体路径如下:

  • Source Asset 数据库Library\SourceAssetDB

  • Artifact 数据库Library\ArtifactDB

注:对应数据库的大小区分也很大

除了上面提到的两个数据库,Library文件夹还存储 Unity 引擎生成的其它缓存

file
file


Unity Meta 文件简介

在 Unity 中,Meta 文件 是每个 Asset 文件和文件夹的伴随文件,主要用于存储有关该 Asset 的关键信息。这些 Meta 文件是 Unity 管理 Asset 的核心机制之一,它们保证了项目中的 Asset 引用关系不会因为文件的移动或修改而丢失。

Meta 文件的作用

  1. 存储唯一 ID
    Meta 文件为每个 Asset 分配了一个唯一的 ID(GUID,Global Unique Identifier)。Unity 使用这个 GUID 来跟踪 Asset,即使 Asset 被重命名或移动到其他文件夹,引用关系依然能够保持一致。

  2. 保存导入设置
    Meta 文件还存储了 Asset 的导入设置,例如:

    • 纹理:存储纹理类型、Wrap Mode、Filter Mode 等。
    • 模型:存储模型的网格设置、缩放比例等。
    • 音频:存储音频格式、压缩设置等。
    • 脚本:Meta 文件记录了脚本的唯一 ID,以确保 GameObject 和 Prefab 能正确引用该脚本。
  3. 引用和依赖管理
    Unity 使用 Meta 文件中的 GUID 来管理 Asset 之间的引用关系。例如:

    • 材质文件引用的纹理文件。
    • Prefab 中引用的脚本或其他 Asset。
    • 场景中引用的资源(如声音、模型、贴图等)。
  4. 版本控制支持
    Meta 文件是文本格式,这使得它们适合被添加到版本控制系统(如 Git)。通过 Meta 文件,团队成员可以在不同的机器上保持 Asset 的一致性,而不会因为 GUID 不匹配导致引用丢失。


Meta 文件的位置与显示

  • Meta 文件位于 Asset 文件所在的位置,文件名与其对应的 Asset 文件相同,但扩展名为 .meta
  • 在 Unity 编辑器的 Project 窗口 中,Meta 文件默认是隐藏的。
  • 如果需要查看 Meta 文件,可以在 Unity 的 Editor Settings 中启用 Visible Meta Files,或者在操作系统中显示隐藏文件。

Meta 文件的结构

Meta 文件是以 YAML 格式存储的,主要包含以下内容:

  1. 文件格式版本
    指定该 Meta 文件的版本号。

  2. GUID
    Meta 文件中的核心字段,存储 Unity 分配的唯一 ID。

  3. 导入设置
    不同类型的 Asset 包含的导入设置字段不同,例如纹理文件会包含 Wrap Mode 和 Filter Mode。

注意事项

  1. 不要手动编辑 Meta 文件
    尽管 Meta 文件是文本格式,但直接修改可能会导致 Asset 的引用关系混乱,除非对其内容完全了解。

  2. Meta 文件和 Asset 文件必须保持一致

    • 如果移动或重命名 Asset 文件,必须同时移动或重命名对应的 Meta 文件。
    • 如果一个 Asset 文件丢失了 Meta 文件,Unity 会生成新的 Meta 文件,但这会导致原有的引用关系丢失。
  3. Meta 文件在版本控制中的重要性

    • 确保将 Meta 文件添加到版本控制系统中,以保证团队协作时引用关系的一致性。
    • 如果不包含 Meta 文件,其他开发者可能会遇到丢失引用的错误。

file

以下是一个示例纹理 Meta 文件的内容:

fileFormatVersion: 2
guid: 25599e1db6d288340930c13c81ead2d0
TextureImporter:
  internalIDToNameTable: []
  externalObjects: {}
  serializedVersion: 12
  省略....
  lightmap: 0
  compressionQuality: 50
  spriteMode: 1
  spriteExtrude: 1
  spriteMeshType: 1

Unity.Object 和 Asset 之间的关系

file
从脚本层面来看,Unity 所认为的 “Asset” 与在 Project 窗口中看到的内容略有不同。你在项目的 Assets 文件夹中放置的文件是这些 Asset 的源文件,但在概念上它们与 Unity 编辑器实际使用的 Asset 对象并不相同。当 Unity 导入这些资产文件时,它会对文件进行处理并生成导入结果:一些派生自 UnityEngine.Object 的已序列化 C# 对象。从脚本的角度来看,当你在 Unity 编辑器中编写脚本访问时,你所能使用的 Asset 实际上是这些导入后生成的对象。

例如,一个最初可能是 JPEG 或 PNG 等二进制文件的 Asset,在导入后会转换为一个 C# 对象,其类型是 UnityEngine.Object 的特化类型。对于 JPEG 或 PNG 文件,它们会被转换为 Texture 类的序列化实例(Texture 继承自 UnityEngine.Object)。这个序列化后的对象数据作为 artifact 存储在 Library 文件夹中。因此,当你在脚本中访问一个 Texture Asset 时,你并不是直接访问原始的 JPEG 或 PNG 文件,而是在访问导入后生成的 C# Texture 对象的序列化版本。Unity 在导入过程中会在原始 Asset 文件旁边创建一个 .meta 文件,其中包含该 Asset 的导入设置,以及一个 GUID,用来将原始 Asset 文件与资产数据库(asset database)中的 artifact 关联起来。


File GUID、Local ID

  • File GUID 用来唯一识别一个 Asset 文件,Local ID 用来标识该 Asset 中的具体对象。

  • 因为Unity在项目中为每个资产文件生成一个独特的 GUID(存放于对应 .meta 文件中)。这使得无论该文件被移动、改名,其引用关系都能通过 GUID 保持不变。

  • 对于同一个资产文件内可能包含多个对象(如一个 Prefab 内有多个组件,一个模型文件中有多个子网格等),Unity 使用 Local ID 区分资产内部的不同对象。

  • 在加载与引用时,Unity 会使用 File GUID 定位到具体的资产文件,再通过 Local ID 找到资产文件中对应的对象,从而精确识别并加载目标对象。
    下图为Cube Prefab 示例

file

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &6453748738911814904
GameObject:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  serializedVersion: 6
  m_Component:
  - component: {fileID: 3855292376531920072}
  - component: {fileID: 6570158793040505349}
  - component: {fileID: 97272203064406730}
  - component: {fileID: 1337968860972952898}
  m_Layer: 0
  m_Name: Cube
  m_TagString: Untagged
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1
--- !u!4 &3855292376531920072
Transform:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 6453748738911814904}
  serializedVersion: 2
  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
  m_LocalPosition: {x: -0.5675458, y: 2.4033766, z: -5.2447443}
  m_LocalScale: {x: 1, y: 1, z: 1}
  m_ConstrainProportionsScale: 0
  m_Children: []
  m_Father: {fileID: 0}
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &6570158793040505349
MeshFilter:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 6453748738911814904}
  m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &97272203064406730
MeshRenderer:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 6453748738911814904}
  m_Enabled: 1
  m_CastShadows: 1
  m_ReceiveShadows: 1
  m_DynamicOccludee: 1
  m_StaticShadowCaster: 0
  m_MotionVectors: 1
  m_LightProbeUsage: 1
  m_ReflectionProbeUsage: 1
  m_RayTracingMode: 2
  m_RayTraceProcedural: 0
  m_RayTracingAccelStructBuildFlagsOverride: 0
  m_RayTracingAccelStructBuildFlags: 1
  m_RenderingLayerMask: 1
  m_RendererPriority: 0
  m_Materials:
  - {fileID: 10303, guid: 0000000000000000f000000000000000, type: 0}
  m_StaticBatchInfo:
    firstSubMesh: 0
    subMeshCount: 0
  m_StaticBatchRoot: {fileID: 0}
  m_ProbeAnchor: {fileID: 0}
  m_LightProbeVolumeOverride: {fileID: 0}
  m_ScaleInLightmap: 1
  m_ReceiveGI: 1
  m_PreserveUVs: 0
  m_IgnoreNormalsForChartDetection: 0
  m_ImportantGI: 0
  m_StitchLightmapSeams: 1
  m_SelectedEditorRenderState: 3
  m_MinimumChartSize: 4
  m_AutoUVMaxDistance: 0.5
  m_AutoUVMaxAngle: 89
  m_LightmapParameters: {fileID: 0}
  m_SortingLayerID: 0
  m_SortingLayer: 0
  m_SortingOrder: 0
  m_AdditionalVertexStreams: {fileID: 0}
--- !u!65 &1337968860972952898
BoxCollider:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 6453748738911814904}
  m_Material: {fileID: 0}
  m_IncludeLayers:
    serializedVersion: 2
    m_Bits: 0
  m_ExcludeLayers:
    serializedVersion: 2
    m_Bits: 0
  m_LayerOverridePriority: 0
  m_IsTrigger: 0
  m_ProvidesContacts: 0
  m_Enabled: 1
  serializedVersion: 3
  m_Size: {x: 1, y: 1, z: 1}
  m_Center: {x: 0, y: 0, z: 0}

对应meta 信息

fileFormatVersion: 2
guid: 6b8676888f4e91b45b7f4cbd267709bd
PrefabImporter:
  externalObjects: {}
  userData: 
  assetBundleName: 
  assetBundleVariant: 

Instance ID

Instance ID 是 Unity 在运行时为已加载到内存中的对象分配的临时唯一标识,只在当前运行会话中有效。

  • Unity 在编辑器或游戏运行时将资源(如 GameObject、组件等)加载到内存后,这些对象都需要一个唯一编号便于引擎管理与访问。Instance ID 仅在进程存活期间有效,一旦编辑器重启或游戏停止,这些 ID 即失效,不同于固定存储于项目中的 GUID 和 Local ID。
    file

总结与对比

属性 GUID Local ID Instance ID
唯一性 全局唯一 文件内唯一 运行时唯一
存储范围 项目范围 单个资源文件中 内存中
存储位置 .meta 文件 场景或 Prefab 文件 不存储
用途 跨文件引用资源 文件内资源定位 内存中引用

Resources 文件夹

简介

Resources 文件夹是 Unity 提供的一个特殊目录,用于存放在运行时可以通过 Resources.Load() 方法动态加载的资产。即使这些资产未被场景直接引用,它们仍然会被包含在构建中。当项目构建时,所有名为“Resources”的文件夹中的Assets和Objects会被合并为一个名为resources.assets的单独序列化文件。此文件还包含元数据和索引信息,类似于一个AssetBundle。正如AssetBundle文档中所描述的,这个索引包括一个序列化的查找树,用于将给定对象的名称解析为其对应的文件GUID和Local ID。此外,它还用于定位对象在序列化文件主体中的特定字节偏移位置。
如果构建中的某个场景引用了一个资产,那么该资产会被序列化到一个名为 sharedAssets*.assets 的文件中。

注意:官方明确说明不建议是使用它!

优点

  1. 动态加载:可以在运行时动态加载资源,便于灵活控制资源使用。
  2. 快速原型开发:对于快速迭代和原型设计而言,Resources 文件夹易于使用,能简化开发流程。
  3. 集中存储:可以将一些全局使用的资源统一管理,例如配置文件、通用预制件等。

缺点

  1. 启动性能问题
    • 在大多数平台上,这种查找数据结构是一个平衡搜索树,其构建时间的增长是O(n log(n))的复杂度。这种增长还导致索引加载时间随着Resources文件夹中对象数量的增加呈现出非线性增长。
    • 这种操作是无法跳过的,并且会在应用程序启动时初始的非交互式启动画面显示期间发生。如果一个Resources系统包含10,000个assets,在低端移动设备上已观察到初始化可能会耗费数秒,即使Resources文件夹中大多数对象在应用程序的首个场景加载时实际上很少需要。
  2. 内存管理困难
    • Resources 中的资源不会被自动卸载,需手动管理内存,可能导致内存泄漏或占用过多内存。
  3. 构建时间增加
    • Resources 文件夹中的资源会导致构建时间变长,尤其是当资源依赖关系复杂时。
  4. 资源不可见性
    • 无法轻松查看哪些资源被实际加载,调试困难。
  5. 跨平台优化受限
    • 无法为不同平台定制资源加载策略,降低了灵活性。

注意事项

  1. 避免滥用
    • 除非必要,不要在完整项目中大量使用 Resources 文件夹。适合仅在原型阶段或需要加载少量全局资源时使用。
  2. 优化资源数量
    • 尽量减少 Resources 文件夹中的资源数量,避免过多资源导致启动时间和构建时间增加。
  3. 依赖管理
    • 注意 Resources 文件夹中的资源可能会引入外部依赖(如材质引用了外部纹理),这些依赖资源会被包含到 resources.assets 文件中,但无法直接加载。

StreamingAssets 特殊文件夹介绍

简介

StreamingAssets 是 Unity 提供的一个特殊文件夹,用于存储需要在运行时直接访问的文件。Unity 会将 StreamingAssets 文件夹中的内容不做任何更改的全部复制到目标设备的特定位置。通过 Application.streamingAssetsPath 可以获取该文件夹的路径,在不同平台上其路径有所不同。

在某些平台(如 Android 和 Web),StreamingAssets 文件夹的内容以 URL 的形式返回,需使用 UnityWebRequest 类访问。

优点

  1. 平台兼容性:StreamingAssets 提供跨平台的访问路径,Application.streamingAssetsPath 确保了文件路径在各平台上的一致性。
  2. 无需动态下载:允许将资源文件(如视频、配置文件等)直接打包在构建中,无需在线下载。

缺点

  1. 只读路径
    • StreamingAssets 目录是只读的,运行时无法修改或写入新文件。
  2. 编译限制
    • 位于 StreamingAssets 文件夹中的 .dll 和脚本文件不会被包含在脚本编译中,无法直接运行。
  3. 访问复杂性
    • 在 Android 和 Web 平台,StreamingAssets 返回 URL,不能通过文件系统 API 直接访问,需额外使用 UnityWebRequest 访问内容。

AssetBundle基础知识

简介

AssetBundle 是一种Unity引擎定义的特殊存档文件,类似于Zip包,其中包含特定平台的非代码资源(例如模型、纹理、Prefab、音频片段,甚至整个场景),也可以根据需求对资源进行压缩减少AssetBundle大小,并在运行时加载这些资源。

AssetBundles 的用途

  • 可下载内容(DLC)。
  • 减少应用程序的初始安装大小。
  • 加载针对最终用户平台优化的资源。
  • 降低运行时内存压力。

AssetBundles 的平台特定性

  • 为任何独立平台构建的 AssetBundle 只能在该平台上加载。例如,在 iOS 上构建的资源包无法在 Android 上使用。这是因为 shaders、纹理以及其他类型的数据会根据 BuildTarget(构建目标)被打包为特定平台的格式。

构建或重建 AssetBundles

在构建或重建 AssetBundles 时,通常会通过一次 API 调用来构建整个项目的所有 AssetBundles。通常不建议单独构建或重建它们,因为当一起构建时,Unity 编辑器会根据每个 AssetBundle 的内容来决定如何引用或嵌入资源,这可能依赖于其他 AssetBundles 中的内容。
例外情况是:如果你是一位资深开发者或高级开发者,并且了解项目中 AssetBundles 的引用和依赖关系,有时可以只构建项目中一部分的 AssetBundles。

AssetBundle 的内部结构

AssetBundle 这种容器文件格式,类似于 zip 文件,具有二进制标头文件,并嵌入了其他文件。

标头

  • 包含有关 AssetBundle 的信息,例如标识符、压缩类型和清单(manifest)。
  • 清单是一个查找表,以对象的名称为键。每个条目提供一个字节索引,用于指示对象在 AssetBundle 数据段中的位置。
  • 在大多数平台上(例如 Windows 和 OSX 衍生平台,包括 iOS),查找表通过平衡搜索树(如红黑树)实现。因此,随着 AssetBundle 中资源数量的增加,构建清单所需的时间将以超过线性增长的方式增加。

其他文件

  1. 序列化文件(Serialized files)

    • 包含序列化的 Unity 对象。这种二进制文件格式与Player Builds中使用的格式相同。
    • 如果 AssetBundle 包含资源(Assets),所有对象会被写入一个序列化文件中。
    • 如果 AssetBundle 包含场景(Scenes),每个场景包含两个序列化文件:一个存储场景层级结构中的对象,另一个存储所有引用的对象。
  2. 资源文件(Resource files)

    • 这些是某些资源(如纹理和音频)单独存储的二进制数据块。
    • 这种分离方式允许 Unity 使用多线程代码高效地从磁盘加载它们。

AssetBundle 依赖关系

  • 如果一个或多个 UnityEngine.Object 包含对位于另一个 AssetBundle 中的 UnityEngine.Object 的引用,则该 AssetBundle 会依赖于另一个 AssetBundle。如果 UnityEngine.Object 包含对未包含在任何 AssetBundle 中的 UnityEngine.Object 的引用,则不会形成依赖关系。在这种情况下,构建 AssetBundle 时,所依赖的对象会被复制到当前 AssetBundle 中。

  • 如果多个 AssetBundle 中的多个对象引用了同一个未分配到 AssetBundle 的对象,则每个 AssetBundle 都会为该对象创建自己的副本,并将其打包到生成的 AssetBundle 中。

  • 当 AssetBundle 包含依赖关系时,必须在加载尝试实例化的对象之前,先加载包含这些依赖项的 AssetBundle。Unity 不会自动加载这些依赖项。

file

例如,一个材质(Material)位于 Bundle 1 中,而它引用的纹理(Texture)位于 Bundle 2 中:
在这个例子中,必须在加载 Bundle 1 中的材质之前,将 Bundle 2 加载到内存中。加载 Bundle 1 和 Bundle 2 的顺序并不重要,关键是在加载材质之前,Bundle 2 已加载。


参考链接

发表评论