赞
踩
目录
4.3.1 AssetBundle.LoadFromMemoryAsync
4.3.2. AssetBundle.LoadFromFile(Async)
4.3.3.UnityWebRequest 's AssetBundleDownloadHandler
4.3.4. WWW.LoadFromCacheOrDownload
深入了解 BuildPipeline.BuildAssetBundles 的三个参数
工具:Unity Asset Bundle Browser tool
教程原地址:
Assets, Resources and AssetBundles - Unity Learnhttps://learn.unity.com/tutorial/assets-resources-and-assetbundles/?_ga=2.115770310.1168700744.1642556774-152315200.1542532429&tab=overview#5c7f8528edbc2a002053b5a6注意:本教程现已弃用。我们现在建议在您的项目中使用 Addressables。同时请参考 Unity manual
Assets 和 UnityEngine.Objects 之间的区别
Asset 是磁盘上的一个文件,存储在 Unity 项目的 Assets 文件夹中。纹理、3D 模型或音频剪辑是资产的常见类型。某些 Assets 包含 Unity 原生格式的数据,例如材质。其他 Assets 需要处理成原生格式,例如 FBX 文件。
UnityEngine.Object 或带有大写“O”的对象是一组序列化数据,共同描述资源的特定实例。这可以是 Unity 引擎使用的任何类型的资源,例如 mesh, sprite, AudioClip 或 AnimationClip。所有对象都是 UnityEngine.Object 基类的子类。
Assets 和 Objects 之间是一对多的关系;也就是说,任何给定的 Assets 文件都包含一个或多个 Objects.
所有 UnityEngine.Objects 都可以引用其他 UnityEngine.Objects。这些其他Objects可能是同一个 Asset 文件中的,也可能是从其他 Asset 文件中导入的。例如,一个材质对象通常有一个或多个对纹理对象的引用。这些纹理对象通常是从一个或多个纹理Asset文件(例如 PNG 或 JPG)中导入的。
序列化时,这些引用包含两个单独的数据:文件 GUID ( File GUID )和本地 ID( Local ID )。
文件 GUID 标识存储目标资源的 Asset 文件。
一个本地唯一的 Local ID 标识Asset文件中的每个Object,因为一个Asset文件可能包含多个Object。 (注意:AA 本地 ID 不同于同一资产文件的所有其他本地 ID。)
如何查看一个material的File GUID 和 Local ID :
使用文本编辑器打开与material关联的 .meta 文件。标有“guid”的行将出现在文件顶部附近。此行定义material Asset 的 File GUID。
要查找Local ID,请在文本编辑器中打开material文件。material对象的定义如下所示:
- --- !u!21 &2100000
- Material:
- serializedVersion: 3
- ... more data …
在上面的示例中,“&”符号后面的数字是材质的Local ID。假如此材质对象位于由File GUID“abcdefg”标识的资产内,则该材质对象可以唯一标识为 File GUID“abcdefg”和Local ID “2100000”的组合。
虽然文件 GUID 和本地 ID 很强大,但相比之下 GUID 速度很慢,并且在运行时需要更高性能的系统。
Unity 内部维护一个缓存,将文件 GUID 和本地 ID 转换为简单的、会话中唯一的整数(在内部,此缓存称为 PersistentManager。)这些整数称为实例 ID,当新对象注册到缓存时,以简单的单调递增顺序分配。
缓存维护给定的实例 ID、文件 GUID 和本地 ID(定义对象源数据的位置)与内存中对象实例(如果有)之间的映射,使得UnityEngine.Objects 能稳健地维护彼此的引用。
解析 Instance ID 引用 可以很快地返回这个Instance ID 所代表的已加载的Object。如果目标对象尚未加载,则可以将文件 GUID 和本地 ID 解析为对象的源数据,从而允许 Unity 及时加载对象。
启动时,Instance ID 缓存 使用以下数据进行初始化:项目立即需要的所有Objects(即:在构建的Scenes中的引用)、Resources文件夹中包含的所有Objects。
当在 运行时 导入新 Asset 和从 AssetBundle 加载对象时,新增条目会添加到缓存中(注意:“运行时创建的Asset ” 比方说——脚本中创建的 Texture2D 对象,如: var myTexture = new Texture2D(1024,768); )。
Instance ID 条目仅在这种情况下从缓存中删除——当提供对特定 文件GUID 和 本地ID 的访问的 AssetBundle 被卸载掉。发生这种情况时,实例 ID、其文件 GUID 和本地 ID 之间的映射将被删除以节省内存。如果 AssetBundle 被重新加载,将为每一个从该AssetBundle中加载的 Object 创建一个新的 Instance ID。
在特定平台上,某些事件可能会迫使 Objects 离开内存。例如,在iOS上,当App被挂起时,图形Assets可以被从 图形内存 中卸载掉。如果这些 Object 来自一个已经被卸载的 AssetBundle,Unity 就无法重新加载这些 Objects 的源数据,所有现存的对这些 Objects 的引用也会无效。在前面的示例中,场景可能看起来具有不可见的网格或洋红色纹理。
在以下情况下会自动加载 Object:
也可以使用脚本显示加载对象,通过创建对象或调用resource-loading API (例如 AssetBundle.LoadAsset) 。加载对象时,Unity 尝试解析任何引用(reference),通过将每个reference的 文件 GUID 和本地 ID 转换为 Instance ID 。当一个 Object 的 Instance ID 第一次被解引用时,如果下面两个条件为真,将按需加载对象:
对象在三个特定场景中被卸载:
当发生 unused Asset清理时,对象会自动被卸载。这个进程是自动触发的,当场景发生破坏性更改时(即 SceneManager.LoadScene is invoked non-additively)或脚本调用 Resources.UnloadUnusedAssets API时。此过程仅卸载未引用的对象;Object 仅在以下情况下会被卸载—— 没有 Mono 变量持有对它的引用,也没有其他现存对象( live Object)持有对它的引用。此外,请注意,标为 HideFlags.DontUnloadUnusedAsset 或 HideFlags.HideAndDontSave的内容不会被卸载。
来自 Resources 文件夹的对象可以通过调用 Resources.UnloadAsset API显式卸载。这些对象的 Instance ID 仍然有效,并且仍将包含有效的文件 GUID 和 LocalID 条目。对于使用 Resources.UnloadAsset 卸载掉的 Object,如果任何 Mono 变量或其他 Object 持有对它的引用,那么一旦任何 live references被解引用,该 Object 就会重新被加载。
来自AssetBundles的对象,当调用 AssetBundle.Unload(true) API 时,会立即自动卸载。这会使对象的 Instance ID 具有的文件 GUID 和本地 ID 无效,并且对它的任何实时引用都将变为“(Missing)”引用。在 C# 脚本中,尝试访问已卸载对象的方法或属性将产生 NullReferenceException。
如果调用 AssetBundle.Unload(false),来自已卸载的 AssetBundle 的活动 Object 将不会被销毁,但 Unity 会废除其Instance ID 的具有的文件 GUID 和本地 ID。如果稍后从内存中卸载这些对象并且对已卸载对象的实时引用仍然存在,Unity 将无法重新加载这些对象。
在序列化 Unity GameObjects 的层次结构时(例如在序列化 prefabs 期间),要记住很重要的一点是,整个层次结构将被完全序列化。也就是说,层次结构中的每个 GameObject 和 Component 都将在序列化数据中逐一表示。这对加载和实例化游戏对象层次结构所需的时间产生了有趣的影响。
在创建任何 GameObject 层次结构时,CPU 时间以几种不同的方式花费:
后三个花费的时间通常是不变的,无论这个 hierarchy 是从现有的 hierarchy 结构克隆而来,还是从存储中加载进来。但是,读取源数据的时间随着序列化到 hierarchy 中的 组件 和 GameObjects 的数量线性增加,并且还乘以数据源的速度。
在所有当前平台上,从内存中的其他位置读取数据比从存储设备加载数据要快得多。此外,可用存储介质的性能特征在不同平台之间差异很大。因此,在存储速度较慢的平台上加载 prefabs 时,从存储中读取 prefabs 的序列化数据所花费的时间将大大超过实例化 prefabs 所花费的时间。也就是说,加载操作的成本与存储 I/O 时间有关。
如前所述,在序列化(庞大而单独的)预制件时,每个 GameObject 和组件的数据都是分别序列化的,这可能会复写数据。例如,具有 30 个相同元素的UI界面会把相同元素序列化 30 次,从而产生大量二进制数据。加载时,这 30 个重复元素中的每一个所包含的所有游戏对象和组件的数据都必须从磁盘读取,然后才能传输到新实例化的对象。该文件读取时间是实例化大型预制件的总成本的重要因素。
大型层次结构应该在模块化块中实例化,然后在运行时拼接在一起。
Unity 5.4 注意:Unity 5.4 改变了内存中 Transform 的表示方法。每个根 Transform 的全部子层次结构都存储在紧凑、连续的内存区域中。
当实例化一个新GameObject时,如果它要立即被置为另一个 hierarchy 的子对象(reparented into another hierarchy),请考虑使用新的接受父参数的 GameObject.Instantiate 重载变体。使用此重载可避免为新游戏对象分配root transform hierarchy 。在测试中,这将实例化操作所需的时间加快了大约 5-10%。
MonoScript 包含三个字符串:程序集名称、类名称和命名空间。
在构建项目时,Unity 将 Assets 文件夹中的所有松散脚本文件编译为 Mono 程序集。Plugins子文件夹之外的 C# 脚本放置在Assembly-CSharp.dll中。Plugins子文件夹中的脚本放置在Assembly-CSharp-firstpass.dll中,等等。
这些程序集以及预构建的程序集 DLL 文件包含在 Unity 应用程序的最终构建中。它们也是 MonoScript 所引用的程序集。与其他资源不同,Unity 应用程序中包含的所有程序集都是在 应用程序启动时 加载的。
这个 MonoScript Object 也就解释了为什么—— AssetBundle(或Scene、prefab)实际上并不包含其 MonoBehaviour 组件中的可执行代码。这允许不同的 MonoBehaviours 引用特定的共享类,即使 MonoBehaviours 位于不同的 AssetBundle 中。
Resources系统允许开发人员将 Assets 存储在一个或多个名为Resources的文件夹中,并在运行时使用Resources API从这些Assets中加载或卸载对象。
不要使用它。
提出这一强烈建议有几个原因:
有两个特定的用例,Resources系统可以在不妨碍良好开发实践的情况下提供帮助:
第二种情况的示例包括用于托管预制件的 MonoBehaviour 单例,或包含第三方配置数据的 ScriptableObjects,例如 Facebook 应用程序 ID。
项目build后,所有名为“Resources”的文件夹中的资产和对象都会合并到一个序列化文件中。该文件还包含元数据(metadata)和索引信息,类似于 AssetBundle。如AssetBundle 文档中所述,此索引包含一个序列化查找树,用于将给定对象的名称解析为其适当的文件 GUID 和本地 ID。它还用于在序列化文件正文中的特定字节偏移处定位对象。
在大多数平台上,查找数据结构是一个平衡的搜索树,它的构建时间以 O(n log(n)) 的速度增长。这种增长还导致索引的加载时间随着Resources文件夹中对象数量的增加而非线性增长。
此操作不可跳过,并且在应用程序启动时、显示初始非交互式启动屏幕时(initial non-interactive splash screen)发生。已经观察到,在低端移动设备上初始化包含 10,000 个资产的Resources系统会花费数秒时间,即使资源文件夹中包含的大多数对象实际上很少需要加载到应用程序的第一个场景中。
AssetBundle 由两部分组成:标头(Header)和数据段(data segment)。
标头包含有关 AssetBundle 的信息,例如其标识符、压缩类型和清单。清单是一个以对象名称为关键字的查找表。每个条目都提供一个字节索引,指示在 AssetBundle 的数据段中可以找到给定对象的位置。在大多数平台上,这个查找表被实现为一个平衡的搜索树。具体来说,Windows 和 OSX 衍生平台(包括 iOS)采用红黑树。因此,构建清单所需的时间将随着 AssetBundle 中资产数量的增加而线性增加。
有四个不同的 API 来加载 AssetBundle。这四个 API 的行为因两个标准而异:
Unity 的建议是不要使用此 API。
此 API 消耗的最大内存量将至少是 AssetBundle 大小的两倍:一份在 API 创建的本机内存中,一份在传递给 API 的托管字节数组中。因此,从通过此 API 创建的 AssetBundle 加载的资产将在内存中复制三次:一次在托管代码字节数组中,一次在 AssetBundle 的本机内存副本中,第三次在 GPU 或系统内存中用于资产本身.
- 教程中的说明:此方法从托管代码字节数组(C# 中的 byte[])加载 AssetBundle。它总是将源数据从托管代码字节数组复制到一个新分配的、连续的本机内存块中。如果 AssetBundle 是 LZMA 压缩的,它会在复制时解压 AssetBundle。未压缩和 LZ4 压缩的 AssetBundle 将被逐字复制。
- 手册中的说明:此函数采用包含 AssetBundle 数据的字节数组。也可以根据需要传递 CRC 值。如果捆绑包采用的是 LZMA 压缩方式,将在加载时解压缩 AssetBundle。LZ4 压缩包则会以压缩状态加载。
但是,这不是实现 LoadFromMemoryAsync 的唯一策略。File.ReadAllBytes(path) 可以替换为获得字节数组的任何所需过程。
用法示例:
- using UnityEngine;
- using System.Collections;
- using System.IO;
-
- public class Example : MonoBehaviour
- {
- IEnumerator LoadFromMemoryAsync(string path)
- {
- AssetBundleCreateRequest createRequest = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path));
- yield return createRequest;
- AssetBundle bundle = createRequest.assetBundle;
- var prefab = bundle.LoadAsset<GameObject>("MyObject");
- Instantiate(prefab);
- }
- }
AssetBundle.LoadFromFile 是一个高效的 API,用于从本地存储加载未压缩或数据块 (LZ4) 压缩的 AssetBundle,比如从硬盘或 SD 卡。(LZMA应使用UWR API。)
在桌面独立、控制台和移动平台上,API 将仅加载 AssetBundle 的标头,并将剩余数据留在磁盘上。 AssetBundle 的对象将在调用加载方法(例如 AssetBundle.Load)或解引用它们的 InstanceID 时按需加载。在这种情况下不会消耗多余的内存。
在 Unity 编辑器中,API 会将整个 AssetBundle 加载到内存中,就像从磁盘读取字节并使用 AssetBundle.LoadFromMemoryAsync 一样。如果在 Unity 编辑器中分析项目,此 API 可能会导致在 AssetBundle 加载期间出现内存峰值。这应该不影响在设备上的性能,并且应在采取补救措施之前在设备上重新测试这些峰值。
示例:
- public class LoadFromFileExample : MonoBehaviour {
- function Start() {
- var myLoadedAssetBundle
- = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetBundle"));
-
- if (myLoadedAssetBundle == null) {
- Debug.Log("Failed to load AssetBundle!");
- return;
- }
- var prefab = myLoadedAssetBundle.LoadAsset.<GameObject>("MyObject");
- Instantiate(prefab);
- }
- }
脚本使用指南:
UnityWebRequest 有一个特定的 API 处理 AssetBundle。
首先,需要使用 UnityWebRequest.GetAssetBundle
来创建 Web request。request返回后,将request对象传递给 DownloadHandlerAssetBundle.GetContent(UnityWebRequest)
。GetContent
调用将返回 AssetBundle 对象。
下载捆绑包后,还可以在 DownloadHandlerAssetBundle 类上使用 assetBundle
属性,从而以 AssetBundle.LoadFromFile
的效率加载 AssetBundle。
以下示例说明了如何加载包含两个游戏对象的 AssetBundle 并实例化这些游戏对象。
- IEnumerator InstantiateObject()
- {
- string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
- UnityEngine.Networking.UnityWebRequest request
- = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
- yield return request.Send();
- AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
- GameObject cube = bundle.LoadAsset<GameObject>("Cube");
- GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
- Instantiate(cube);
- Instantiate(sprite);
- }
更多说明:
UnityWebRequest API 允许开发人员准确指定 Unity 究竟要如何处理下载的数据,并允许开发人员消除不必要的内存使用。使用 UnityWebRequest 下载 AssetBundle 的最简单方法是调用 UnityWebRequest.GetAssetBundle.
就本指南而言,感兴趣的类是 DownloadHandlerAssetBundle。使用工作线程,它将下载的数据流式传输到固定大小的缓冲区,然后根据下载处理程序的配置方式,将缓冲的数据假脱机到临时存储或 AssetBundle 缓存。所有这些操作都发生在本机代码中,消除了扩展托管堆的风险。此外,此下载处理程序不会保留所有下载字节的本机代码副本,从而进一步减少了下载 AssetBundle 的内存开销。
LZMA 压缩的 AssetBundle 将在下载期间解压缩并使用 LZ4 压缩进行缓存。可以通过设置 Caching.CompressionEnabled 更改此行为。
下载完成后,下载处理程序的assetBundle 属性提供对下载的assetBundle 的访问,就像在下载的AssetBundle 上调用了AssetBundle.LoadFromFile 一样。
如果向 UnityWebRequest 对象提供了缓存信息,并且请求的 AssetBundle 已经存在于 Unity 的缓存中,则 AssetBundle 将立即可用,并且此 API 的操作与 AssetBundle.LoadFromFile 相同。
鉴于此方法已过时,将在未来版本中弃用,Unity建议 使用 Unity 2017.1 或更高版本的开发人员应迁移到 UnityWebRequest。如有需要自行查看手册。
通常,应尽可能使用 AssetBundle.LoadFromFile。这个 API 在速度、磁盘使用和运行时内存使用方面是最有效的。
对于LZMA包,应使用 UnityWebRequest API,因为 AssetBundle.LoadFromFile 或 AssetBundle.LoadFromFileAsync 会使用内存缓存。
UnityEngine.Objects 可以使用三个不同的 API 从 AssetBundle 加载,这些 API 都附加到 AssetBundle 对象,它们具有同步和异步变体:
这些 API 的同步版本总是比异步版本 快 至少一帧。异步加载将在每帧加载多个对象,直到它们的时间片限制。
通用代码片段:
T objectFromBundle = bundleObject.LoadAsset<T>(assetName);
同步方法
- //加载单个游戏对象:
- GameObject gameObject = loadedAssetBundle.LoadAsset<GameObject>(assetName);
- //加载所有资源:
- Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets();
异步方法
异步方法返回 AssetBundleRequest。在访问资源之前,需要等待此操作完成。加载资源:
- //加载单个对象:
- AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<GameObject>(assetName);
- yield return request;
- var loadedAsset = request.asset;
-
-
- //加载多个对象:
- AssetBundleRequest request = loadedAssetBundle.LoadAllAssetsAsync();
- yield return request;
- var loadedAssets = request.allAssets;
-
加载资源后,就可以开始了!可以像使用 Unity 中的任何对象一样使用加载的对象。
同步的 AssetBundle.Load 方法将暂停主线程,直到对象加载完成。他们还将对对象加载进行时间切片,以便对象集成不会占用超过一定毫秒数的帧时间。毫秒数由属性 Application.backgroundLoadingPriority 设置:
【manual page】https://docs.unity3d.com/Manual/AssetBundles-Dependencies.html当父 AssetBundle 中有一个或多个 UnityEngine.Objects 引用另一个 AssetBundle 中一个或多个 UnityEngine.Objects 时,一个 AssetBundle 依赖于另一个 AssetBundle。
如果一个UnityEngine.Object包含对另一个UnityEngine.Object的引用,而另一个Object没有分配在任何AssetsBundle中,不会发生依赖关系。在这种情况下,在构建 AssetBundle 时,包所依赖的对象的副本将复制到包中。(如果多个包中的多个对象都包含对同一个对象的引用,而这个对象没有被分配到任何Bundle中,那么,每个依赖于该对象的Bundle都将创建该对象的副本,并将其打包到构建的 AssetBundle 中。)
如果将公共资源分配到了单独的 AssetBundle ,要小心依赖关系。特别是,如果您只使用一个Prefab来实例化某个模块,材质不会加载。「图:没有加载材质的Prefab」
要解决这个问题,应该在加载属于这个模块的 AssetBundle 之前,先将 Materials的 AssetBundle 加载到内存中。
加载 AssetBundle 的顺序并不重要。但是,在加载 Object 本身之前,加载所有包含 Object 依赖项的 AssetBundle 很重要。Unity 不会尝试自动加载依赖项。
例如:
假设 材质A 引用 纹理B。 材质A 被打包到 AssetBundle 1 中,纹理B 被打包到 AssetBundle 2 中。
这种情况下,在从 AssetBundle 1 加载材料 A 之前,必须先加载 AssetBundle 2 。
这并不意味着必须在加载 AssetBundle 1 之前加载 AssetBundle 2,或者必须从 AssetBundle 2 显式加载纹理 B。在从 AssetBundle 1 加载材质 A 之前加载 AssetBundle 2 就足够了。
但是,加载 AssetBundle 1 时,Unity 不会自动加载 AssetBundle 2。这必须在脚本代码中手动完成。
加载 AssetBundle 清单可能非常有用。特别是在处理 AssetBundle 依赖关系时。
要获得可用的 AssetBundleManifest 对象,需要加载另外的 AssetBundle(与其所在的文件夹名称相同的那个)并从中加载 AssetBundleManifest 类型的对象。
加载清单本身的操作方法与 AssetBundle 中的任何其他资源完全相同:
- AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
- AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
至此,可以使用manifest对象来获取有关您构建的 AssetBundle 的信息,包括 AssetBundle 的依赖项数据、哈希数据和变体数据。 manifest对象可以动态地查找加载依赖项。假设我们想要为名为“assetBundle”的 AssetBundle 加载所有依赖项:
- AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
- AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
- string[] dependencies = manifest.GetAllDependencies("assetBundle"); //传递想要依赖项的捆绑包的名称。
- foreach(string dependency in dependencies)
- {
- AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, dependency));
- }
教程中的讲解:
当使用 BuildPipeline.BuildAssetBundles API 执行 AssetBundle 构建管道时,Unity 会序列化一个Object,它包含每个 AssetBundle 的依赖信息。它存储在单独的 AssetBundle 中,其中只包含一个AssetBundleManifest 类型的 Object。
存储这个Asset 的 AssetBundle,它与正在构建的AssetBundle所在的文件夹同名。如果项目把 AssetBundles 构建到 (projectroot)/build/Client/ 的目录中,则包含清单的 AssetBundle 将保存为 (projectroot)/build/Client/Client.manifest。
包含清单的 AssetBundle 可以像任何其他 AssetBundle 一样被加载、缓存和卸载。
AssetBundleManifest 对象本身提供了 GetAllAssetBundles API 来列出与清单同时构建的所有 AssetBundles,以及两种查询特定 AssetBundle 依赖关系的方法:
请注意,这两个 API 都分配字符串数组。因此,它们应该只被谨慎使用,而不是在应用程序生命周期的性能敏感部分。
在许多情况下,最好在玩家进入应用程序的性能关键区域(例如主游戏关卡或世界)之前加载尽可能多的所需对象。这在移动平台上尤其重要,在这些平台上,对本地存储的访问速度很慢,而且在play-time 加载、卸载对象造成的内存波动 会触发GC。
在内存敏感的环境中谨慎控制加载对象的大小和数量至关重要。当从活动场景中移除对象时,Unity 不会自动卸载对象。资产清理(Asset cleanup)在特定时间触发,也可以手动触发。
AssetBundles 本身必须小心管理。由本地存储上的文件所支持的的 AssetBundle(包括在 Unity 缓存中的,或通过 AssetBundle.LoadFromFile 加载的)具有最小的内存开销,很少消耗超过几十 KB。但是,如果存在大量 AssetBundle,这种开销仍然会成为问题。
由于大多数项目允许用户重新体验内容(例如重玩关卡),因此了解何时加载或卸载 AssetBundle 非常重要。如果 AssetBundle 卸载不当,可能会导致内存中的对象重复。在某些情况下,不正确地卸载 AssetBundle 也会导致不符合预期的表现,例如导致纹理丢失。
管理资产和 AssetBundle 时,最重要的一点是要理解调用 AssetBundle.Unload 时,参数“unloadAllLoadedObjects” 使用 true 和 false 的行为差异。
此 API 将卸载正在调用的 AssetBundle 的标头信息。 unloadAllLoadedObjects 参数决定了是否也卸载从此 AssetBundle 实例化的所有对象。如果设置为 true,则来自 AssetBundle 的所有对象也将立即卸载——即使它们当前正在活动场景中使用。
例如,假设材质 M 是从 AssetBundle AB 加载的,并假设 M 当前处于活动场景中。
如果调用 AssetBundle.Unload(true),则 M 将从场景中移除、销毁并卸载。但是,如果调用 AssetBundle.Unload(false),则 AB 的标头信息将被卸载,但 M 将保留在场景中,仍然可以正常工作。调用 AssetBundle.Unload(false) 会断开 M 和 AB 之间的链接。如果稍后再次加载 AB,则 AB 中包含的 Objects 的新副本将加载到内存中。
如果稍后再次加载 AB,则将重新加载 AssetBundle 标头信息的新副本。但是,M 没有从这个新的 AB 副本中加载。 Unity 不会在 AB 和 M 的新副本之间建立任何链接。
如果调用 AssetBundle.LoadAsset() 来重新加载 M,Unity 不会将 M 的旧副本解释为 AB 中数据的实例。因此,Unity 将加载 M 的一个新副本,并且场景中将有两个相同的 M 副本。
对于大多数项目,这种行为是不可取的。大多数项目应该使用 AssetBundle.Unload(true) 并采用一种方法来确保 Objects 不重复。两种常见的方法是:
如果应用程序必须使用 AssetBundle.Unload(false),那么单个 Objects 只能通过两种方式卸载:
消除对不需要的对象的所有引用,包括场景和代码中。之后调用 Resources.UnloadUnusedAssets.
以非附加方式加载场景(non-additively)。这将销毁当前场景中的所有对象并自动调用 Resources.UnloadUnusedAssets.
如果项目有明确定义的点,可以让用户等待对象加载和卸载,例如在游戏模式或关卡之间,这些点应该用于根据需要卸载尽可能多的对象并加载新的对象。
最简单的方法是将项目的离散块打包到场景中,然后将这些场景连同它们的所有依赖项一起构建到 AssetBundles 中。然后应用程序可以进入“Loading”场景,完全卸载包含旧场景的 AssetBundle,然后加载包含新场景的 AssetBundle。
虽然这是最简单的流程,但有些项目需要更复杂的 AssetBundle 管理。由于每个项目都不同,因此没有通用的 AssetBundle 设计模式。
在决定如何将对象分组到 AssetBundle 中时,如果必须同时加载或更新它们,通常最好先将它们捆绑到 AssetBundle 中。例如,考虑一个RPG游戏。单个地图和过场动画可以按场景分组到 AssetBundle 中。但有些Objects在大多数场景中都需要,可以构建一个 AssetBundle 以提供立绘、游戏内 UI 以及不同的角色模型和纹理,然后将后面的这些对象和资产分组到第二种 AssetBundle 中,在启动时加载并在应用程序的生命周期内保持加载状态。
还有一个问题可能会出现,当一个 AssetBundle 被卸载之后,如果 Unity 必须从这个 AssetBundle 中重新加载一个对象。这时,重新加载将失败,并且该对象将作为(Missing)对象出现在 Unity 编辑器的hierarchy视图中。
这主要发生在 Unity 失去并重新获得对其图形上下文的控制时,例如当移动应用程序暂停或用户锁定他们的 PC 时。在这种情况下,Unity 必须将纹理和着色器重新上传到 GPU。如果这些资产的源 AssetBundle 不可用,应用程序会将场景中的对象呈现为洋红色。
与项目一起提供 AssetBundle 是分发它们的最简单方法,因为它不需要额外的下载管理代码。项目可能在安装中包含 AssetBundle 有两个主要原因:
如果想要在安装 Unity 应用程序时,就包含任何类型的内容(包括 AssetBundle),最简单方法是:在构建项目前,将内容构建到 /Assets/StreamingAssets/ 文件夹中。构建时 StreamingAssets 文件夹中包含的任何内容都将复制到最终应用程序中。
在运行时,StreamingAssets 文件夹在本机存储上的完整路径 可通过属性 Application.streamingAssetsPath 访问。然后在大多数平台上,可以通过 AssetBundle.LoadFromFile 加载 AssetBundle。
注意:StreamingAssets在某些平台上不是可写位置。如果安装后需要更新项目的 AssetBundle,请使用 WWW.LoadFromCacheOrDownload 或编写自定义下载器。
要决定如何将项目的Assets划分为 AssetBundle 并不简单。采用简单的策略是很诱人的,例如所有对象都放在仅包含它们自己的 AssetBundle,或只使用一个 AssetBundle,但这些解决方案具有明显的缺点:
决定如何将对象分组到 AssetBundles 中很关键,主要策略是:
最常见的用例是针对基于场景的捆绑包,应包含大部分或全部场景依赖项。
请注意,绝对可以根据需求混用这些策略,而且也应当这么做。使用最优策略可以大大提高项目效率。
无论遵循何种策略,下面这些额外提示都有助于掌控全局:
当对象构建到 AssetBundle 中时,Unity 5 的 AssetBundle 系统会查找对象的所有依赖项。这是使用资源数据库完成的。此依赖关系信息用于确定包含在 AssetBundle 中的对象集。
显式分配给 AssetBundle 的对象将仅构建到该 AssetBundle 中。“显式分配”是指:对象的 AssetImporter 的 assetBundleName 属性设置为非空字符串。
任何没有被显式分配到 AssetBundle 中的对象,将被包含在所有「包含(一个或多个)引用了该“未标记对象”的对象」的 AssetBundle 中。
例如,如果将两个不同的对象分配给两个不同的 AssetBundle,但它们都引用了一个公共的依赖对象,那么该依赖对象将被复制到两个 AssetBundle 中。复制的依赖也将被实例化,这意味着依赖对象的两个副本将被视为具有不同标识符的不同对象。这将增加应用程序 AssetBundle 的总大小。如果应用程序加载它的两个父对象,这也会导致对象的两个不同副本被加载到内存中。
有几种方法可以解决这个问题:
确保构建到不同 AssetBundle 中的对象不共享依赖项。任何共享依赖关系的对象都可以放入同一个 AssetBundle 中,而不会复制它们的依赖关系。
这种方法通常不适用于具有许多共享依赖项的项目。它会生成很大的单体 AssetBundle,必须频繁地重新构建和重新下载,这既不方便也不高效。
将 AssetBundle 分段,以便不会同时加载共享依赖项的两个 AssetBundle。
此方法可能适用于某些类型的项目,例如基于关卡的游戏。但是,它仍然不必要地增加了项目 AssetBundle 的大小,并增加了构建时间和加载时间。
将公共资源(例如材质)分配到只属于它们自己的 AssetBundle。这完全消除了重复资产的风险,但也引入了复杂性。应用程序必须跟踪 AssetBundle 之间的依赖关系,并确保在调用任何 AssetBundle.LoadAsset API 之前,已经加载了正确的 AssetBundle。
对象依赖项通过 AssetDatabase API 进行跟踪( 属于UnityEditor命名空间)。正如命名空间所暗示的,此 API 仅在 Unity 编辑器中可用,而不在运行时可用。 AssetDatabase.GetDependencies 可用于定位指定Object或Asset的所有直接依赖项。请注意,这些依赖项可能有自己的依赖项。此外,AssetImporter API 可用于查询任何特定对象分配到了哪个 AssetBundle。
通过组合 AssetDatabase 和 AssetImporter API,可以编写一个编辑器脚本,以确保一个 AssetBundle 的所有直接或间接依赖项都分配了 AssetBundle,或者没有两个 AssetBundle 共享尚未分配到 AssetBundle 的依赖项。由于复制资产的内存成本,建议所有项目都有这样的脚本。
以下部分将介绍 Unity 5 的资源依赖性计算代码在与自动生成的精灵图集结合使用时出现的奇怪行为。
任何 自动生成的精灵图集 都将与生成图集的 Sprite 对象一起分配到同一个 AssetBundle。*(automatically-generated sprite atlas,个人理解为使用sprite packer生成的图集,而不是在外部制作好并导入unity的图集)
为确保精灵图集不重复,请检查标记到同一精灵图集中的所有精灵都分配给同一个 AssetBundle。
AssetBundle 系统的一个关键特性是引入了 AssetBundle Variants。变体的目的是允许应用程序调整其内容以更好地适应它的运行环境。变体允许不同 AssetBundle 文件中的不同 UnityEngine.Object 在加载Objects、解析Instance ID引用时,显示为“相同”对象。从概念上讲,它允许两个 UnityEngine.Object 看起来共享相同的文件 GUID 和本地 ID,并通过字符串 Variant ID 标识要加载的实际 UnityEngine.Object。
该系统有两个主要用例:
变体简化了适用于给定平台的 AssetBundle 的加载。
示例:构建系统可能会创建一个 AssetBundle,其中包含适用于独立 DirectX11 Windows 构建的高分辨率纹理和复杂着色器,以及另一个用于 Android 的具有较低保真度内容的 AssetBundle。在运行时,项目的资源加载(resource loading)代码可以为其平台加载适当的 AssetBundle Variant,传递到 AssetBundle.Load API 的对象名称不需要更改
变体允许应用程序在 使用不同硬件的同一平台上 加载不同内容。
这是支持各种移动设备的关键。 iPhone 4 无法在任何实际应用程序中显示与最新 iPhone 相同的内容保真度。在 Android 上,AssetBundle Variants 可用于解决设备之间屏幕纵横比和 DPI 的巨大碎片化问题。
AssetBundle Variant 系统的一个关键限制是:它要求从不同的Assets构建Variants。即使这些Assets 之间的唯一变化是它们的导入设置。如果将一个Texture 构建到 Variant A 和 Variant B 中、唯一区别是 Unity 纹理导入器中选择的特定纹理压缩算法,Variant A 和 Variant B 仍然必须是完全不同的 Assets。这意味着变体 A 和变体 B 必须是磁盘上单独的文件。
此限制使大型项目的管理变得复杂,因为必须将特定资产的多个副本保持在源代码控制中。当开发人员想要更改Asset的内容时,必须更新Asset的所有副本。没有针对此问题的内置解决方法。
大多数团队都用自己的方式实现了AssetBundle 变体。这是通过「在构建的AssetBundle文件名后面附加明确定义的后缀」来完成的,以便识别给定 AssetBundle 所代表的特定变体。自定义代码以编程方式,在构建这些 AssetBundle 时,更改其所包含的Assets的导入设置。一些开发人员已经扩展了他们的自定义系统,以便能够更改附加到预制件的组件的参数。
是否压缩 AssetBundle 需要考虑几个重要的因素,其中包括:
主要由使用了 Crunch 压缩算法的 DXT-compressed textures 组成的Bundle应构建为未压缩。
Unity 默认使用 LZMA 压缩来创建 AssetBundle,然后通过 LZ4 压缩将其缓存。
LZMA
Unity 的 AssetBundle build pipeline 通过 LZMA 压缩来创建 AssetBundle。此压缩格式是一个表示整个 AssetBundle 的数据流,这意味着如果要从中读取某个Asset,就必须解压缩整个数据流。这是从内容分发网络 (CDN) 下载 AssetBundle 的首选格式,因为文件大小小于使用 LZ4 压缩的文件。
LZ4
而LZ4 压缩是一种基于块的压缩算法。如果 Unity 要从 LZ4 存档中访问一个Asset,只需解压缩并读取包含Asset的字节的块。这是 Unity 在其「两种 AssetBundle 缓存中」都使用的压缩方法。在构建 AssetBundle 时,要强制进行 LZ4(HC) 压缩时,请使用 BuildAssetBundleOptions.ChunkBasedCompression 值。
使用 BuildAssetBundleOptions.UncompressedAssetBundle 构建的无压缩的 AssetBundle 无需解压缩,但会占用更多磁盘空间。
Unity 有两种缓存,用于(在使用 WWW或 UnityWebRequest (UWR)时)优化 LZMA 资源包 的提取、再压缩和版本控制。
内存缓存——以 UncompressedRuntime 格式将 AssetBundle 存储在 RAM 中。
磁盘缓存——将提取的 AssetBundle 以下文描述的压缩格式存储在可写介质中。
将 AssetBundle 加载到内存缓存中会耗用大量的内存。除非您特别希望频繁且快速地访问 AssetBundle 的内容,否则内存缓存的性价比可能不高。因此,应改用磁盘缓存。
如果向 UnityWebRequest API 提供了version参数(可以是版本号或哈希),AssetBundle 数据将存储在磁盘缓存中。否则使用内存缓存。
初次加载缓存的 LZMA AssetBundle 花费的时间更长,因为 Unity 必须将存档重新压缩为目标格式。随后的加载将使用缓存版本。
对于LZMA包,应使用 UnityWebRequest API,因为 AssetBundle.LoadFromFile 或 AssetBundle.LoadFromFileAsync 会使用内存缓存。如果无法使用 UWR API,您可以使用 AssetBundle.RecompressAssetBundleAsync 将 LZMA AssetBundle 重写到磁盘中。
手册内容
要将指定的的Asset分配给 AssetBundle,请执行以下步骤:
/
分隔文件夹名称。 例如,使用 AssetBundle 名称 environment/forest
,就会在 environment
子文件夹下创建名为 forest
的捆绑包。在 Assets 文件夹中创建一个名为 Editor 的文件夹,并将包含以下内容的脚本放在该文件夹中:
- using UnityEditor;
- using System.IO;
-
- public class CreateAssetBundles
- {
- [MenuItem("Assets/Build AssetBundles")]
- static void BuildAllAssetBundles()
- {
- string assetBundleDirectory = "Assets/AssetBundles";
- if(!Directory.Exists(assetBundleDirectory))
- {
- Directory.CreateDirectory(assetBundleDirectory);
- }
- BuildPipeline.BuildAssetBundles(assetBundleDirectory,
- BuildAssetBundleOptions.None,
- BuildTarget.StandaloneWindows);
- }
- }
此脚本将在 Assets 菜单底部创建一个名为 Build AssetBundles 的菜单项,该菜单项将执行与这个标签关联的函数中的代码。单击 Build AssetBundles 时,将随build对话框一起显示一个进度条。这将获取所有你标记了 AssetBundle 名称的Assets,并将它们放在 assetBundleDirectory
所指的文件夹中。
成功构建了 AssetBundle后,您可能会注意到 AssetBundles 目录包含的文件数量超出了最初的预期。确切地说,是多出了 2*(n+1) 个文件。让我们花点时间详细了解一下 BuildPipeline.BuildAssetBundles
产生的结果。
对于在编辑器中指定的每个 AssetBundle,可以看到一个具有 AssetBundle 名称+“.manifest”的文件。
还有一个额外Bundle和manifest的名称不同于先前创建的任何 AssetBundle,而是以其所在的目录(也是这些 AssetBundle 的build目录)同名。这就是清单包(Manifest Bundle)。
BuildAssetBundleOptions
,但有三个特定的 BuildAssetBundleOptions
可以处理 AssetBundle 压缩:
| LZMA 格式压缩。它可以得到最小的文件大小,但由于需要解压缩,加载时间略长。 LZMA 要求在Bundle在使用前,需要解压缩。也就是说,要想使用包中的任何资源,必须首先解压缩整个包。 建议:将LZMA 压缩仅用于「从异地主机初次下载 AssetBundle 时」,因为它压缩的文件最小。 通过 UnityWebRequestAssetBundle 加载的 LZMA 的 AssetBundle 会自动重新压缩为 LZ4 ,并缓存在本地文件系统上。如果通过其他方式下载并存储包,则可以使用 AssetBundle.RecompressAssetBundleAsync API 对其进行重新压缩。 |
|
| 数据完全未压缩。 缺点:文件下载大小增大。优点:下载后的加载时间会快得多。 |
|
| LZ4 压缩法。压缩后,文件大小比 LZMA 大,但不像 LZMA 那样需要解压缩整个包才能使用。 LZ4 使用基于块的算法,允许按段或“块”加载 AssetBundle。解压缩单个块即可使用包含的资源,即使该 AssetBundle 中的其他块未解压缩也不影响。 加载时间与未压缩大致相当,额外的优势是减小了磁盘占用的大小。 |
|
EditorUserBuildSettings.activeBuildTarget
,它将自动找到当前设置的目标构建平台,并根据该目标构建 AssetBundle。如果您想从本地存储中加载,请使用 AssetBundles.LoadFromFile
API,如下所示:
- public class LoadFromFileExample : MonoBehaviour {
- void Start() {
- var myLoadedAssetBundle
- = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetBundle"));
- if (myLoadedAssetBundle == null) {
- Debug.Log("Failed to load AssetBundle!");
- return;
- }
- var prefab = myLoadedAssetBundle.LoadAsset<GameObject>("MyObject");
- Instantiate(prefab);
- }
- }
LoadFromFile 需要bundle文件的路径.
如果您自己托管 AssetBundle 并需要将它们下载到您的应用程序中,请使用 UnityWebRequestAssetBundle API。例:
- IEnumerator InstantiateObject()
- {
- string url = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
- UnityEngine.Networking.UnityWebRequest request
- = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(url, 0);
- yield return request.Send();
- AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
- GameObject cube = bundle.LoadAsset<GameObject>("Cube");
- GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
- Instantiate(cube);
- Instantiate(sprite);
- }
GetAssetBundle(string, int)
使用 AssetBundle 的位置 URL 和 你想下载的包的版本。此示例仍然指向一个本地文件,但 string url
可以指向托管 AssetBundle 的任何 URL。
UnityWebRequest 类有一个特定的句柄来处理 AssetBundle:DownloadHandlerAssetBundle
,可根据请求获取 AssetBundle。
无论使用哪种方法,现在您都可以访问 AssetBundle 对象了。对该对象需要使用 LoadAsset<T>(string)
,此函数需要您尝试加载的Asset的类型 T
,以及包里面对象的名称(作为string)。它会返回你从 AssetBundle 加载的任何对象。这些返回的对象,你可以像使用 Unity 中的任何对象一样使用它们。例如,如果你想在场景中创建一个GameObject,只需调用 Instantiate(gameObjectFromAssetBundle)
。
下载链接:Asset Bundle Browserhttps://github.com/Unity-Technologies/AssetBundles-Browser
https://github.com/Unity-Technologies/AssetBundles-Browser.git
Package Manager 下载并安装 这个包的 “master” 分支。
使用说明页面:
非常通俗易懂的原理讲解
程序丨入门必看:Unity资源加载及管理Unity的资源加载及管理,基础很重要。https://mp.weixin.qq.com/s/0XFQt8LmqoTxxst_kKDMjw?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。