Unity最佳实践-AssetBundle使⽤模式
Best Practices(5) - AssetBundle usage patterns
适⽤版本:2017.3
原⽂地址:
本系列的前⼀章介绍了AssetBundles的基础知识,其中包括各种加载API的底层⾏为。 本章讨论在实践中使⽤AssetBundles的各个⽅⾯的问题和可能的解决⽅案。
管理已加载的资产免检车辆
AssetBundles本⾝必须⼩⼼管理。由本地存储上的⽂件(在Unity缓存中或通过AssetBundle.LoadFromFile加载的⽂件)⽀持的AssetBundle具有最⼩的内存开销(这⾥应该是说,如果AB来⾃本地,加载AB只会加载头⽂件,相较于从⽹络上下载的AB),很少超过⼏万字节。但是,如果存在⼤量的AssetBundles,则此项开销仍是⼀个⼤问题。
由于⼤多数项⽬允许⽤户重新体验内容(例如重新调整关卡),因此知道何时加载或卸载AssetBundle⾮常重要。如果AssetBundle卸载不当,可能会导致内存中的对象重复。在某些情况下不当卸载AssetBundles也会导致不良⾏为,例如导致纹理丢失。要理解为什么会发⽣这种情况,请参阅的“对象间引⽤”部分。
管理资产和AssetBundles时要了解的最重要的⼀点是,在为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中包含的对象的新副本将被加载到内存中。
如果稍后再次加载AB,则将重新加载AssetBundle头⽂件的新副本。 但是,M并未从AB的这个新副本中加载。 Unity没有在AB和M的新副本之间建⽴任何关联。
如果调⽤AssetBundle.LoadAsset()来重新加载M,则Unity不会将M的旧副本解释为AB中数据的⼀个实例。 因此,Unity将加载M的新副本,并且在场景中将有两个相同的M副本。
对于⼤多数项⽬来说,这种⾏为是不可取的。⼤多数项⽬应该使⽤AssetBundle.Unload(true)并采⽤⼀种⽅法来确保对象不重复。两种常⽤⽅法是:
在应⽤程序的整个⽣命周期中特定的时间点(例如在关卡之间或在转场读条期间)卸载临时的AssetBundles。这是更简单和最常见的选择。
为每个实例维护⼀个引⽤计数,这样就可以仅在所有实例对象都不使⽤的时候,卸载AssetBundle。这允许应⽤程序卸载并重新加载单个对象⽽不重复消耗内存。
如果应⽤程序必须使⽤AssetBundle.Unload(false),那么单个对象只能通过两种⽅式卸载:
在场景和代码中消除对不需要的对象的所有引⽤。然后,调⽤Resources.UnloadUnusedAssets。
⾮叠加地加载场景。这将销毁当前场景中的所有对象并⾃动调⽤Resources.UnloadUnusedAssets。
如果⼀个项⽬有明确定义的卸载时间点,可以使⽤户等待对象加载和卸载(例如在游戏转场之间),那么应使⽤这些点来卸载尽可能多的对象并加载新的对象。
最简单的⽅法是将项⽬的离散块打包到场景中,然后将这些场景及其所有依赖项构建到AssetBundles中。然后,应⽤程序可以进⼊“加载”场景,完全卸载包含旧场景的AssetBundle,然后加载包含新场景的AssetBundle。
虽然这是最简单的流程,但有些项⽬需要更复杂的AssetBundle管理。由于每个项⽬都不同,因此没有通⽤的AssetBundle设计模式。
在决定如何将对象分组为AssetBundles时,如果必须同时加载或更新对象,通常最好先将对象捆绑到AssetBundles中。例如,考虑⾓⾊扮演游戏。个别地图和过场动画可按场景分组为AssetBundles,但在⼤多数场景中都需要⼀些对象。可以构建AssetBundles来提供肖像,游戏中的⽤户界⾯以及不同的⾓⾊模型和纹理。后⾯的对象和资产可以被分组到第⼆组资产包中,这些资产包在启动时加载并在应
⽤的整个⽣命周期内保持加载状态。
如果Unity必须在AssetBundle卸载后从它的AssetBundle重新加载对象,则可能会出现另⼀个问题。在这种情况下,重新加载将失败,对象将作为(缺失)对象出现在Unity编辑器的层次结构中。
这主要发⽣在Unity丢失并恢复对其图形上下⽂的控制时,例如,当移动应⽤程序被暂停或⽤户锁定其PC时。在这种情况下,Unity必须将纹理和着⾊器重新上传到GPU。如果这些资产的源AssetBundle不可⽤,则应⽤程序将以洋红⾊呈现场景中的对象。
1.2 分发
将项⽬的AssetBundles分发给客户端有两种基本⽅式:与项⽬同时安装或在安装后下载。
移动项⽬通常选择后⼀种。 主机和PC项⽬通常选择前⼀种。
正确的程序框架允许在安装后将新内容或修补的内容作为补丁更新,⽽不⽤关⼼初始的AssetBundles。 有关这⽅⾯的更多信息,请参阅Unity⼿册的“”部分。
1.2.1 跟随项⽬发布
将AssetBundles与项⽬⼀起发布是最简单的发布⽅式,因为它不需要额外的下载管理代码。 为什么⼀个项⽬需要在安装时包含AssetBundles有两个主要原因:
减少项⽬构建时间并允许更简单的迭代开发。 如果这些AssetBundles不需要与应⽤程序本⾝分开更新,那么AssetBundles可以通过将资产包存储在StreamingAsset中从⽽包含在应⽤程序中。 请参阅下⾯的StreamingAsset部分。
发布可更新内容的初始版本。 通常这样做是为了节省最终⽤户在初次安装后的时间,或者作为以后打补丁的基础。 SteamingAsset对于这种情况并不是最理想的⽅案。 但是,如果不愿意编写⼀个⾃定义下载和缓存的系统,那么可以使⽤这种⽅法,从
StreamingAssets将可更新内容的初始修订加载到Unity缓存中。
1.2.1.1 StreamingAsset
想在安装时,让Unity应⽤程序包含任何类型的内容(包括AssetBundles)的最简单⽅法是在构建项⽬之前将内容构建
到/Assets/StreamingAssets ⽂件夹中。构建时包含在StreamingAssets⽂件夹中的任何内容都将被复制到最终的应⽤程序中。
本地存储的StreamingAssets⽂件夹的完整路径可在运⾏时通过属性Application.streamingAssetsPath访问。然后可以在⼤多数平台上通过AssetBundle.LoadFromFile加载AssetBundles。
Android开发⼈员:在Android上,StreamingAssets⽂件夹中的资源会存储到APK中,并且可能需要更多时间才能加载(因为存储在APK中的⽂件可能使⽤不同的存储算法)。使⽤的算法可能会因Unity版本⽽异。您可以使⽤7-zip等解压⼯具打开APK以确定⽂件是否被压缩。如果被压缩,AssetBundle.LoadFromFile()执⾏得更慢。在这种情况下,您可以使⽤UnityWebRequest.GetAssetBundle作为⼀个优化⽅法来检索是否有已经缓存的版本。通过使⽤UnityWebRequest.AssetBundle将在第⼀次运⾏期间解压缩并缓存,从⽽使后续执⾏速度更快。请注意,这将需要更多的存储空间,因为AssetBundle将被复制到缓存中。或者,您可以导出您的Gradle项⽬,并在构建时向您的AssetBundles添加扩展。然后,您可以编辑adle⽂件并将该扩展名添加到noCompress部分。完成后,您应该可以使⽤AssetBundle.LoadFromFile()⽽⽆需⽀付解压缩性能成本。
怎样制作拉面注意:StreamingAsset在某些平台上不是可写⽂件。如果安装后需要更新项⽬的AssetBundles,则可以使⽤
WWW.LoadFromCacheOrDownload或编写⾃定义下载程序。
1.2.2 下载后安装
将AssetBundles部署到移动设备的最佳⽅法是在安装应⽤程序后下载它们。这也允许在安装后更新内容⽽不强制⽤户重新下载整个应⽤程序。在许多平台上,应⽤程序⼆进制⽂件必须经过昂贵且冗长的重新认证过程。因此,开发⼀个良好的安装后下载系统⾄关重要。
交付AssetBundles的最简单⽅法是将它们放置在Web服务器上并通过UnityWebRequest部署。 Unity会⾃动将下载的AssetBundles缓存在本地存储上。如果下载的AssetBundle是LZMA压缩的,则AssetBundle将以未压缩或重新压缩为LZ4(取决于
CachingpressionEnabled设置)的形式存储在缓存中,以便将来加载更快。如果下载的捆绑包压缩了LZ4,则AssetBundle将被压缩存储。如果缓存填满,Unity将从缓存中删除最近最少使⽤的AssetBundle。有关更多详细信息,请参阅下⾯的内置缓存部分。
通常建议尽可能使⽤UnityWebRequest,或者仅在使⽤Unity 5.2或更早版本时使⽤WWW.LoadFromCacheOrDownload。如果内置API的内存消耗,缓存⾏为或性能对于特定项⽬影响很⼤,或者项⽬必须运⾏特定于平台的代码以实现其要求,那么只能定制下载系统了。
使⽤UnityWebRequest或WWW.LoadFromCacheOrDownload可能不理想的情况⽰例:
当需要对AssetBundle缓存进⾏细粒度控制时
当项⽬需要实施⾃定义压缩策略时
当项⽬希望使⽤平台特定的API来满⾜某些要求时,例如需要在后台传输数据。
- ⽰例:使⽤iOS的后台任务API在后台下载数据。
如果AssetBundles必须通过SSL下载,但是Unity没有正确的SSL⽀持(如PC)。
1.2.3 内置缓存
Unity有⼀个内置的AssetBundle缓存系统,可⽤于缓存通过UnityWebRequest API下载的AssetBundles,该API包含⼀个接受AssetBundle版本号作为参数的重载。此版本号不存储在AssetBundle内部,并且不由AssetBundle系统⽣成。
缓存系统跟踪传递给UnityWebRequest的最新版本号。当调⽤此API时传⼊⼀个版本号,⾼速缓存系统通过⽐较版本号来检查是否存在缓存的AssetBundle。如果这些数字匹配,系统将加载缓存的AssetBundle。如果版本号不匹配,或没有缓存的AssetBundle,Unity将下载⼀个新副本。这个新副本将与新版本号相关联。
缓存系统中的AssetBundles仅由其⽂件名来标识,⽽不是由其下载的完整URL标识。这意味着具有相同⽂件名的AssetBundle可以存储在多个不同的位置,例如CDN(内容分发⽹络,居然还可以有这种思路)。只要⽂件名称相同,缓存系统就会将它们识别为相同的AssetBundle。
每个应⽤程序都要决定将版本号分配给AssetBundles的适当策略,并将这些版本号传递给UnityWebRequest。这些数字可能来⾃各种唯⼀标识符,例如CRC值。请注意,虽然AssetBundleManifest.GetAssetBundleHash()也可⽤于此⽬的,但我们不建议使⽤此功能进⾏版本控制,因为它仅提供估算值,⽽不是真正的Hash值计算)。
有关更多详细信息,请参阅Unity⼿册的“”部分。
在Unity 2017.1以后,缓存API已经扩展到提供更精细的控制,允许开发⼈员从多个缓存中选择⼀个活动缓存。以前的Unity版本只能修改pirationDelay和Caching.maximumAvailableDiskSpace来删除缓存的资源(Unity 2017.1中这些属性保留在Cache类中)。
我很好的英文expirationDelay是⾃动删除AssetBundle之前必须经过的最⼩秒数。如果在此期间没有访问AssetBundle,它将被⾃动删除。
maximumAvailableDiskSpace指定本地存储空间量(以字节为单位),这个量是指缓存在删除那些已q币可以赠送吗
经超过expirationDelay时间,没有使⽤的AssetBundle之前,可以使⽤的空间量。达到限制时,Unity将删除最近最少打开的缓存中的AssetBundle(或通过
Caching.MarkAsUsed标记为已使⽤)。 Unity会删除缓存的AssetBundles,直到有⾜够的空间完成新的下载为⽌。
1.2.3.1 缓存填充
由于AssetBundles使⽤⽂件名作为标识,所以可以使⽤应⽤程序附带的AssetBundles“填充”缓存。 为此,请将每个AssetBundle的初始版本或基本版本存储在/Assets/StreamingAssets /中。 该过程与“跟随项⽬发布”部分中详细介绍的过程相同。
第⼀次运⾏应⽤程序时,可以通过从Application.streamingAssetsPath加载AssetBundles来填充缓存。 此后,应⽤程序可以正常调⽤UnityWebRequest(UnityWebRequest也可⽤于最初从StreamingAssets路径加载AssetBundles)。
1.2.4 ⾃定义下载器
编写⾃定义下载程序可以让应⽤程序完全控制AssetBundles的下载,解压缩和存储⽅式。 由于所涉及的⼯程⼯作不是必要的,所以我们只为⼤型团队推荐此⽅法。 编写⾃定义下载器时有四个主要考虑事
项:
下载机制
存储位置
压缩类型
修补
有关修补AssetBundles的信息,请参阅使⽤AssetBundles修补部分。
1.2.4.1 下载
对于⼤多数应⽤程序,HTTP是下载AssetBundles最简单的⽅法。 但是,实现基于HTTP的下载程序并不简单务。 ⾃定义下载程序必须避免过多的内存分配,过多的线程使⽤和过多的线程唤醒。 Unity的WWW类不适合的原因在这⾥就不详细描述了。
在编写⾃定义下载器时,有三个选项:
C#的HttpWebRequest和WebClient类
⾃定义原⽣插件
Asset Store包
1.2.4.1.1 c#类
如果应⽤程序不需要使⽤HTTPS/SSL,那么C#的WebClient类提供了下载AssetBundles最简单的机制。 它能够将任何⽂件直接异步下载到本地存储,⽽⽆需过多管理内存分配。
要使⽤WebClient下载AssetBundle,请分配该类的⼀个实例,并将其传递给AssetBundle的URL以下载和⽬标路径。 如果需要对请求参数进⾏更多控制,可以使⽤C#的HttpWebRequest类编写下载程序:
从HttpWebResponse.GetResponseStream获取⼀个字节流。
在堆栈上分配⼀个固定⼤⼩的字节缓冲区。
从返回流(reponse)中读⼊缓冲区。
使⽤C#的File.IO API或任何其他流式IO系统将缓冲区写⼊磁盘。
1.2.4.1.2 Asset Store包
很多插件包提供了本地代码的实现,以通过HTTP,HTTPS和其他协议下载⽂件。 在为Unity编写⾃定义本机代码插件之前,建议您先评估可⽤的Asset Store包。
1.2.4.1.3 ⾃定义原⽣插件
编写⾃定义原⽣插件是在Unity中下载数据最耗时,但最灵活的⽅法。 由于编程时间花费⾼且技术风险⾼,只有在没有其他⽅法能够满⾜应⽤程序的要求时才推荐此⽅法。 例如,如果应⽤程序必须在Unity中没有C#SSL⽀持的平台上使⽤SSL通信,则可能需要定制本机插件。
⾃定义本机插件通常会包装⽬标平台的原⽣下载API。 ⽰例包括iOS上的NSURLConnection和Android上的
java.HttpURLConnection。 请查阅每个平台的本地⽂档以获取有关使⽤这些API的更多详细信息。
1.2.4.2 存储
在所有平台上,Application.persistentDataPath指向⼀个可写的位置,适合⽤于存储在应⽤程序运⾏
时保持的数据(不要误解为不运⾏就会删除数据)。 在编写⾃定义下载器时,强烈建议使⽤Application.persistentDataPath的⼦⽬录来存储下载的数据。
Application.streamingAssetPath不可写,对于AssetBundle缓存来说是⼀个糟糕的选择。 streamingAssetsPath的⽰例位置包括:OSX:在.app包内; 不可写。
Windows:在安装⽬录中(例如Program Files); 通常不可写
iOS:在.ipa包内; 不可写
Android:在.apk⽂件中; 不可写
1.3 资产分配策略
如何将项⽬资产划分为AssetBundles并不简单。 经常采⽤简单的策略,⽐如将所有对象都单独⽣成⼀个AssetBundle或仅使⽤⼀个AssetBundle,但这些解决⽅案具有明显的缺点:
拥有太少的AssetBundles …
增加运⾏时内存使⽤量
增加加载时间
需要更⼤的下载量
拥有太多的AssetBundles …
增加构建时间
可能会使开发复杂化
增加总下载时间
关键的决定是如何将对象分组为AssetBundles。 主要战略是:
逻辑实体
对象类型事业编制考试
合并内容
有关这些分组策略的更多信息可以在中到。
1.4 常见的坑
本节介绍使⽤AssetBundles项⽬中常见的⼏个问题。
发布评论