Asset资源映射

什么是Asset

Assets目录

创建一个新项目时,会有一些默认目录

image-20220924133258243

  • Assets 实际资源目录
  • Library 存放Unity处理完毕的资源,大部分的资源导入到Assets目录之后,还需要通过Unity转化成Unity认可的文件,转化后的文件会存储在这个目录

  • Packages 2018以后新增的目录,用于管理Unity分离的packages组件

  • ProjectSettings 存放Unity的各种项目设定

AssetBundles

等同于ZIP或者RAR等压缩文件,但是只针对于Unity的Asset,拥有平台差异性,且不包含代码

Unity Asset

也就是Unity能够识别的文件,一种是Unity原生支持的文件,比如材质球。一种是需要特殊处理后才能支持的文件,比如FBX。对于后者,Unity提供了可以自定义的脚本Import

image-20220924134508586

Asset的识别和引用

Assets和Objects

Asset指Project面板里可以看到的所有文件,包含单个文件以及文件夹

Object指继承自UnityEngine.Object的对象,一个可以序列化的数据

大多数Object都是内置支持的,除了:

  • ScriptableObject 提供了一个指向MonoScript的转换器。MonoScript是一个Unity内部的数据类型,它不是可执行代码,但是会在特定的命名空间和程序集

    下,保持对某个或者特殊脚本的引用。

  • MonoBehaviour 提供了一个指向MonoScript的转换器。MonoScript是一个Unity内部的数据类型,它不是可执行代码,但是会在特定的命名空间和程序集

    下,保持对某个或者特殊脚本的引用。

Asset与Object是1对N的关系,一个Prefab可以作为Asset,可以包含很多的Object

File GUIDs和Local IDs

Unity把序列化分为两个部分

第一部分叫做File GUID,标识这个资产的位置,这个GUID是由Unity根据内部算法自动生成的,并且存放在原始文件的同目录、同名但是后缀为.meta的文件里

  • 第一次导入资源的时候Unity会自动生成
  • 在Unity的面板里移动位置,Unity会自动帮你同步.meta文件
  • 在Unity打开的情况下,单独删除.meta,Unity可以确保重新生成的GUID和现有的一样
  • 在Unity关闭的情况下,移动或者删除.meta文件,Unity无法恢复到原有的GUID,也就是说引用会丢失

第二部分叫做Local ID,表示当前的Object在Asset里的唯一标识

File GUID确保了资产在整个Unity工程里唯一,Local ID确保Objects在资产里唯一,这样就可以通过二者的组合去快速找到对应的引用

Unity还在内部维护了一张资产GUID和路径的映射表,每当有新的资源进入工程,或者删除了某些资源,又或者调整了资源路径,Unity的编辑器都会自动修改这

张映射表以便正确地记录资产位置

所以如果.meta文件丢失或者重新生成了不一样的GUID,Unity就会丢失引用

Library中的资源位置

Unity不支持的资源格式,会通过Import进行资源转换,所有的转换结果都会存储在Library/metadata/目录下,以File GUID的前两位进行命名的文件夹里

image-20220924140512311

Instance ID

运行时所采用的ID,与File ID和Local ID呈映射关系

只要系统解析到一个Instance ID,就能快速找到代表这个Instance ID的已加载的对象。如果Object没有被加载,File GUID和Local ID也可以快速地定位到指定的

Asset资源从而即时进行资源加载

资源生命周期

加载

当Unity启动时,缓存系统会对项目立刻要用到的数据(比如:启动场景里的这些或者它的依赖项),以及所有包含在Resources目录的Objects进行初始化

如果在运行时导入了Asset或者从AssetBundles(比如:远程下载下来的)加载Object都会产生新的Instance ID

另外Object在满足下列条件的情况时会自动加载,比如:

  • 某个Object的Instance ID被间接引用了
  • Object当前没有被加载进内存
  • 可以定位到Object的源位置(File GUID 和 Local ID)

卸载

  • 当没有使用的Asset在执行清理的时候,会自动卸载对应的Object。一般是由切场景或者手动调用了Resources.UnloadUnusedAssets的API的时候触发的。但

    是这个过程只会卸载那些没有任何引用的Objects

  • 从Resources目录下加载的Objects可以通过调用Resources.UnloadAsset进行显式的卸载。但这些Objects的Instance ID会保持有效,并且仍然会包含有

    效的FileGUID 和LocalID。这个Object在被直接或者间接引用之后会马上被加载。

  • 从AssetBundles里得到的Objects在执行了AssetBundle.Unload(true)之后,会立刻自动的被卸载,并且这会立刻让这些Objects的File GUID、Local ID以

    及Instance ID立马失效。任何试图访问它的操作都会触发一个NullReferenceException。但如果调用的是AssetBundle.Unload(false)API,那么生命周期内的

    Objects不会随着AssetBundle一起被销毁,但是Unity会中断File GUID 、Local ID和对应Instance IDs之间的联系,也就是说,如果这些Objects在未

    来的某些时候被销毁了,那么当再次对这些Objects进行引用的时候,是没法再自动进行重加载的。如果再次加载相同Asset,Unity也不会复用先前加载的

    Objects,而是会重新创建Instance ID,也就是说内存里会有多份冗余的资源。

Resources目录的优点与痛点

特殊的工程目录

Editor

这个目录是用来辅助开发的,可以是一个,也可以有很多个,可以在任意的子目录下面。主要作用就是可以编译Unity编辑器模式下提供的脚本和接口,用于辅助

研发,创建各种资源检查,生成工具,以及自定义Unity的工具栏,窗口栏等等。Editor目录下的所有脚本都不会被编译到正式发行包里,很多很多优秀的插件都是

需要通过Unity提供的编辑器下的扩展接口来实现功能扩展的。

Editor Default Resources

这个需要和Editor配合使用。一个扩展面板或者工具光秃秃的也不好看,配上一些美化资源之后会让你的插件或者工具格局更高。要注意的是,这个目录只能是唯

一的,并且只能放在Assets的根目录下

Plugins

这个目录是存放代码之外的库文件的。比如引入的第三方代码,SDK接入的各种jar包,.a文件,.so文件.framwork文件等等,这些库文件会在Unity编译的时候链

接到你的DLL里

Resources

这个和Editor一样,可以在Assets下的任何目录下,并且可以有任意多份。所有Resources目录下的文件都会直接打进一个特殊的Bundles中,并且在游戏启动

时,会生成一个序列化映射表,并加载进内存里。

Gizmos

这个目录其实比较简单,就是可以辅助你在Unity的Scene视窗里显示一些辅助标线,图标或者其它的标识,用来辅助开发定位资源,因为是Scene视窗的,所以

Game视窗和发布之后都不会看到。

StreamingAssets

这个文件是Unity的一个重要文件。Unity发布程序或者游戏,资源随包出去的时候,只有2个地方,一个就是Resources目录,另外一个就是StreamingAssets。这

个目录的资源,文件或者任何东西,都会原封不动的复制到最终的Apk或者iOS的包内。

Resources详解

AB有很多的缺点,比如:

  • 无法直观地看到包内的资源情况
  • 异步加载,需要写比较繁琐的回调处理
  • 调试的时候,无法通过Hierarchy直接定位到资源
  • 使用之前需要花费时间进行打包,尤其是在开发的时候,调整资源频繁,如果忘记打包可能导致Bug

Resource的使用相对来说就非常简单了

Resources目录实践

官方建议:不要使用Resouces(摊手)

  • Resources内的资源会增加应用程序的启动时间和构建时长
  • Resources内的资源无法增量更新,这是现在手机游戏开发的致命点

那些情况可以使用Resources

  • 某些资源是项目整个生命周期都必须要用的
  • 有些很重要,但是却不怎么占内存的
  • 不怎么需要变化,并且不需要进行平台差异化处理的
  • 用于系统启动时候最小引导的

开发时的替代方案

AssetDatabase(略)

AssetBundle原理

结构

和传统ZIP没什么区别,由两个部分组成:包头、数据

包头包含有关AssetBundle的信息,比如标识符、压缩类型和内容清单

数据段包含通过序列化AssetBundle中的Assets而生成的原始数据。如果指定LZMA为压缩方案,则是先把所有Asset打包,然后整体压缩。如果指定了

LZ4,则是先压缩每一个Asset,然后一起打包。当然你也可以不压缩。

加载AssetBundle

AssetBundle.LoadFromMemory(Async)

不建议使用这个API

他是从托管代码的字节数组里加载AssetBundle,也就是说你要提前用其它的方式将资源的二进制数组加入到内存中

然后该接口会将源数据从托管代码字节数组复制到新分配的、连续的本机内存块中

但如果AssetBundle使用了LZMA压缩类型,它将在复制时解压AssetBundle。而未压缩和LZ4压缩类型的AssetBundle将逐字节的完整复制

之所以不建议使用该API是因为,此API消耗的最大内存量将至少是AssetBundle的两倍:本机内存中的一个副本,和从托管字节数组中复制的一个副本

AssetBundle.LoadFromFile(Async)

一种高效的API,用于从本地存储加载未压缩或LZ4压缩格式的AssetBundle

在桌面独立平台、控制台和移动平台上,API将只加载AssetBundle的头部,并将剩余的数据留在磁盘上,AssetBundle的Objects会按需加载

但在Editor环境下,API还是会把整个AssetBundle加载到内存中

AssetBundleDownloadHandler

DownloadHandlerAssetBundle的操作是通过UnityWebRequest的API来完成的

使用UnityWebRequest下载AssetBundle的最简单方法是调用UnityWebRequest.GetAssetBundle

LZMA压缩的AssetBundles将在下载和缓存的时候更改为LZ4压缩。这个可以通过设置Caching.CompressionEnable属性来更改

建议

  • 只要有可能,就应该使用AssetBundle.LoadFromFile
  • 对于必须下载或热更新AssetBundles的项目,建议使用UnityWebRequest
  • 当使用UnityWebRequest时,要确保下载程序代码在加载AssetBundle后正确地调用Dispose
  • 对于需要独特的、特定的缓存或下载需求的大项目,可以考虑使用自定义的下载器

加载Asset

  • LoadAsset (LoadAssetAsync)
  • LoadAllAssets (LoadAllAssetsAsync)
  • LoadAssetWithSubAssets (LoadAssetWithSubAssetsAsync)

这些API的同步版本总是比异步版本快至少一个帧(其实是因为异步版本为了确保异步,都至少延迟了1帧)

LoadAllAsset比对LoadAsset的多个单独调用略快一些。因此,如果要加载的Asset数量很大,那么使用LoadAllAsset

但如果需要一次性加载不到三分之二的AssetBundle,则要考虑将AssetBundle拆分为多个较小的包,再使用LoadAllAsset

如果需要加载的对象都来自同一Asset,但与许多其它无关对象一起存储在AssetBundle中,则使用LoadAssetWithSubAssets

任何其它情况,请使用LoadAsset或LoadAssetAsync

加载细节

Object加载是在主线程上执行,同步AssetBundle.Load方法将暂停主线程,直到Object加载完成,通过设置Application.backgroundLoadingPriority

可以限制每帧最多的加载时长

  • ThreadPriority.High:每帧最多50毫秒
  • ThreadPriority.Normal:每帧最多10毫秒
  • ThreadPriority.BelowNormal:每帧最多4毫秒
  • ThreadPriority.Low:每帧最多2毫秒

AssetBundle之间的依赖

可以通过AssetDatabaseAPI查询AssetBundle依赖项

当某个AssetBundle中的Object引用了其它AssetBundle里的Object时,那么这个AssetBundle就会依赖于另外的AssetBundle

在加载AB时,就会为所有的Object分配好InstanceID,而在某一个InstanceID间接或直接被引用时,对应的Object才会加载

因此哪怕两个AB包之间存在依赖,AB包的加载顺序也不重要,但是一旦你需要加载AB包里指定的资源了,那么需要保证这个资源所依赖的AB已经被加载过了

image-20220924154040338

假设Material A引用Texture B。Material A被打包到AssetBundle1中,Texture B被打包到AssetBundle2中

AssetBundle2必须在Material A从AssetBundle1中加载之前先加载。这并不意味着AssetBundle 2必须在AssetBundle 1之前加载

在将Material A从AssetBundle 1加载之前加载AssetBundle 2就足够了。简单来说就是AssetBundle之间的加载没有先后,但是Asset的加载有

AssetBundle manifests

当使用BuildPiine.BuildAssetBundles API执行AssetBundle构建管线时,Unity会序列化一个包含每个AssetBundle依赖项信息的数据。此数据存储在单独的一个

AssetBundle中,其中包含AssetBundleManifest类型的单个对象

包含Manifest的AssetBundle可以像任何其它AssetBundle一样加载、缓存和卸载

  • AssetBundleManifest.GetAllDependencies 返回AssetBundle的所有层次依赖项,其中包括AssetBundle的直接子级、其子级的依赖项等。
  • AssetBundleManifest.GetDirectDependations 只返回AssetBundle的直接子级

建议

在多数情况下,最好在玩家进入应用程序的性能关键区域(如主游戏关卡或世界)之前加载尽可能多的所需对象

这在移动平台上尤为重要,因为在移动平台上,访问本地存储的速度很慢,并且在运行时加载和卸载对象会触发垃圾回收

实践

管理已经加载的Asset

AssetBundle.unload(bool)

此API将卸载正在调用的AssetBundle的包头信息。unload参数决定是否也卸载从此AssetBundle实例化的所有对象。如果设置为true,那么从AssetBundle创建的

所有对象也将立即卸载,即使它们目前正在活动场景中被引用

对于大多数项目来说,应该使用AssetBundle.Unload(True)

  • 在应用程序的生命周期内定义一个合适的节点,并在此期间卸载不需要的AssetBundle,例如在关卡切换或加载屏幕期间。这是最简单和最常见的选择
  • 维护单个对象的引用计数,并仅当所有组成对象都未使用时才卸载AssetBundles。这允许应用程序在不重复内存的情况下卸载和重新加载单个对象

如果必须使用AssetBundle.Unload(False),那么只能通过两种方式卸载各个对象

  • 在场景和代码中消除对不需要的对象的所有引用。完成后,调用Resources.UnusedAsset
  • 非附加加载场景。这将销毁当前场景中的所有对象并调用Resources.UnusedAsset

发布

客户端发布项目的AssetBundles有两种基本方法:和项目一起安装或安装后再下载它们

移动项目通常选择先安装后下载,以减少初始安装大小,并保持低于相关平台下载的大小限制

和项目一起发布

最简单的方法是放到StreamingAsset文件夹中,此文件夹中包含的任何内容都将复制到最终的应用程序里

运行时可以通过属性Application.StreamingAssets访问本地存储上StreamingAsset文件夹的完整路径。然后通过AssetBundle.LoadFromFile加载

在Android上,StreamingAsset文件夹中的Asset存储在APK中,如果它们被压缩,可能需要更多的时间来加载,因为存储在APK中的文件可以使

用不同的存储算法。并且所使用的算法还可能因Unity版本而异。

安装后下载

交付AssetBundles最简单的方法是将它们放在某个Web服务器上,并通过UnityWebRequest发布。Unity将自动在本地存储上缓存下载的AssetBundles