Unity资源解决方案之AssetBundle
1、什么是AssetBundle
AssetBundle是Unity pro提供的一种用来存储资源的文件格式,它可以存储任意一种Unity引擎能够识别的资源,如Scene、Mesh、Material、Texture、Audio、noxss等等,同时,AssetBundle也可以包含开发者自定义的二进制文件,只需要将自定义文件的扩展名改为.bytes,Unity就可以把它识别为TextAsset,进而就可以被打包到AssetBundle中。Unity引擎所能识别的资源我们称为Asset,AssetBundle就是Asset的一个集合。
AssetBundle的特点:
压缩(缺省)、动态载入、本地缓存;
2、AssetBundle VS Resource
AssetBundle作为Unity官方推崇的资源更新方案,与传统的Resource差异如下:
a、Resource放在Resources目录下,resources.assets文件,单个文件有2GB限制,首次必须全部下载;
b、AssetBundle创建需要通过Editor脚本创建,支持动态下载,是Unity Web Caching License唯一可以缓存的内容;
3、AssetBundle的适用平台与跨平台性
AssetBundle适用于多种平台,包括网页应用、移动应用、桌面应用等,可以动态更新,但不同平台所使用的AssetBundle并不相同,在创建离线AssetBundle的时候需要通过参数来指定目标平台,相容关系如表所示
4、AssetBundle的工作流程(与flash加载swf类似)
a、创建AssetBundle;
b、上传到Server;
c、游戏运行时根据需要下载(或者从本地cache中加载)AssetBundle文件;
d、解析加载Assets;
e、使用完毕后释放;
5、创建AssetBundle
5.1、如何创建AssetBundle
Unity引擎提供了创建AssetBundle的API,通过编译管线BuildPipeline来创建AssetBundle文件,总共有三种方法:
a、BuildPipeline.BuildAssetBundle(mainAsset : Object, assets : Object[], pathName : string, options : BuildAssetBundleOptions = BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, targetPlatform : BuildTarget = BuildTarget.WebPlayer) : bool
该API将编辑器中的任意类型的Assets打包成一个AssetBundle,适用于对单个大规模场景的细分;
b、BuildPipeline.BuildStreamedSceneAssetBundle(level : string[], locationPath : string, target : BuildTarget) : String
该API将一个或多个场景中的资源及其所有依赖以流加载的方式打包成AssetBundle,一般适用于多单个或多个场景进行集中打包;
c、BuildPipeline.BuildAssetBundleExplicitAssetNames(assets : Object[], assetNames : string[],pathName : string, options : BuildAssetBundleOptions = BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, targetPlatform : BuildTarget = BuildTarget.WebPlayer) : bool
该API功能与a相同,但创建的时候可以为每个Object指定一个自定义的名字。(一般不太常用)
5.2、关于BuildAssetBundleOptions
a、CompleteAssets
使每个Asset自身完备,包含所有的Components;
b、CollectDependencies
包含每个Asset依赖的所有其他Asset;
c、DisableWriteTypeTree
在AssetBundle中不包含类型信息。需要注意的是,如果将AssetBundle发布到web平台上,则不能使用这个选项;
d、DeterministricAssetBundle
使每个Object具有唯一的、不变的HashID,便于后续查找可以用于增量发布AssetBundle;
e、UncompressedAssetBundle
不进行数据压缩。如果使用这个选项,因为没有压缩/解压的过程,AssetBundle发布和加载会更快,但是AssetBundle也会更大,导致下载变慢。
5.3、AssetBundle之间的依赖
如果游戏中的某个资源被多个资源引用(例如游戏中的Material),单独创建AssetBundle会使多个AssetBundle都包含被引用的资源(这里跟flash编译选项中的链接选项有些像),从而导致资源变大,这里可以通过指定AssetBundle之间的依赖关系来减少最终AssetBundle文件的大小(把AssetBundle解耦)。
具体方法是在创建AssetBundle之前调用BuildPipeline.PushAssetDependencies和BuildPipeline.PopAssetDependencies来创建AssetBundle之间的依赖关系,它的用法就是一个栈,后压入栈中的元素依赖栈内的元素。(记得要pop!)
举个例子:
现在游戏内有mat1和mat2两个Material,他们使用了相同的Texture zhuanqiang
不使用依赖
使用依赖
6、远端Server的AssetBundle下载
Unity引擎提供了两种方式从服务器下载AssetBundle文件,分别是缓存机制和非缓存机制。
6.1、缓存机制
通过WWW.LoadFromCacheOrDownload (url : string, version : int)接口来下载AssetBundle,下载后的AssetBundle会自动被保存到Unity引擎的缓存区内,该方法是Unity推荐的AssetBundle下载方式。下载AssetBundle的时候,该接口会先在本地缓存中查找该文件,看其之前是否被下载过,如果下载过,则直接从缓存中加载,如果没有,则从服务器尽享下载。这样做的好处是可以节省AssetBundle文件的下载时间,从而提高游戏资源的载入速度(还可以节省下载流量)。同时开启多个Coroutine进行WWW的LoadFromCacheOrDownload操作(缓存中),经测试开启的WWW现成越多,速度会越快,但是需要考虑时机的机器火平台的承载能力。如果一定要从网上下载资源的话,线程数最好设为5个(别人的经验),很多平台也有自己的限制,例如有的浏览器只能同事加载6个等等。
需要注意的是,Unity提供的默认缓存大小是根据发布平台不同而不同的(可以向Unity购买Caching license支持)。目前对于web player的网页游戏,默认缓存大小为50M;对于PC上的客户端或者IOS¥Android上的移动游戏,默认缓存大小为4GB。
代码:
WWW www = WWW.LoadFromCacheOrDownload (Url, 1);
yield return www;
AssetBundle ab = www.assetBundle;
6.2、非缓存机制
通过创建一个WWW实例来对AssetBundle文件下载,下载后的AssetBundle文件将不会进入Unity的缓存区。使用这种方法每次都会从远端服务器下载。
代码:
WWW www = new WWW(Url);
yield return www;
AssetBundle ab = www.assetBundle;
7、载入AssetBundle对象
7.1、通过WWW类方法和属性
直接通过WWW.assetBundle属性来创建AssetBundle,注意:通过WWW加载的AssetBundle在解析Asset之前一定要先调用WWW.assetBundle;
7.2、通过API动态创建
AssetBundle.CreateFromFile接口从磁盘创建一个AssetBundle文件的内存对象,该方法仅支持非压缩格式的AssetBundle。
7.3通过API动态创建
AssetBundle.CreateFromMemory接口可以从内存数据流创建一个AssetBundle内存对象。主要用于对数据的加解密上。
例如:
WWW www = new WWW(url);
yield return www;
byte[] encrypedData = www.bytes;
byte[] decryptedData = YourDecryptionMethod(encrypedData);//解密函数
AssetBundle ab = AssetBundle.CreateFromMemory(decrypedData);
8、从AssetBundle中加载Assets
8.1、Assets的加载
AssetBundle.Load (name : string) : Object 从bundle中加载名为name的对象;
AssetBundle.Load (name : string, type : Type) : Object 从bundle中加载被指定类型的名为name的对象;
AssetBundle.LoadAsync (name : string, type : Type) : AssetBundleRequest 异步地从bundle中加载被指定类型的名为name的对象(异步加载需要Unity Pro专业版);
AssetBundle.LoadAll (type : Type) : Object[] 加载所有包含在asset bundle中且继承自type的对象;
AssetBundle.LoadAll () : Object[] 加载所有包含在asset bundle中的对象;
AssetBundle.mainAsset 主资源在构建资源boundle时指定(只读),该功能可以方便的找到bundle内的主资源。例如,你也许想有一个角色的预制体并包括所有纹理、材质、网格和动画文件。但是完全操纵角色的预设体应该是你的mainAsset并且易于被访问;
例如:
//开始下载
WWW www = new WWW(url);
//等待下载完成
yield return www;
//获取指定的主资源并实例化
Instantiate(www.assetBundle.mainAsset);
8.2、AssetBundle中加载Level
Application.LoadLevel 该接口可以通过名字或者索引载入AssetBundle文件中包含的对应场景,当夹在新场景时,所有之前加载的GameObject都会被销毁;
Application.LoadLevelAsync 该接口的作用与Application.LoadLevel相同,不同的是该接口是异步加载,即加载时主线程可以继续执行;
Application.LoadLevelAdditive 该接口不同与Application.LoadLevel的是并不销毁之前加载的GameObject;
Application.LoadLevelAdditiveAsync 该接口的作用与Application.LoadLevelAdditive相同,不同的是该接口是异步加载,即加载时主线程可以继续执行;
例如:
WWW www = new WWW(url);
yield return www;
AssetBundle ab = www.assetBunlde;
Application.loadLevel("Level1");
9、AssetBundle与内存
内存一直都是开发者关注的一个重点,如果要了解清楚内存的使用情况。
9.1、加载AssetBundle对内存的影响
Unity引擎在使用WWW方法时会分配一系列的内存空间来存放WWW实例对象、WebStream数据。该数据包括原始的AssetBundle数据、解压后的AssetBundle数据以及一个用于解压的Decompression Buffer。一般情况下,Decompression Buffer会在原始的AssetBundle解压完成后自动销毁,但需要注意的是,Unity会自动保留一个Decompression Buffer,不被系统回收,这样做的好处是不用过于频繁的开辟和销毁解压Buffer,从而在一定程度上降低CPU的消耗。
当把AssetBundle解压到内存后,开发者可以使用WWW.assetBundle属性来获取AssetBundle对象,从而可以得到各种Assets,进而对这些Assets进行加载或者实例化操作。加载过程中,Unity会将AssetBundle中的数据流转变为引擎可以识别的信息类型(纹理、材质、对象等)。加载完成后,开发者可以对其进行进一步的操作,比如对象的实例化、纹理和材质的复制和替换等。
9.2、AssetBundle的卸载
1)AssetBundle.Unload(true) 该接口会强制卸载掉所有AssetBundle中加载的Asset,包括AssetBundle的映射结构、自身对Web Stream的引用以及从AssetBundle创建出来的所有资源,该接口不推荐使用。
2)AssetBundle.Unload(false) 该接口会释放AssetBundle内的序列化数据,但是任何从这个AssetBundle中实例化的物体都将完好。当然,你不能从这个AssetBundle中加载更多物体。
Resources.UnloadUnusedAssets该接口会卸载掉没有使用的Assets,作用范围是整个系统。
3)对于实例化出来的GameObject,可以调用GameObject.Destory()接口来卸载。该接口会延后到一个合理的时机进行处理。
注意:这是U3D没有处理好的一个环节。在WWW加载资源完毕后,对资源进行instantiate后,对其资源进行unload,这时问题就发生了,instantiate处理渲染需要一定的时间,虽然很短,但也是需要1,2帧的时间。此时进行unload会对资源渲染造成影响,以至于没有贴图或 者等等问题发生。解决办法:自己写个时间等待代码,等待个0.5秒到1秒之后再进行Unload。这样就不会出现instantiate渲染中就运行unload的情况了。
10、关于其他
10.1、在AssetBundle中嵌入脚本
AssetBundle中的资源上如果Attach了脚本,打包的时候该脚本是不会被打到AssetBundle中的,其实这里只是保存了一个类似于指针的关联,如果需要把脚本也动态打到AssetBundle中,还需要做一番工作。
首先,将脚本预先编译成assembly,把assembly保存成.bytes文件,这样Unity会把它识别为TextAsset,就可以将这个TextAsset打包到AssetBundle中了,载入后可以通过反射机制使用该脚本,代码如下:
AssetBundle bundle = WWWW.assetBundle;
TextAsset txt = bundle.load("MyBinaryAsText", typeof(TextAsset)) as TextAsset;
byte[] bytes = txt.bytes;
var assembly = System.Reflection.Assembly.Load(bytes);
需要注意的是,IOS平台不支持动态载入脚本。
10.2、AssetBundle的版本控制
AssetBundle使用WWW.LoadFromCacheOrDownload(string url, int version, uint crc)加载,其中的第二个参数-version可以用来做版本控制,该参数强制用户从服务器下载一个更高版本的AssetBundle。我们可以通过第三个参数crc来实现AssetBundle的内容校验,当crc不为0的时候,Unity会校验AssetBundle的CRC码,如果不等,则说明文件损坏,Unity会重新下载该文件。对于crc的获取,(老版本没有提供方法,只能通过LoadFromCacheOrDownload传一个错误的crc,从log中获取),新版本在BuildAssetBundle的时候增加了一个out类型的参数,该参数会返回正确的crc码,打包的时候可以记录下来以供后面使用。
10.3、关于Editor和Runtime之间共享资源
1)Unity提供了一种可以公用的类——noxssableObject,适用于描述动态划分场景;通过代码划分场景——>打包多个AssetBundle——>将划分信息记录在ScriptableObject中,并保存至Asset——>载入时先载入划分信息,再根据这个划分信息载入AssetBundle。
2)使用XML文件。
10.4、关于编辑器扩展
Unity3D可以通过事件触发来执行你的编辑器代码,但是我们需要一些编译器参数来告知编译器何时需要触发该段代码。 [MenuItem(XXX)]声明在一个函数上方,告知编译器给Unity3D编辑器添加一个菜单项,并且当点击该菜单项的时候调用该函数。触发函数里可以编写任何合法的代码,可以是一个资源批处理程序,也可以弹出一个编辑器窗口。代码里可以访问到当前选中的内容(通过Selection类),并据此来确定显示视图。与此类似,[ContextMenu("XXX")]可以向你的上下文菜单中添加一个菜单项。 当你编写了一些Component脚本,当它被附属到某个GameObject时,想在编辑视图即可在Scene视图观察到效果,那么你可以把[ExecuteInEditMode]写在类上方来通知编译器,该类的OnGUI和Update等函数在编辑模式也也会被调用。我们还可以使用[AddComponentMenu("XXX/XXX")]来把该脚本关联到Component菜单中,点击相应菜单项即可为GameObject添加该Component脚本。
为了避免不必要的包含,Unity3D的运行时和编辑器类分辨存储在不同的Assemblies里(UnityEngine和UnityEditor)。Editor目录下的脚本会在其它脚本之后进行编译,这方便了你去使用那些运行时的内容。而那些目录下的脚本是不能访问到Editor目录下的内容的。所以,你最好把你的编辑器脚本写在Editor目录下。
10.5、关于差量发布
在5.2中介绍了创建AssetBundle的参数,其中的d选项在选中的时候可以使相同的内容两次发布出来的文件是完全一样的,我们在创建AssetBundle的时候选择上这个参数,那么就可以做差量了。测试数据如下:
没有选择该参数:
选择了该参数后:
10.6、关于项目中应用
项目中使用AssetBundle做开发可以使用宏进行隔离,接口封装尽量采用异步接口,通过引用计数cache机制,确定ab的释放时机。大体流程如下:
a、确定加载ab次数(资源数)
b、加载ab
c、成功后根据资源url引用计数减去对应资源数
d、引用计数为0的时候调用AssetBundle的Unload(false)
代码中使用的地方可以通过封装的GetObject获取已经加载的对象,使用完成后可以调用Resources的UnloadUnUsedAssets释放资源。
10.7、关于AssetBundle的粒度控制
AB的粒度越小,差量更新的冗余就会越小,粒度越大,差量更新的冗余就会越大。但是,并不是说粒度越小就越好,粒度小了,(运行时)加载的时候会增加IO次数、解压次数(AB一般选择压缩格式)和申请内存的次数,导致加载时长变长。因此粒度的控制是一个时间与空间平衡的选择过程。经过实验,大体有一个数据可以用来参考,1M左右的AssetBundle包加载性能最好,冗余也可以接受。
10.8、关于AssetBundle的压缩选择
AssetBundle压缩与不压缩的差异主要有两方面:
a、外存(安装包的大小或者安装后占用磁盘空间的大小)
b、加载方式的选择(能否使用同步方法)
这里如果一些对性能要求特别高,资源又不大的AB可以采用非压缩方式,通过CreateFromFile加载AB包,性能最高又不会产生大量的内存。对于其他的资源文件,建议压缩处理,压缩与非压缩在磁盘占用上会有4倍左右的大小差别,如果都采用非压缩格式,有可能会导致你的磁盘占用达到一个非常恐怖的量级。
10.9、AssetBundle在外存优化中的应用
安装后的磁盘构成基本上是资源的内存值(Resources目录下资源),举个粒子,一张真彩色的1024*1024的图片放到Resources目录下安装后占用的内存为4M(4*1024*1024)。如果你的游戏是一个2D的,又包含很多的图片资源,这样会使你的安装包在用户机器上安装需要大量的磁盘空间,这里你可以把它们打成AB包放到用户的手机上,这样就磁盘占用就会小很多了。