Unity资源解决方案之AssetBundle
前阵子研究了一下Unity的AssetBundle,现将相关知识点整理分享如下。
1、什么是AssetBundle
AssetBundle是Unity Pro提供的一种用于存储资源的文件格式,它能够存储任意一种Unity引擎可识别的资源,例如Scene(场景)、Mesh(网格)、Material(材质)、Texture(纹理)、Audio(音频)等。同时,AssetBundle也可以包含开发者自定义的二进制文件,只需将自定义文件的扩展名改为 .bytes,Unity就会将其识别为TextAsset,进而可被打包到AssetBundle中。我们将Unity引擎所能识别的资源称为Asset,而AssetBundle就是Asset的集合。
AssetBundle的特点
- 压缩(缺省):默认情况下会对资源进行压缩,以减少文件大小。
- 动态载入:可以在游戏运行时根据需要动态加载资源。
- 本地缓存:支持将加载的资源缓存到本地,避免重复下载。
2、AssetBundle VS Resource
AssetBundle作为Unity官方推荐的资源更新方案,与传统的Resource存在以下差异:
- Resource:存放在Resources目录下的
resources.assets文件中,单个文件有2GB的大小限制,并且首次使用时必须全部下载。 - AssetBundle:需要通过Editor脚本创建,支持动态下载,是Unity Web Caching License唯一可以缓存的内容。
3、AssetBundle的适用平台与跨平台性
AssetBundle适用于多种平台,包括网页应用、移动应用、桌面应用等,并且支持动态更新。不过,不同平台所使用的AssetBundle并不相同。在创建离线AssetBundle时,需要通过参数指定目标平台,其相容关系如下表所示(原文未给出表格内容)。
4、AssetBundle的工作流程(与flash加载swf类似)
- 创建AssetBundle:使用Unity提供的API将资源打包成AssetBundle文件。
- 上传到Server:将创建好的AssetBundle文件上传到服务器。
- 游戏运行时根据需要下载(或者从本地cache中加载)AssetBundle文件:游戏运行过程中,根据需求从服务器下载或从本地缓存中加载AssetBundle文件。
- 解析加载Assets:对加载的AssetBundle文件进行解析,从中加载所需的资源。
- 使用完毕后释放:当资源使用完毕后,释放相关的内存资源。
5、创建AssetBundle
5.1、如何创建AssetBundle
Unity引擎提供了通过编译管线 BuildPipeline 来创建AssetBundle文件的API,共有三种方法:
- BuildPipeline.BuildAssetBundle(mainAsset : Object, assets : Object[], pathName : string, options : BuildAssetBundleOptions = BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, targetPlatform : BuildTarget = BuildTarget.WebPlayer) : bool
- 该API可将编辑器中的任意类型的Assets打包成一个AssetBundle,适用于对单个大规模场景进行细分。
- BuildPipeline.BuildStreamedSceneAssetBundle(level : string[], locationPath : string, target : BuildTarget) : String
- 此API会将一个或多个场景中的资源及其所有依赖以流加载的方式打包成AssetBundle,一般用于对单个或多个场景进行集中打包。
- BuildPipeline.BuildAssetBundleExplicitAssetNames(assets : Object[], assetNames : string[], pathName : string, options : BuildAssetBundleOptions = BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, targetPlatform : BuildTarget = BuildTarget.WebPlayer) : bool
- 该API功能与第一种方法相同,但在创建时可以为每个Object指定一个自定义的名字,不过一般不太常用。
5.2、关于BuildAssetBundleOptions
- CompleteAssets:使每个Asset自身完备,包含所有的Components。
- CollectDependencies:包含每个Asset依赖的所有其他Asset。
- DisableWriteTypeTree:在AssetBundle中不包含类型信息。需要注意的是,如果将AssetBundle发布到web平台上,则不能使用这个选项。
- DeterministricAssetBundle:使每个Object具有唯一的、不变的HashID,便于后续查找,可用于增量发布AssetBundle。
- UncompressedAssetBundle:不进行数据压缩。使用该选项时,由于没有压缩/解压过程,AssetBundle的发布和加载会更快,但文件会更大,导致下载变慢。
5.3、AssetBundle之间的依赖
如果游戏中的某个资源被多个资源引用(例如游戏中的Material),单独创建AssetBundle会使多个AssetBundle都包含被引用的资源,从而导致资源文件变大。此时,可以通过指定AssetBundle之间的依赖关系来减少最终AssetBundle文件的大小(即把AssetBundle解耦)。
具体做法是在创建AssetBundle之前,调用 BuildPipeline.PushAssetDependencies 和 BuildPipeline.PopAssetDependencies 来创建AssetBundle之间的依赖关系,其用法类似于栈,后压入栈中的元素依赖栈内的元素,并且记得要调用 Pop 操作。
例如,游戏内有 mat1 和 mat2 两个Material,它们使用了相同的Texture zhuanqiang。不使用依赖时,多个AssetBundle会重复包含该Texture;使用依赖后,可避免这种情况。
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:主资源在构建资源bundle时指定(只读),该功能可方便地找到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.assetBundle;
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的卸载
AssetBundle.Unload(true):该接口会强制卸载掉所有AssetBundle中加载的Asset,包括AssetBundle的映射结构、自身对Web Stream的引用以及从AssetBundle创建出来的所有资源,不建议使用该接口。AssetBundle.Unload(false):该接口会释放AssetBundle内的序列化数据,但任何从这个AssetBundle中实例化的物体都将保持完好。不过,不能再从这个AssetBundle中加载更多物体。Resources.UnloadUnusedAssets:该接口会卸载掉没有使用的Assets,作用范围是整个系统。- 对于实例化出来的GameObject,可以调用
GameObject.Destory()接口来卸载,该接口会延后到一个合理的时机进行处理。
需要注意的是,在 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 = WWW.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之间共享资源
- 使用
ScriptableObject:Unity提供了一种可公用的类ScriptableObject,适用于描述动态划分场景。具体做法是通过代码划分场景,打包多个AssetBundle,将划分信息记录在ScriptableObject中,并保存至Asset,载入时先载入划分信息,再根据该信息载入AssetBundle。 - 使用XML文件:也可以使用XML文件来实现Editor和Runtime之间的资源共享。
10.4、关于编辑器扩展
Unity3D可以通过事件触发来执行编辑器代码,但需要一些编译器参数来告知编译器何时触发该段代码。
[MenuItem(XXX)]:声明在一个函数上方,告知编译器给Unity3D编辑器添加一个菜单项,点击该菜单项时调用该函数。触发函数中可以编写任何合法的代码,例如资源批处理程序或弹出编辑器窗口。代码中可以通过Selection类访问当前选中的内容,并据此确定显示视图。[ContextMenu("XXX")]:可以向上下文菜单中添加一个菜单项。[ExecuteInEditMode]:写在类上方,通知编译器该类的OnGUI和Update等函数在编辑模式下也会被调用。当编写了一些Component脚本,将其附属到某个GameObject时,若想在编辑视图(如Scene视图)观察到效果,可使用该属性。[AddComponentMenu("XXX/XXX")]:可将该脚本关联到Component菜单中,点击相应菜单项即可为GameObject添加该Component脚本。
为了避免不必要的包含,Unity3D的运行时和编辑器类分别存储在不同的Assemblies里(UnityEngine 和 UnityEditor)。Editor 目录下的脚本会在其它脚本之后进行编译,这便于使用运行时的内容,而其他目录下的脚本无法访问 Editor 目录下的内容。因此,建议将编辑器脚本写在 Editor 目录下。
10.5、关于差量发布
在创建AssetBundle的参数中,DeterministricAssetBundle 选项被选中时,相同的内容两次发布出来的文件会完全一样。创建AssetBundle时选择该参数,就可以进行差量发布。
10.6、关于项目中应用
在项目中使用AssetBundle进行开发时,可以使用宏进行隔离,接口封装尽量采用异步接口,并通过引用计数cache机制确定AssetBundle的释放时机。大体流程如下:
- 确定加载ab次数(资源数):明确需要加载的AssetBundle数量。
- 加载ab:根据需求加载AssetBundle。
- 成功后根据资源url引用计数减去对应资源数:加载成功后,更新引用计数。
- 引用计数为0时调用AssetBundle的Unload(false):当引用计数为0时,释放AssetBundle的序列化数据。
代码中使用的地方可以通过封装的 GetObject 获取已经加载的对象,使用完成后可以调用 Resources.UnloadUnUsedAssets 释放资源。
10.7、关于AssetBundle的粒度控制
AssetBundle的粒度越小,差量更新的冗余就越小;粒度越大,差量更新的冗余就越大。但并非粒度越小越好,因为粒度小了,运行时加载时会增加IO次数、解压次数(AB一般选择压缩格式)和申请内存的次数,导致加载时长变长。因此,粒度的控制是一个时间与空间平衡的选择过程。经过实验,1M左右的AssetBundle包加载性能较好,冗余也可以接受。
10.8、关于AssetBundle的压缩选择
AssetBundle压缩与不压缩的差异主要体现在以下两方面:
- 外存:影响安装包的大小或者安装后占用磁盘空间的大小。
- 加载方式的选择:决定能否使用同步方法。
对于一些对性能要求特别高、资源又不大的AssetBundle,可以采用非压缩方式,通过 CreateFromFile 加载AB包,这样性能最高且不会产生大量内存占用。对于其他资源文件,建议进行压缩处理,因为压缩与非压缩在磁盘占用上会有4倍左右的大小差别,如果都采用非压缩格式,可能会导致磁盘占用达到一个非常大的量级。
10.9、AssetBundle在外存优化中的应用
安装后的磁盘构成主要是资源的内存值(即Resources目录下的资源)。例如,一张真彩色的1024 1024的图片放到Resources目录下,安装后占用的内存为4M(4 1024 * 1024)。如果游戏是2D的,且包含很多图片资源,会使安装包在用户机器上占用大量磁盘空间。此时,可以将这些资源打成AB包放到用户的手机上,从而减小磁盘占用。