转载请标明出处:http://www.cnblogs.com/zblade/
本文参照unity官网上对于assetBundle的一系列讲解,主要针对assetbundle的知识点做一个梳理笔记,也为后续的资源打包设计做一个基础学习,本文的代码和图片均来自unity官网,详情可以查看Unity的DOCUMENTATION。
一、什么是AssetBundle
AssetBundle就像一个ZIP压缩文件,里面存储着不同平台的特殊资源(models/texture/prefabs/materials/audio clip/scenes...), 这些资源都可以在运行时进行加载。具体的assetBundle中主要包含什么?主要包含两种互相关联的东西:
1. 磁盘上的文件:也就是assetbundle文档,可以将其视为一个容器或者文件夹,其中包含两类文件:序列化文件和资源文件,序列化文件就是资源在打包后对应的各个平台的序列化操作后的文件,资源文件主要是针对textures/audio等较大的文件打包的二进制文件,这类文件在加载的时候是在其他线程执行的(效率更高)。
2. 就是实际的assetbundle对象了,可以通过代码来进行资源加载,其中主体是各个资源在进行加载的时候的存储路径图。用图表示:
二、如何对资源进行分组便于打包AssetBundle
虽然可以对AssetBundle自由的进行规划,但是在进行项目的资源管理的时候,还是有一些规划建议可以采用:
1.依据逻辑实体进行分组
这种资源分类方式是依据资源的功能进行分类,例如 UI/角色/场景/code等具体各自功能规划的部分来进行资源分组,可以将所有的textures/layout data都打入UI相关分类中,可以将所有的模型和动画都打入角色相关的资源中,将场景相关的贴图和模型都打入场景资源中。
采用逻辑实体分组,对于资源的下载更新更为有利,由于资源的分类,可以在进行资源更新的时候,只更新对应的资源,而无需更新冗余的其他资源。
使用这种分类方式最合适的策略,就是将资源进行详细的分类(when and where will be used)
2.类型分组
主要依据资源的类型来进行分组,这样对于不同的应用平台都具有一定的适用性。比如对于audio文件的压缩设置,在mac和windows上都是一致的,那么可以将audio文件都归类为一类文件,实现文件资源的复用(不同平台的打包设置),对于shaders而言,对于不同平台需要不同的编译设置,那么就需要分类处理。这类分类方法,对于在不同的版本中变动频率较低的代码文件和prefabs显得更有优势。
3.相互关联的内容分组
这种策略的,就是将需要同时进行加载的资源都归类为一个分组,例如将不同场景中的角色都依据场景来进行分组,这就要求单独一个场景中的资源只能用于该场景,各个分组之间没有互相关联的关系。这种分类方式,对于资源的加载时间有较大的缩减,这种分类方式的使用场合主要在场景资源中,在不同的场景资源中,其包含的资源各自互相不关联。
在一个项目中,可以将上述的几种策略都交互使用,对应具体的应用需求来灵活的采用分组策略,当然unity也提供了一些资源分组的tips:
- 分离高频和低频更新的资源;
- 将需要同时下载的资源合并进一个组,例如Model以及其关联的animations;
- 如果出现多个bundle中的多个object都依赖于另一个完全不同的bundle,那么将这些依赖关系都移动到一个单独的bundle,这样可以降低依赖关系的复杂度;多个bundle均依赖于另一个bundle中的资源,那么将这些bundle以及其依赖的资源归类到一个资源,这样可以降低资源的重复率(避免一份资源被拷贝到多个不同的bundle中);
- 不可能同时加载的资源,需要归类的各自的assetbundle中,例如标准和高配的资源;
- 如果一个assetbundle中资源在加载的时候低于50%需要被加载,那么可以考虑将这些需要被加载的资源单独分类为一个资源(避免冗余的加载);
- 如果一组Objects对应的是一个资源的不同版本,那么可以考虑assetbundle variants
三、如何打包AssetBundle
unity资源打包的接口,就是BuildPipeline.BuildAssetBundles函数,其对应具体的三个参数,第一个是bundle的输出路径,下面来详细分析一下剩下的2个参数:
1、BuildAssetBundleOptions
具体的参数设置,可以参看API当中的详细信息,下面主要集中说三个参数,分别对应三种压缩格式的选择。
1)BuildAssetBundleOptions.None :
采用LZMA的压缩格式, 这种压缩格式要求资源在使用之前需要全部被解压,这就会带来在使用一个极小的文件的时候会额外带来较长的解压时间消耗。比较蛋疼的是,一旦这个bundle被解压之后,在磁盘上又会以LZ4的格式重新压缩,LZ4的压缩格式,在使用资源的时候不需要全部解压。
这种压缩格式主要用于一个bundle中资源都需要被加载的时候,例如打包角色或者场景资源的时候,这种压缩格式在初始化下载的时候被推荐(更小的包体),这些资源在被解压后,又会以LZ4的格式被缓存。
2) BuildAssetBundleOptions.UncompressedAssetBundle:
无压缩的打包,加载的文件更大,但是时间更快(省去解压的时间)
3)BuildAssetBundleOptions.ChunkBasedCompression:
采用LZ4的压缩格式,相比于LZMA而言文件体积更大,但是不要求在使用之前整个bundle都被解压。LZ4使用chunk based 算法,这就运行文件以chunk或者piece的方式加载,只解压一个chunk文件,而无需解压bundle中其余不相关的chunk。
2、BuildTarget
也就是当前资源需要被使用的平台的分类,在打完资源后,会发现文件夹中有更多的文件,一般是2*(n+1), 对于各个不同的资源,都会有一个manifest文件,一般是 bundlename+".manifest",除此之外,还会有一个额外的manifest文件,对应不同的平台会有不同的额外的manifest(这是一个总体的manifest文件)。
对于manifest中内容,可以在文本文件中打开,一般举例:
ManifestFileVersion: 0
CRC: 2422268106
Hashes:
AssetFileHash:
serializedVersion: 2
Hash: 8b6db55a2344f068cf8a9be0a662ba15
TypeTreeHash:
serializedVersion: 2
Hash: 37ad974993dbaa77485dd2a0c38f347a
HashAppended: 0
ClassTypes:
- Class: 91
Script: {instanceID: 0}
Assets:
Asset_0: Assets/Mecanim/StateMachine.controller
Dependencies: {}
首先是版本,然后是CRC校验码,hash码,classtypes,包含的资源及其路径,依赖关系(dependencies)。
PS:
对于依赖关系就多详细讲解一下,所谓的依赖关系,指的是一个bundle中的一个或者多个UnityEngine.Objects包含对另一个bundle中的一个UnityEngine.Object的引用,如果一个bundle中的object对包含对其他非该bundle的object的引用,但是该object不属于任何一个bundle,那么这种依赖关系就不存在(这儿会带来一个问题,如果多个bundle均引用一个不属于bundle的object,那么在加载的时候,各个bundle均会将该object进行copy到其内存中,这就带来内存的额外冗余占用),所以依赖关系就是两个或者多个bundle之间的object的引用关系。
对于依赖资源的加载,需要在该资源被加载前加载,unity不会自动的加载依赖资源,这需要在代码中实现依赖资源的加载。
四、如何使用AssetBundle
在说完bundle的分类,打包后,接下来就是如何在实际的游戏中加载使用这些bundle。在unity5以后,提供了4种不同类型的加载接口,下面逐一分析一下这四种不同接口的使用:
1、AssetBundle.LoadFromMemoryAsync(byte[] binary, unit crc = 0)
这个方法用来加载ab数据的bytes数组,如果数据是使用LZMA的压缩格式,那么在加载的时候会进行解压的操作,LZ4格式的数据则会保持其压缩的状态,使用示例:
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);
}
}
当然,对于bytes数组,也可以使用File.ReadAllBytes(path)的方式来加载数组。
2、 AssetBundle.LoadFromFile
在加载非压缩文件或者LZ4压缩类型文件的时候,该接口效率极高,对于LZMA压缩格式的文件,也会在加载的时候执行解压的操作,使用示例:
public class LoadFromFileExample extends 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);
}
}
ps: 在unity5.3及更早的版本中,在安卓平台上如果从streaming assets路径中加载文件会失败(路径文件夹中会额外包含.jar文件)。
3、WWW.LoadFromCacheOrDownload
这个接口会被淘汰(被UnityWebRequest替换),那么就不过多的讲解这个接口(注意这个接口会进行存储分配的操作以容纳资源,如果分配不足以存储会使得加载失败)。
4、UnityWebRequest
这个接口,会有两步操作,首先是创建一个web request(调用UnityWebRequest.GetAssetBundle), 然后进行资源的获取(调用DownloadHandlerAssetBundle.GetContent),unity提供的使用示例为:
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);
}
使用这种方式,可以使得开发者更为灵活的操作下载数据,同时进行内存使用分配,会逐渐的被用来WWW接口。
在加载完assetBundle后,接下来,就是如何从bundle中获取资源(asset),其基本的接口模板为:
T objectFromBundle = bundleObject.LoadAsset<T>(assetName);
如果想获取所有的assets则可以使用接口:
Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets();
一旦获取到asset,那么就可以在游戏中使用这些资源了(一般是实例化创建操作)。
5、加载AssetBundle Manifest
除了加载assetbundle,一般还会加载其对应的manifest(与其存储在同一个文件夹下的相同名字的manifest),一般加载manifest的操作示例:
AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
在前文也提及到,如果一个assetbundle依赖于另一个assetbundle,那么需要提前加载依赖相关的bundle,那么依据manifest,可以加载其依赖的assetbundle:
AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] dependencies = manifest.GetAllDependencies("assetBundle"); //Pass the name of the bundle you want the dependencies for.
foreach(string dependency in dependencies)
{
AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, dependency));
}
现在,已经加载了assetbundle, 也获取了assetbundle的dependencies,以及其中assets,这样就可以管理这些assetbundle了。
五、管理AssetBundle
unity在场景中的Object被移除的时候不自动释放objects,资源的清理需要再特定的时间触发(场景切换)或者手动的管理。所以怎么加载和卸载资源显得尤为重要,不合适的加载可能会导致资源的重复加载,不合适的卸载可能会带来资源的缺失(比如丢失贴图)。
对于assetbundle的资源管理,最重要的是掌握什么时候调用AssetBundle.Unload(bool)这个函数,传入true/false会有不同的卸载策略。这个API会卸载对应的assetbundle的头部信息,参数对应着是否同时卸载从该assetbundle中实例化的所有Objects。
AssetBundle.Unload(true)会卸载assetbundle中的所有gameobjects以及其依赖关系,但是并不包括基于其Objects实例化(复制)的Object(因为这些object不属于该assetbundle,只是引用),所以当卸载贴图相关的assetbundle的时候,场景中对其引用的实例化物体上会出现贴图丢失,也就是场景中会出现红色的区域,unity都会将其处理成贴图丢失。
举例说明,假设材质M来自于assetbundle AB, 如果 AB.Unload(true), 那么场景中任何M的实例都会被卸载和销毁,如果AB.Unload(false), 那么就会切断材质M实例与AB之间的关系:
那么如果该assetbundle AB在后面再次被加载,unity不会重新关联其关系,这样在后续的使用中,就会出现一份材质的多个实例:
所以通常情况下,AssetBundle.Unload(false) 并不能带来较为合理的释放结果,AssetBundle.Unload(true)通常用来确保不会在内存中多次拷贝同一个资源,所以其更多的被项目所采纳,此外还有两个常用的方法用来确保其使用:
1)在游戏中,对于场景的卸载有明确的规划,比如在场景切换中或者场景加载中;
2)管理好对每个单独的object的计数,只有在没有引用的时候才卸载该assetbundle,这样可以规避加载和卸载过程中的多份内存拷贝问题。
如果要使用AssetBundle.Unload(false), 那么这些实例化的对象可以通过2中途径卸载:
1)清除对不需要物体的所有引用,场景和代码中都需要清楚,然后调用Resources.UnloadUnusedAssets;
2) 在场景加载的时候采用非增量的方式加载,这会清楚当前场景中的所有Objects,然后反射自动调用Resources.UnloadUnusedAssets
如果你不想管理这些assetbundle,unity推出了AssetBundle Manager,可以学习了解一下,此外Unity还推出了一些AssetBundle Browser Tool, 也可以学习了解一下。