「翻译」Unity中的AssetBundle详解(四)
AssetBundle依赖关系
如果一个或多个UnityEngine.Objects包含位于另一个bundle中的UnityEngine.Object的引用,则AssetBundles可以依赖于其他AssetBundles。如果UnityEngine.Object包含一个在其他任何AssetBundle中都不包含的UnityEngine.Object的引用,则不会发生依赖关系。在这种情况下,在构建AssetBundles时,将bundle所依赖的对象的副本复制到捆绑包中。如果多个bundle中的多个对象包含对未分配给bundle的同一对象的引用,那么对该对象具有依赖关系的每个bundle将各自制作一个该对象的副本并将其打包到内置的AssetBundle中。
如果AssetBundle包含依赖关系,则在加载要尝试实例化的对象之前,加载包含这些依赖关系的bundles是重要的。 Unity不会尝试自动加载依赖关系。
考虑以下示例,Bundle 1中的材料引用了Bundle 2中的纹理:
在此示例中,加载Bundle 1中的Material之前,需要将Bundle 2加载到内存中。Bundle 1和Bundle 2的加载顺序无关紧要,重要的是在从Bundle 1加载Material之前加载Bundle 2。在下一节中,我们将讨论如何使用在上一篇博客中所涉及的AssetBundleManifest对象,在运行时确定和加载依赖关系。
使用本地AssetBundles
在 Unity 5 中,我们可以使用四种不同的 API 来加载 。它们的行为根据正在加载的平台和 AssetBundles 构建时使用的压缩方式(未压缩,LZMA,LZ4)而有所不同。
我们需要用到的四个 API 是:
- AssetBundle.LoadFromMemoryAsync
- AssetBundle.LoadFromFile
- WWW.LoadfromCacheOrDownload
- UnityWebRequest’s DownloadHandlerAssetBundle (Unity 5.3 或者更高的版本)
AssetBundle.LoadFromMemoryAsync
AssetBundle.LoadFromMemoryAsync
此函数使用包含 AssetBundle 数据的字节数组的参数。如果需要也可以传递一个 CRC 值。如果 Bundle 是LZMA 压缩的,它将在加载时解压缩 AssetBundle。 LZ4 压缩的 Bundle 在压缩状态时被加载。
以下是使用此方法的一个示例:
1 | IEnumerator LoadFromMemoryAsync(string path) |
但是,这不是唯一可以使用 LoadFromMemoryAsync 的策略。 可以用任何获取所需的字节数组的过程替代File.ReadAllBytes(path)
方法。
AssetBundle.LoadFromFile
AssetBundle.LoadFromFile
从本地存储加载未压缩的 Bundles 时,此API非常高效。如果 Bundle 是未压缩或块(LZ4)压缩的,LoadFromFile 将直接从磁盘加载 Bundle。使用此方法加载完全压缩(LZMA)的 Bundle 将首先解压缩包,然后再将其加载到内存中。
如何使用 LoadFromFile
的一个例子:
1 | public class LoadFromFileExample extends MonoBehaviour { |
注意:在使用 Unity 5.3 或更早版本的Android设备上,尝试从 Streaming Assets 路径加载 AssetBundles 时,此API将失效。这是因为该路径的内容将驻留在压缩的 .jar
文件中。 Unity 5.4和更新版本可以使用这个API 加载 Streaming Assets 路径下的资源。
WWW.LoadFromCacheOrDownload
WWW.LoadFromCacheOrDownload
**此 API 将被废弃 (请使用 UnityWebRequest) **
此API可用于从远程服务器下载 AssetBundles 或加载本地 AssetBundles。它是 UnityWebRequest API 的较旧且不太理想的版本。
从远程位置加载 AssetBundle 将自动缓存 AssetBundle。如果AssetBundle被压缩,那么一个工作线程将自动解压缩该包并将其写入缓存。一旦捆绑包解压缩并缓存,它将像 AssetBundle.LoadFromFile 一样加载。
如何使用 LoadFromCacheOrDownload
的一个例子:
1 | using UnityEngine; |
由于缓存 AssetBundle 在 WWW 对象中的字节的内存开销,建议使用 WWW.LoadFromCacheOrDownload
的所有开发人员确保其 AssetBundles 保持较小——最多为几兆字节。还建议在限制内存平台(如移动设备)上运行的开发人员确保其代码一次只下载一个 AssetBundle,以避免内存尖峰。
如果缓存文件夹没有用于缓存附加文件的空间,LoadFromCacheOrDownload将从缓存中迭代删除最近最少使用的AssetBundle,直到有足够的空间可用于存储新的AssetBundle。如果无法进行空间(因为硬盘已满,或者当前正在使用缓存中的所有文件)释放,LoadFromCacheOrDownload() 将绕过缓存并将文件以流的形式加入内存。
为了强制 LoadFromCacheOrDownload,版本参数(第二个参数)将需要更改。如果传递给该函数的版本与当前缓存的AssetBundle的版本匹配,则AssetBundle将仅从缓存加载。
UnityWebRequest
UnityWebRequest
UnityWebRequest 有一个特定的 API 调用来处理 AssetBundles。首先,你需要使用
UnityWebRequest.GetAssetBundle 创建您的 Web 请求。返回请求后,将请求对象传递给DownloadHandlerAssetBundle.GetContent(UnityWebRequest)
。此 GetContent
函数调用将返回AssetBundle 对象。
你还可以在下载 Bundle 后使用 DownloadHandlerAssetBundle 类中的 assetBundle
属性,以AssetBundle.LoadFromFile
的效率加载 AssetBundle。
以下是一个如何加载包含两个 GameObject 的 AssetBundle 并实例化它们的示例。要开始这个过程,我们只需要调用 StartCoroutine(InstantiateObject())
;
1 | IEnumerator InstantiateObject() |
使用 UnityWebRequest 的优点是它允许开发人员以更灵活的方式处理下载的数据,并可能消除不必要的内存使用情况。这是 UnityEngine.WWW 类中首选的 API。
从 AssetBundles 载入资产
现在,您已经成功下载了 AssetBundle,是时候最终加载某些资产了。
通用代码段:
1 | T objectFromBundle = bundleObject.LoadAsset<T>(assetName); |
T
是尝试加载的资产的类型。
决定如何加载资产时,有几种选择。分别是 LoadAsset 、LoadAllAssets 和它们的对应的异步方法 LoadAssetAsync 和 LoadAllAssetsAsync。
下面代码展示了如何从AssetBundles同步加载资产:
加载单个 GameObject:
1 | GameObject gameObject = loadedAssetBundle.LoadAsset<GameObject>(assetName); |
加载所有资产:
1 | Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets(); |
现在,正如上文所示的方法返回的对象类型或要加载的对象的数组,异步方法返回一个 AssetBundleRequest。访问资产之前,需要等待此操作完成。加载一个 Asset:
1 | AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<GameObject>(assetName); |
以及
1 | AssetBundleRequest request = loadedAssetBundle.LoadAllAssetsAsync(); |
一旦你加载了你所需要的 Assets,接下来就可以像使用 Unity 中任何其他对象一样使用加载的对象。
加载 AssetBundle 清单文件(Manifests)
加载 AssetBundle 清单文件非常有用。特别是在处理 AssetBundle 依赖项时。
要获得一个可用的 AssetBundleManifest 对象,你需要加载额外的 AssetBundle(名称与其所在文件夹相同),并从中加载类型为 AssetBundleManifest 的对象。
加载清单本身与加载 AssetBundle 中的任何其他资产完全相同:
1 | AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath); |
现在,你可以通过上述示例中的 manifest 对象访问 AssetBundleManifest
API 的调用。从这里你可以使用清单来获取有关你所构建的 AssetBundles 的信息。该信息包括 AssetBundles 的依赖关系数据、散列数据和变体数据。
在前面的部分,当我们讨论 AssetBundle Dependencies 并且如果一个 bundle 对另一个 bundle 有依赖关系,那么在从原始 bundle 加载任何资源之前,需要加载这些 bundle。清单对象使得动态地找到加载依赖性成为可能。假设我们要加载名为 “assetBundle” 的 AssetBundle 的所有依赖项。
1 | AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath); |
上面代码表示正在加载 AssetBundles、AssetBundle依赖关系和资产,现在是时候讨论管理所有这些加载的 AssetBundles 了。
管理加载的 AssetBundles
另请参阅:Unity 官方教程中有关 Managing Loaded AssetBundles的教程。
当它们从活动场景中删除时,Unity不会自动卸载对象。资产清理在特定时间触发,也可以手动触发。知道何时加载和卸载 AssetBundle 很重要。不正确卸载 AssetBundle 可能会导致内存中的对象复制或其他不合要求的情况(如缺少纹理)。
了解 AssetBundle 管理最重要的是什么时候调用 AssetBundle.Unload(bool)
,并且在函数调用时你应该将 true 或 false 作为参数传递给函数。卸载 AssetBundle 是一个非静态的功能,此 API 卸载正在调用的 AssetBundle 的头部信息。该函数的参数表示是否也卸载从此 AssetBundle 实例化的所有对象。
如果使用 AssetBundle.Unload(true)
方法,就会卸载从 AssetBundle 中加载的所有对象,即使它们正在当前活动的场景中使用。这正是我们前面提到的,这可能会导致纹理丢失。我们假设材质 M 是从 AssetBundle AB 加载的,如下所示。如果调用 AB.Unload(true)。活动场景中的任何 M 实例也将被卸载和销毁。如果你改为调用 AB.Unload(false),它会破坏当前 M 和 AB 实例的链接。
如果以后再次加载 AB,并调用 AB.LoadAsset(),Unity 将不会将 M 的现有副本重新链接到新加载的材质。那么 M 会加载两个副本。
一般来说,使用 AssetBundle.Unload(false) 并不会导致理想的情况。大多数项目应该使用 AssetBundle.Unload(true) 来避免在内存中复制对象。
大多数项目应该使用 AssetBundle.Unload(true) 并采用一种方法来确保对象不被重复。两种常见的方法是:
- 在应用程序的生命周期中具有明确定义的地方,卸载临时 AssetBundles,例如关卡之间或加载屏幕期间。
- 维护单个对象的引用计数,并仅在其所有组成对象未使用时卸载 AssetBundles。这允许应用程序卸载和重新加载单个对象而没有重复的内存。
如果应用程序必须使用 AssetBundle.Unload(false),那么单个对象只能以两种方式卸载:
- 在场景和代码中消除对不需要的对象的所有引用。完成之后,调用 Resources.UnloadUnusedAssets。
- 使用非叠加式加载场景。这将销毁当前场景中的所有对象并自动调用 Resources.UnloadUnusedAssets。
如果你不想自己管理加载 AssetBundles、依赖关系和 Assets 本身,你可能会发现自己需要 AssetBundle Manager。
原文链接:
- AssetBundle Dependencies
- Using AssetBundles Natively
同系列文章
「翻译」Unity中的AssetBundle详解(一)
「翻译」Unity中的AssetBundle详解(二)
「翻译」Unity中的AssetBundle详解(三)
「翻译」Unity中的AssetBundle详解(四)
「翻译」Unity中的AssetBundle详解(五)