UnityAssetBundle详解

简介

之前有段时间研究过unityAssetBundle,读了一大堆的文章和相关的案例,废话不少而且讲的也不是很详细很通俗,所以抽空结合Unity的最新官方教程(2018.7.30)给出了自己对于Assetbundle的思考和一些通俗解释,以及上手示例。别人的官方的好的解释我会原搬过来,整个思路我会给出自己的理解。
资源包就是一个为特定平台打包的在运行时可以加载的特定资源的文件系统(包括 Models, Textures, Prefabs, Audio clips, and even entire Scenes)。它的最大的特性官网也说的很清晰,就是资源之间的相互依赖关系(后面再通过例子详细解释)。同时为了网络传输的方便,自带了一些压缩的算法。
所以自然而然主要的用途就包括:下载游戏内容/减少初始安装包的大小/加载针对特定平台的资源/减少运行时的内存压力。

AB一方面指向硬盘上的额外文件(除了安装包)。主要有两类:序列化的文件和资源文件。序列化文件,就是将你的资源分成独立的对象然后被写进单个文件(这里不求深究,接着往下看),而资源文件指的是为某些资源单独存储的二进制数据块,方便你利用其他线程高效的调用。
另一方面,是通过代码去交互的AB对象,可以从特定的文件档案加载资源。这个对象加载了你需要的一些东西,是什么呢,原文真特么长,分开看,这个AB对象包含了一幅映射地图–所有你想加载到这个归档里的资源的文件路径的映射地图,一旦你需要加载该资源内的某个对象,你通知AB,AB就可以帮你调取相应的文件路径并加载相应的资源。

Unity资源

Unity资源有三种分类,Unity自动打包的资源,Resources资源,另外一个是AssetBundle:
自动打包资源
场景中用到的资源会被自动打包,只要放在Asset下任何目录就行,程序不关心它的打包和加载,这些资源都是静态加载的。
Resources资源
Assets/Resources文件夹下,该目录下的资源,无论是否使用都会被打包到游戏中,通过Resources.Load方法动态加载。常用,缺点是没法更新。
AB资源
通过编辑器脚本控制的计划性打包资源,这些AB和游戏包是分离的,恶意通过www类加载,目录比较灵活。

资源文件夹

Assets
为Unity编辑器下的资源文件夹,Unity项目编辑时的所有资源都将置入此文件夹内。在编辑器下,可以使用以下方法获得资源对象:
AssetDatabase.LoadAssetAtPath(“Assets/x.txt”);
注意:此方法只能在编辑器下使用,当项目打包后,在游戏内无法运作。参数为包含Assets内的文件全路径,并且需要文件后缀。

Resources
资源载入:
Assets下的特殊文件夹,此文件夹内的资源将会在项目打包时,全部打入包内,并能通过以下方法获得对象:Resources.Load(“fileName”);
注意:函数内的参数为相对于Resource目录下的文件路径与名称,不包含后缀。Assets目录下可以拥有任意路径及数量的Resources文件夹,在运行时,Resources下的文件路径将被合并。
例:Assets/Resources/test.txt与 Assets/TestFloder/Resources/test.png在使用Resource.Load(“test”)载入时,将被视为同一资源,只会返回第一个符合名称的对象。
如果使用Resource.Load(“test”)将返回text.txt;
如果在Resources下有相同路径及名称的资源,使用以上方法只能获得第一个符合查找条件的对象,使用以下方法能或得到所有符合条件的对象:
Object[] assets = Resources.LoadAll(“fileName”);
TextAsset[] assets = Resources.LoadAll(“fileName”);

相关机制:
在工程进行打包后,Resource文件夹中的资源将进行加密与压缩,打包后的程序内将不存在Resource文件夹,故无法通过路径访问以及更新资源。
在程序启动时会为Resource下的所有对象进行初始化,构建实例ID。随着Resource内资源的数量增加,此过程耗时的增加是非线性的。故会出现程序启动时间过长的问题,请密切留意Resource内的资源数量。

卸载资源:
所有实例化后的GameObject可以通过Destroy函数销毁。请留意Object与GameObject之间的区别与联系。
Object可以通过Resources中的相关Api进行卸载
Resources.UnloadAsset(Object);//卸载对应Object
Resources.UnloadUnusedAssets();//卸载所有没有被引用以及实例化的Object
注意以下情况:
Object obj = Resources.Load(“MyPrefab”);
GameObject instance = Instantiate(obj) as GameObjct;
……
Destroy(instance);
Resources.UnloadUnusedAssets();
此时UnloadUnusedAssets将不会生效,因为obj依然引用了MyPrefab,需要将obj = null,才可生效。

StreamingAssets
StreamingAssets必须在Assets根目录下,文件夹为流媒体文件夹,此文件夹内的资源将不会经过压缩与加密,原封不动的打包进游戏包内。在游戏安装时,StreamAssets文件件内的资源将根据平台,移动到对应的文件夹内。StreamingAssets文件夹在Android与IOS平台上为只读文件夹。
Unity基本也没有提供从该路径下直接读取资源的方法,只有www可以加载audioClip、texture和二进制文件。但Unity提供了从该目录加载AssetBundle的方法,我们一般直接在这个目录下存放AssetBundle文件。可以通过Application.streamingAssetsPath访问该路径。
你可以使用以下函数获得不同平台下的StreamingAssets文件夹路径:
Application.streamingAssetsPath
请参考以下各平台下StreamingAssets文件夹的等价路径,Application.dataPath为程序安装路径。Android平台下的路径比较特殊,请留意此路径的前缀,在一些资源读取的方法中是不必要的
Windows/MacOS:Application.dataPath+”/StreamingAssets”
IOS:Application.dataPath+”/Raw” //
Android:”jar:file://“+Application.dataPath+”!/assets/“ //jar:file:///data/app/com.myCompany.myProj-1/base.apk!/assets

文件读取:
StreamingAssets文件夹下的文件在游戏中只能通过IO Stream或者WWW的方式读取(AssetBundle除外)
IO Stream方式
using(FileStream stream =
File.Open(Application.streamingAssetsPath+”fileName”,
FileMode.Open))
{
//处理方法
}
WWW方式(注意协议与不同平台下路径的区别)
using(WWW www = new WWW(
Application.streamingAssetsPath+”fileName”))
{
yield return www;
www.text;
www.texture;
}
AssetBundle特有的同步读取方式(注意安卓平台下的路径区别)
string assetbundlePath =
‘#if UNITY_ANDROID
Application.dataPath+”!/assets”;
‘#else
Application.streamingAssetsPath;
‘#endif
AssetBundle.LoadFromFile(assetbundlePath+”/name.unity3d”);

PersistentDataPath
Application.persistentDataPath
沙盒目录,应用程序安装后才出现。Unity指定的一个可读写的外部文件夹,该路径因平台及系统配置不同而不同。可以用来保存数据及文件。该目录下的资源不会在打包时被打入包中,也不会自动被Unity导入及转换。该文件夹只能通过IO Stream以及WWW的方式进行资源加载。
注意:一般将下载的assetbundle放在这里,使用Application.persistentDataPath访问。
  各平台PersistentDataPath路径打印:
  Win:C:/Users/lodypig/Appdata/LocalLow/myCompany/myProj
  Mac : /Users/lodypig/Library/Application Support/myCompany/myProj
  Andorid:/data/data/com.myCompany.myProj/files
  iOS: /var/mobile/Containers/Data/Appliction/A112252D-6B0E-459Z-9D49-CD3EAC6D47D/Documents

www载入资源

概述:
WWW是一个Unity封装的网络下载模块,支持Http以及file两种URL协议,并会尝试将资源转换成Unity能使用的AssetsComponents(如果资源是Unity不支持的格式,则只能取出byte[])。WWW加载是异步方法。

1
2
3
4
5
6
byte[] bytes = WWW.bytes;
string text = WWW.text;
Texture2D texture = WWW.texture;
MovieTexture movie = WWW.movie;
AssetBundle assetbundle = WWW.assetBundle;
AudioClip audioClip = WWW.audioClip;

相关机制:
new WWW
每次new WWW时,Unity都会启用一个线程去进行下载。通过此方式读取或者下载资源,会在内存中生成WebStream,WebStream为下载文件转换后的内容,占用内存较大。使用WWW.Dispose将终止仍在加载过程中的进程,并释放掉内存中的WebStream。
如果WWW不及时释放,将占用大量的内存,推荐搭配using方式使用,以下两种方式等价。

1
2
3
4
5
6
7
8
9
10
11
WWW www = new WWW(Application.streamingAssetsPath+"fileName");
try
{
yield return www;
www.text;
www.texture;
}
finally
{
www.Dispose();
}

1
2
3
4
5
6
using(WWW www = new WWW( Application.streamingAssetsPath+"fileName"))
{
yield return www;
www.text;
www.texture;
}

如果载入的为Assetbundle且进行过压缩,则还会在内存中占用一份AssetBundle解压用的缓冲区Deompresion Buffer,AssetBundle压缩格式的不同会影响此区域的大小。

1
2
3
WWW.LoadFromCacheOrDownload
int version = 1;
WWW.LoadFromCacheOrDownload(PathURL+"/fileName",version);

使用此方式加载,将先从硬盘上的存储区域查找是否有对应的资源,再验证本地Version与传入值之间的关系,如果传入的Version>本地,则从传入的URL地址下载资源,并缓存到硬盘,替换掉现有资源,如果传入Version<=本地,则直接从本地读取资源;如果本地没有存储资源,则下载资源。此方法的存储路径无法设定以及访问。使用此方法载入资源,不会在内存中生成 WebStream(其实已经将WebStream保存在本地),如果硬盘空间不够进行存储,将自动使用new WWW方法加载,并在内存中生成WebStream。在本地存储中,使用fileName作为标识符,所以更换URL地址而不更改文件名,将不会造成缓存资源的变更。 保存的路径无法更改,也没有接口去获取此路径。

各种ID认知

GUID与fileID(本地ID)
Unity会为每个导入到Assets目录中的资源创建一个meta文件,文件中记录了GUID,GUID用来记录资源之间的引用关系。还有fileID(本地ID),用于标识资源内部的资源。资源间的依赖关系通过GUID来确定;资源内部的依赖关系使用fileID来确定。
InstanceID(实例ID)
Unity为了在运行时,提升资源管理的效率,会在内部维护一个缓存表,负责将文件的GUID与fileID转换成为整数数值,这个数值在本次会话中是唯一的,称作实例ID(InstanceID)。程序启动时,实例ID缓存与所有工程内建的对象(例如在场景中被引用),以及Resource文件夹下的所有对象,都会被一起初始化。如果在运行时导入了新的资源,或从AssetBundle中载入了新的对象,缓存会被更新,并为这些对象添加相应条目。实例ID仅在失效时才会被从缓存中移除,当提供了指定文件GUID和fileID的AssetBundle被卸载时会产生移除操作。卸载AssetBundle会使实例ID失效,实例ID与其文件GUID和fileID之间的映射会被删除以便节省内存。重新载入AssetBundle后,载入的每个对象都会获得新的实例ID。

资源的生命周期和内存管理

Object从内存中加载或卸载的时间点是定义好的。Object有两种加载方式:自动加载与外部加载。当对象的实例ID与对象本身解引用,对象当前未被加载到内存中,而且可以定位到对象的源数据,此时对象会被自动加载。对象也可以外部加载,通过在脚本中创建对象或者调用资源加载API来载入对象(例如:AssetBundle.LoadAsset)。
对象加载后,Unity会尝试修复任何可能存在的引用关系,通过将每个引用文件的GUID与FileID转化成实例ID的方式。一旦对象的实例ID被解引用且满足以下两个标准时,对象会被强制加载:
实例ID引用了一个没有被加载的对象。
实例ID在缓存中存在对应的有效GUID和本地ID。
如果文件GUID和本地ID没有实例ID,或一个已卸载对象的实例ID引用了非法的文件GUID和本地ID,则引用本身会被保留,但实例对象不会被加载。在Unity编辑器中表现为空引用,在运行的应用中,或场景视图里,空对象会以多种方式表示,取决于丢失对象的类型:网格会变得不可见,纹理呈现为紫红色等等。

注意:
不管是www还是reatefromfile创建Assetbundle都是创建了一个文件内存镜像。直到AssetBundle.LoadAsset或者Resource.Load才真正创建出了asset,而instaniate复制了这个对象,包括深拷贝和浅拷贝,比如比较大的texture是只读的,肯定是浅拷贝(拷贝引用)。
这里其实说的是一个资源从加载到实例化有三块内存被创建,对应的三种内存释放:
1 文件内存释放 assetbundle.unload
2 实例化的object内存通过destroy释放
3 AssetBundle.Unload(true)不单会释放文件内存镜像,还会释放AssetBundle.Load创建的Assets。这个方法是不安全的,除非你能保证这些Assets没有Object在引用,否则就出问题了。
4 Resources.UnloadAsset和Resources.UnloadUnusedAssets可以用来释放Asset。

说明

Unity5以及之后的版本打包已经大大简化。在每个资源的设置中设置好包体名称后,一般打包过程只需要BuildPipeline.BuildAssetBundles一句话就行了,Unity5会根据依赖关系自动生成所有的包。每个包还会生成一个manifest文件,这个文件描述了包大小、crc验证、包之间的依赖关系等等,通过这个manifest打包工具在下次打包的时候可以判断哪些包中的资源有改变,只打包资源改变的包,加快了打包速度。manifest只是打包工具自己用的,发布包的时候并不需要。

AB的工作流程

简要步骤

将某个资源分配给一个AB,基本是以下步骤:
1.从工程目录下,选择你想添加到一个AB的资源
2.检查inspector下面的选中对象
3.在inspector底部,可以看到分配AB和Variants的区域
4.左边是AB,右边是Varriants(变种,你也可以理解为版本?)
5.6.7制定自定义的AB名字,AB名字支持/来定义子目录,比如environment/forest
8.variants名字并不是必须的

至于怎么组织和管理AB,有一些常用的建议:
1 同频率同时间使用的资源最好同时打包(和加载)
2 同一个对象的相关模型、贴图、动画最好同时加载
3 如果不同包的不同游戏对象同时依赖与另一个不同的AB包,将依赖单独做成一个包。减少重复。
4 如果两套资源不可能在同一时间加载,务必放在单独的AB包里。
5 如果这个包里50%的资源很少同时被夹在,单独打包。
6 频率使用高但是资源相对少的包可以一起打包
7 同一个对象的不同版本,设置成不同的Variants名称

打包

最通用的打包方式,打包所有被配置过的AB资源,不会自动分配AB名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using UnityEditor;
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);
}
}

打包的核心代码,必须在编辑模式下才有BuildPipeline 的接口,打包的目标是你所配置过的资源对象。如果是None的话是不会被打包的。打包的路径是你所指定的路径。
BuildAssetBundleOptions.None:使用LZMA算法压缩,压缩的包更小,但是加载时间更长。使用之前需要整体解压。一旦被解压,这个包会使用LZ4重新压缩。使用资源的时候不需要整体解压。在下载的时候可以使用LZMA算法,一旦它被下载了之后,它会使用LZ4算法保存到本地上。
BuildAssetBundleOptions.UncompressedAssetBundle:不压缩,包大,加载快
BuildAssetBundleOptions.ChunkBasedCompression:使用LZ4压缩,压缩率没有LZMA高,但是我们可以加载指定资源而不用解压全部。
注意使用LZ4压缩,可以获得可以跟不压缩想媲美的加载速度,而且比不压缩文件要小。

测试结果:
理论上应该只有我们自定义的test和相应的manifest文件。但是多出来一个,是根据目录来命名的一个AB文件和相应的manifest文件。包含这次打包所有的包信息以及依赖信息。(以后再说用处)参考图ab001,ab002.
The AssetBundle File也就是没有manifest后缀的文件,就是你在游戏运行时需要加载资源的文件。(文件内部)一般的结构是这样:参考图ab003
The manifest文件,包含着CLC(cylic redundancy check)循环冗余校验和bundle 的依赖。
materialab.manifest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ManifestFileVersion: 0
CRC: 143795399
Hashes:
AssetFileHash:
serializedVersion: 2
Hash: 0105f2fffeb3f03e3478a86bcb970218
TypeTreeHash:
serializedVersion: 2
Hash: cc983a3149e5fb03ce027e70c7a1a559
HashAppended: 0
ClassTypes:
- Class: 21
Script: {instanceID: 0}
- Class: 28
Script: {instanceID: 0}
- Class: 48
Script: {instanceID: 0}
Assets:
- Assets/AB/cubematerials.mat
Dependencies:
- C:/Users/hp/Desktop/UnityTestDemoAll/Assets/AssetBundles/textureab

包含资源、依赖以及其他信息,另外一个是这样(AssetBundle.manifest):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ManifestFileVersion: 0
CRC: 1228010060
AssetBundleManifest:
AssetBundleInfos:
Info_0:
Name: test
Dependencies: {}
Info_1:
Name: materialab
Dependencies:
Dependency_0: textureab
Info_2:
Name: textureab
Dependencies: {}

这里包含了包的依赖信息,可以利用这个信息加载一些依赖包:

1
2
3
4
5
6
7
8
9
10
11
AssetBundle manifesAB = AssetBundle.LoadFromFile("AssetBundles/AssetBundles");
AssetBundleManifest manifest= manifesAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
foreach (string name in manifest.GetAllAssetBundles())
{
print(name);
}
string []strs=manifest.GetAllDependencies("Cube.ab");
foreach (var name in strs)
{
AssetBundle.LoadFromFile("AssetBundles/"+name);
}

上传包

第三方服务器或者本地均可

加载AssetBundles

核心:AssetBundles.LoadFromFile API
简单用例1:

1
2
3
4
5
6
7
8
9
10
11
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);
}
}

简单用例2:

1
2
3
4
5
6
7
8
9
10
11
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);
}

GetAssetBundle(string,int)加载包的目标位置和版本。UnityWebRequest有一个特殊的handleDownloadHandlerAssetBundle,将从request拿到AB(这里的request应该是http/ftp请求之类的)。
然后Unity会将这部分资源加载到内存镜像,然后使用loadasset加载对象,类似于resource.load之类的。

加载和使用的四种API:
1AssetBundle.LoadFromMemoryAsync
2AssetBundle.LoadFromFile
3WWW.LoadfromCacheOrDownload
4UnityWebRequest’s DownloadHandlerAssetBundle (Unity 5.3 or newer)

1-AssetBundle.LoadFromMemoryAsync
加载包含AB资源包的二进制数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}
}

然而,这并不是让使用LaodFromMemoryAsync成为可能的唯一途径。File.ReadAllBytes(path)可以被任何其他类似的接口或者方法代替。

2-AssetBundle.LoadFromFile
2.1同步加载
加载非压缩数据时非常高效,如果是使用这个方法加载LZMA数据的话肯定要先解压再加载到内存中。

1
2
3
4
5
6
7
8
9
10
11
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);
}
}

注意在不同平台/不同路径类型上可能会有不一致:
比如说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AssetBundleLoader {
// 根据不同平台,声明StreamingAssetsPath路径
public static readonly string STREAMING_ASSET_PATH =
#if UNITY_ANDROID
Application.dataPath + "!assets"; // 安卓平台
#else
Application.streamingAssetsPath; // 其他平台
#endif
// 从StreamingAssetsPath加载
public static AssetBundle LoadFromStreamingAssetsPath(string assetbundle) {
return AssetBundle.LoadFromFile(STREAMING_ASSET_PATH + "/" + assetbundle);
}
// PersistantDataPath加载
public static AssetBundle LoadFromPersistantDataPath(string assetbundle) {
return AssetBundle.LoadFromFile(Application.persistentDataPath+ "/" + assetbundle)
}
}

2.2异步加载
核心函数:AssetBundleCreateRequest AssetBundle.LoadFromFileAsync(string path, uint crc = 0, ulong offset = 0);
异步加载类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AssetBundleLoader : MonoBehaviour {
// 声明StreamingAssetsPath如上
public static readonly string STREAMING_ASSET_PATH = ...
// 协程实现
static IEnumerator LoadAsyncCoroutine(string path, Action<AssetBundle> callback) {
AssetBundleCreateRequest abcr = AssetBundle.LoadFromFileAsync(path);
yield return abcr;
callback(abcr.assetBundle);
}
// 开启协程
static bool LoadAssetbundleAsync(string finalPath, Action<AssetBundle> callback)
{
StartCoroutine(LoadAsyncCoroutine(finalPath, callback));
}
// 从StreamingAssetsPath异步加载
public static AssetBundle LoadFromStreamingAssetsPathAsync(string assetbundle) {
return LoadAssetbundleAsync(STREAMING_ASSET_PATH + "/" + assetbundle);
}
// PersistantDataPath异步加载
public static AssetBundle LoadFromPersistantDataPathAsync(string assetbundle) {
return LoadAssetbundleAsync(Application.persistentDataPath+ "/" + assetbundle)
}
}

3WWW.LoadfromCacheOrDownload
将被弃用,会被unitywebrequest代替。简单将用法罗列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
using System.Collections;
public class LoadFromCacheOrDownloadExample : MonoBehaviour
{
IEnumerator Start ()
{
while (!Caching.ready)
yield return null;
var www = WWW.LoadFromCacheOrDownload("http://myserver.com/myassetBundle", 5);
yield return www;
if(!string.IsNullOrEmpty(www.error))
{
Debug.Log(www.error);
yield return;
}
var myLoadedAssetBundle = www.assetBundle;
var asset = myLoadedAssetBundle.mainAsset;
}
}

建议:
移动平台上对内存限制比较苛刻,所以该方法相对来说比较耗内存和资源,尽量保持同时只有一个包在被下载中。而且,尽量限制下载包体大小在少量MB大小。
同时,如果缓冲区比较小,该方法也会自动删除掉旧的AB包,除非空间再次被满足。如果实在空间不足,该方法会同时支配一部分内存出来来存储数据流。

4-UnityWebRequest’s DownloadHandlerAssetBundle (Unity 5.3 or newer)
下面是一个下载两个物体的一个包体的实例代码:

1
2
3
4
5
6
7
8
9
10
11
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);
}

方法具体的流程是创建一个request对象,然后将这个对象传递给DownloadHandlerAssetBundle.GetContent(request),getcontent会返回AB对象。
这个方法的优点是,它容许开发者更灵活地处理下载数据。(怎么灵活?)

加载资源

T objectFromBundle = bundleObject.LoadAsset(assetName);
基本的方法包括LoadAsset,LoadAllAsset以及相应的异步方法loadassetasync/loadassetasync
加载一个单的的gameobject:
GameOject gameObject = loadedAssetBundle.LoadAsset(assetName);
加载一个AB包里的所有对象:
Unity.Object[] = ojjectArray = loadedAssetBundle.LoadAllAssets();

之前的方法返回的都是对象的类型或者对象数组,异步的(加载)方法返回的是一个AssetBundleRequest。在完成操作完成之后才可以访问资源:

1
2
3
AssetBundleRequest request = loadedAssetBundleObject.LoadAssetAsync<GameObject>(assetName)
yield return request;
var loadedAsset = request.asset;

1
2
3
AssetBundleRequesr request = loadedAssetBundle.LoadAllAssetAsync();
yield return request;
var loadedAssets = request.allAssets;

AB依赖

如果一个或者多个UnityEngine.object引用了另一个bundle 的UnityEngine.object对象,就会形成依赖。比如Bundle A的 材料引用了Bundle B的贴图,一般B要在A之前加载。需要注意的是,在加载这个物体前,要确保贴图已经加载,其实跟具体包的加载顺序不是特别相关。

加载AB Manifest

在处理包的依赖的时候,AB manifest非常有用。
为了得到AssetBundleManifest对象,需要先加载额外的AB包(对,同名同姓那个),然后加载一个AssetBundleManifest类型的对象。
加载ABManifest方法同AB基本相同:

1
2
AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

现在可以从该文件加载manifest文件信息,包括依赖数据/哈希数据/variants数据。正是manifest让动态地搜索依赖变得可能。比如,需要加载“assetbundle”的所有依赖:

1
2
3
4
5
6
7
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));
}

现在,加载了AB,也加载了AB的依赖,以及资源。

管理加载资源

关于管理加载资源的详细教程,也可以参考这里:
https://unity3d.com/fr/learn/tutorials/topics/best-practices/assetbundle-usage-patterns?&_ga=2.211534165.1107157193.1532930499-1934737889.1507532948#Managing_Loaded_Assets
unity不会自动地将对象从活动的scene 里面卸载,asset资源的清理在特定的时间被触发,或者可以手动触发。知道何时加载和卸载资源比较关键。
首先AssetBundle.Unload(bool),unload是一个卸载AB包的非静态方法,该API卸载被调用的AB的header信息,这个参数也同时决定是不是要卸载从这个AB包实例化的对象。

AssetBundle.Unload(true)
卸载(unload)从该AB加载的所有的游戏对象(包括依赖),但是不包括复制的GameObjects(比如实例化的GameObjects),因为这些游戏对象以及不属于AB(而是属于内存)
这种情况下,如果是从该AB加载的纹理资源,将会消失,会被unity当作missing textures。
假设,材质M是从资源包AB加载过来的:
如果调用AB.Unload(true):
活动场景中所有的M实例都会被销毁。
AB.Unload(false)
只是打断了实例M和AB包之间的联系。如果AB再次被加载(AB.LoadAsset()被调用),unity 并不会重新将M和新加载的材质M链接,所以会造成有两个M材质的实例拷贝。
我自己做的测试:
一个材质materialcube,打包到materialab
一个贴图texturecube,打包到textureab
materialcube材质依赖贴图textureab
按照不同的顺序加载和卸载游戏资源的和对象,结果略;

所以,通常的两种卸载包的方案比较
AB.Unload(true):
保证物体不会被重复复制,两种通常的方案
1 很清晰地知道什么对象什么资源会在何时被创建和卸载,关卡之间或者在执行加载界面的时候
2 为独立的对象维护引用计数,并当且仅当所有他们的组成对象没有被使用时卸载AB,这容许应用程序在不复制内存的情况下卸载和重新加载独立的对象。
AB.Unload(false):
如果一个应用程序必须使用unloadfalse,独立的对象有两种方法被卸载:
1 消除所有不期望对象的引用,包括代码中的或者场景中的,然后,调用 Resources.UnloadUnusedAssets.
2 Load a scene non-additively,这会销毁当前场景中的所有对象,并且自动唤醒 Resources.UnloadUnusedAssets.

AssetBundle Manager

如果你不想手动管理AB,我们来探讨AB Manager。看了下简单来说就是不用真的创建和加载AB就可以调试,并没有多少用。需要学习的自行转移https://docs.unity3d.com/Manual/AssetBundles-Manager.html

AssetBundle Patching System

打包、下载还是更新包都比较容易,如果使用WWW.LoadFromCacheOrDownload 和 UnityWebRequest进行应用缓冲AB的管理,传递不同的版本参数将会触发新资源包的下载。
Patching系统比较麻烦的事情是检测哪一个包需要被替代,一个打包系统需要两列(list)信息:
当前已经被下载的包名单和相应的版本信息
服务器上的包列表以及他们的版本信息
打包器(The patcher)需要下载服务器上的包列表名单并跟本地的比较,缺失的包以及版本有更新的包都需要重新下载。一般来说,开发者自己来进行版本控制,比如通过JSON文件和标准的C#类来行进行校验,比如使用MD5。
Unity以确定的排序方式来构建AB,这容许程序有自定义的下载系统来进行差异修补。但是并没有为差异化打包提供任何的内建机制,不管是WWW.LoadFromCacheOrDownload还是UnityWebRequest都没有。如果需要差异化打包的话,还是需要自己来设计整个下载系统。

几个常见的问题

资源重复

如果两个不同的对象被打包进了不同的AB包,但两个对象都引用了共同的另外一个对象(依赖对象),那这个依赖对象会被拷贝进这两个AB。重复的依赖也会被实例化,这就意味着依赖对象的两份拷贝会被当成两个不同的对象从而有不同的ID。这会增加整个应用程序的包体大小。也会造成如果应用加载了这两个父对象的话,这个依赖对象的两份拷贝也会被加载进内存中。避免这个问题有三种方案:
1 避免打包到不同AB的对象不共享依赖对象,哪个包依赖就打包到哪个AB包里。但是这种方法对那种有特别多shader 的项目不适用,会造成单片的不可分割AB包必须被频繁地重构和重新下载。
2 分割AB包,使得不会同时加载依赖同一个对象所在包的不同包。在某种程度上还是有用的,尤其在关卡类型的项目中,但是对减少包体大小、游戏build时间和加载时间仍然没有好处。
3 确保所有的依赖资源被打包进自己的包体,从而大大减少重复包体的风险,同时也增加了复杂度。应用程序必须跟踪不同包体间的依赖关系,确保在执行任何AssetBundle.LoadAsset调用前正确地加载所有的包。

Unity5中,包体依赖可以通过AssetDataBase API追踪(在UnityEditor模块中),AssetDatabase.GetDependencies 能够用来定位特定对象或者资源的即时依赖信息。要注意,这些依赖还有可能有自己的依赖。另外, AssetImporter API 可以被用来查询特定的对象被分配给那个AB。
结合AssetDatabase and AssetImporter APIs,可以在通过自己构建Editor代码来保证所有AB包直接和间接的依赖能够指派给AB包(指的就是这些个对象所在的AB包),不会出现:没有两个AB共享依赖的包但是却没有被打包进相应的包里,所有的项目都强烈建议使用这样一个打包的脚本。

精灵地图集复制

这块说的是资源打包和精灵地图自动生成之间的冲突。
有关Sprite Atlas的资料请阅读 https://docs.unity3d.com/Manual/SpriteAtlas.html
简单来说Sprite也是一种资源,也可以作为AB打包的资源内容。所有自动生成的sprite atlas会被分配给这个SA包含的sprite被打包的那个包中。如果这个SA下的sprites对象被指定给了多个AB包,那么这个SA就不会被分配给某个AB,而且会被复制。如果这些sprites 并没有给分配给任何一个AB,那么这个SA也并不会被分配给任何AB。
所以这里就又产生了资源重复打包的问题,跟之前类似,而且对应不同版本有不同的解决方法。

安卓贴图

由于安卓系统的碎片化问题比较严重,所以贴图需要被压缩成不同的格式。然而,所有安卓设备支持ETC1,但是ETC1不支持带有透明通道的贴图。如果一个应用不需要OpenGLES2 支持的话,那么最清晰的方法就是使用ETC2,这是被所有OPENGLES3设备支持的。其他的不详述。

Unity Asset Bundle Browser Tool

这是unity官方的一个工具,使得我们能够可视化编辑AB的配置。它会自动屏蔽掉会生成无效AB的,并通知你一些存在的问题。当然也支持最基本的打包功能。

AssetBundle的实际用例

打包解包完整解决方案

实现思路
一,确定好什么资源是不变化的,直接扔到resources里面就可以,需要更新的放到assetbundle这里。如何来确定一个资源是从Resources加载还是AssetBundle加载。为此我们需要一个配置文件resourcesinfo。这个文件随打包过程自动生成。里面包含了资源版本号version,所有包的名字,每个包的HashCode以及每个包里面包含的资源的名字。HashCode直接可以从Unity生成的manifest中得到(AssetBundleManifest.GetAssetBundleHash),用来检查包的内容是否发生变化。这个resourceinfo每次打包AssetBundle时都会生成一个,发布增量时将它和新的Bundle一起全部复制到服务器上。同时在Resources文件夹下也存一份,随完整安装包发布,这就保证了新安装游戏的玩家手机上也有一份完整的资源配置文件,记录了这个完整包包含的资源。

二,AB的粒度问题。两个极端情况的取舍,一个是一个资源一个ab包,开销比较大。另一个极端是,所有资源一个包,显然如果有不同场景、关卡、角色的不同资源,这样只会给内存带来负担,所以,一般根据我们之前提到的类型来划分和分配资源包。

三,游戏启动,请求服务器版本号,客户端用的版本号就是存在Resources下面的resourcesinfo中的version。对比版本号,是否更新。
更新:获取服务器的resourceinfo(说白了就是资源版本信息),对比每个bundle 的hashcode,下载更新的资源包。下载完成后,保存新的服务器的这个resourcesinfo到本地resources。
可以有一个ResourceManager类,先读取resourcesinfo,知道了所有游戏中bundle 的资源,去外部存储路径搜索这个资源是不是存在,在的话加载,不在,就从resources加载。

四,加载AssetBundle,我们直接使用WWW类而不用WWW.LoadFromCacheOrDownload, 因为我们的资源在游戏开始的时候已经下载到外部存储了,不要再Download也不要再Cache。注意WWW类加载是异步的,在游戏中我们需要同步加载资源的地方就要注意把资源预加载好存在ResourceManager中,不然等用的时候加载肯定要写异步代码了。大部分时候我们应该在一个场景初始化时就预加载好所有资源,用的时候直接从ResourceManager的缓存取就可以了。

所以设置BundleName这个工作最好还是由编辑器脚本来完成。
打包方案

步骤1 设置目标AssetBundle的包体名和变种名
用asset.assetBundleName=“text”设置AssetBundleName,核心函数AssetImporter asset= AssetImporter.GetAtPath(path);方法获取AssetImporter

方案一,手动设置每个资源的包体名,然后默认所有配置资源都打包
不需要代码,手动设置即可,适用于资源项目不是特别多而且你很闲的时候。

方案二,按照资源文件路径/文件名来设置包体名称。
这里面的一些核心机制和API:

Object[] selects = Selection.objects;//选中的所有对象
Object selected in selects //selected表示所选的对象
selected.name //所选对象的名称string 不包含后缀名

string path = AssetDatabase.GetAssetPath(selected);//对象的全路径字符串,包含对象及其扩展名:Assets/Resources/ab1/model1.prefab
AssetImporter asset = AssetImporter.GetAtPath(path);//这里path必须是资源的全路径:Assets/Resources/ab1/model1.prefab
asset.assetBundleName = “”; //设置Bundle文件的名称
asset.assetBundleVariant = “unity3d”;//设置Bundle文件的扩展名
asset.SaveAndReimport();//编辑器下重新导入资源
AssetDatabase.Refresh();//刷新显示

步骤2 打包 BuildPipeline.BuildAssetBundles(dataPath, Options, BuildTarget);(其他不常用的API略)
打包输出的目录为datapath
如果打包名称中有类似结构://ab2/picprefab.unity3d(具体的资源完整路径名,包含有资源名),则会在打包的路径下按照这个组织形式新建文档路径;

方案一:直接打包

方案二:使用buildmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class BuildAssetBundlesBuildMapExample : MonoBehaviour
{
[MenuItem("Example/Build Asset Bundles Using BuildMap")]
static void BuildMapABs()
{
// 创建映射数组
AssetBundleBuild[] buildMap = new AssetBundleBuild[2];
//修改assetBundleName第一个
buildMap[0].assetBundleName = "enemybundle";
//assetBundleName = "enemybundle"下的所有资源名称数组
string[] enemyAssets = new string[2];
enemyAssets[0] = "Assets/Textures/char_enemy_alienShip.jpg";
enemyAssets[1] = "Assets/Textures/char_enemy_alienShip-damaged.jpg";
buildMap[0].assetNames = enemyAssets;
//修改assetBundleName第二个
buildMap[1].assetBundleName = "herobundle";
//assetBundleName = "herobundle"下的所有资源名称数组
string[] heroAssets = new string[1];
heroAssets[0] = "char_hero_beanMan";
buildMap[1].assetNames = heroAssets;
//创建Bundle包
//将这些资源包放在一个名为ABs的目录下
string assetBundleDirectory = "Assets/ABs";
//如果目录不存在,就创建一个目录
if (!Directory.Exists(assetBundleDirectory))
{
Directory.CreateDirectory(assetBundleDirectory);
}
BuildPipeline.BuildAssetBundles(assetBundleDirectory, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
}
}

一些辅助API
1 需要用到的一些处理路径,或者建立打包目录方案的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//public static string sourcePath = Application.dataPath + "/Resources";
//传入路径,这是需要打包的资源所在的总路径
DirectoryInfo folder = new DirectoryInfo(sourcePath);
FileSystemInfo[] files = folder.GetFileSystemInfos();//files返回的是这个路径下的子目录
files[i] is DirectoryInfo//判断这个子目录是不是还是目录,是的话继续搜索当前目录
//不是的话,这是我们要找到的资源
!files[i].Name.EndsWith(".meta")//如果这个资源不是meta资源,那么就是我们要锁定的资源本身,接下来根据这个处理路径字符串给资源分配相应的资源包名称
static void file(string source)//最终给资源命名,这个命名带有目录结构,所以自动按照资源包名称安排目录结构,一个资源对应一个资源包
{
string _source = Replace(source);//source =
string _assetPath = "Assets" + _source.Substring(Application.dataPath.Length);//Assets/Resources/ab1/model1.prefab
string _assetPath2 = _source.Substring(Application.dataPath.Length + 1);//Resources/ab1/model1.prefab
//在代码中给资源设置AssetBundleName
AssetImporter assetImporter = AssetImporter.GetAtPath(_assetPath);
string assetName = _assetPath2.Substring(_assetPath2.IndexOf("/") + 1);//ab2/picprefab.prefab
assetName = assetName.Replace(Path.GetExtension(assetName), ".unity3d");//ab2/picprefab.unity3d(具体的资源完整路径名,包含有资源名)
assetImporter.assetBundleName = assetName;
}

注意:
用IO流的DirectoryInfo.GetFileSystemInfos()和FileInfonfo获取完整目录(这种方法要注意:获取到的目录如果是”\”或者”//”要替换为“/”)。

2 清除所有AssetDataBase数据集里的AB包配置
var names = AssetDatabase.GetAllAssetBundleNames();//所有配置包体的名称
AssetDatabase.RemoveAssetBundleName(oldAssetBundleNames[j], true);//删除某个名称的包体的资源配置

3 当目录下的资源有任何变动或者重新导入、移动位置时,出发AssetPostprocessor方法,进行资源的打包,参考ab004,用AssetPostprocessor的OnPostprocessAllAssets方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  using UnityEngine;
using System.Collections;
using UnityEditor;
//自动设置Assetbundle名字为全路径--文件夹路径名_文件名
public class AutoSetTextureUISprite : AssetPostprocessor
{
static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
foreach (var str in importedAssets)
{
if (!str.EndsWith(".cs"))
{
AssetImporter importer = AssetImporter.GetAtPath(str);
importer.assetBundleName = str;
}
}
foreach (var str in deletedAssets)
{
if (!str.EndsWith(".cs"))
{
AssetImporter importer = AssetImporter.GetAtPath(str);
importer.assetBundleName = str;
}
}
for (var i = 0; i < movedAssets.Length; i++)
{
//Debug.Log("Moved Asset: " + movedAssets[i] + " from: " + movedFromAssetPaths[i]);
}
}
}

打包解包的整体解决方案代码参考这里github

解包方案
解压的包体路径
///解包
///按照固定路径读AB包,不管何种解压包,核心的方法和思路都在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class loadasset : MonoBehaviour
{
public void LoadAssetBundle()
{
Debug.Log(Application.streamingAssetsPath);
string path2 = Application.streamingAssetsPath + "/One/Model1";
//本地文件地址加载AssetBundle
AssetBundle asset2 = AssetBundle.LoadFromFile(path2);
if (asset2 != null)
{
GameObject obj = asset2.LoadAsset("Model1") as GameObject;
//实例化
Instantiate(obj);
}
}
}

解包的整个流程需要根据自己的资源管理合理规划和读取。所以并没有一套方案适用全部,需要的自己去搭建和测试。最近根据项目需求和版本管理简单写了一个适合自己的管理类。这里不做赘述。

配置游戏数据的本地读取

一般:解决思路是将xml文件直接打包使用
改进:excel-xml-prefab-ab-游戏运行读取数据
https://blog.csdn.net/cglzy1982/article/details/77033170

其他常见问题归纳

1 Resources.Load方法传入的资源路径需是从Resources文件夹下一级开始的相对路径且不能包含扩展名;而AssetBundle.LoadAsset方法传入的资源名需是从Assets文件开始的全路径且要包含扩展名。路径不区分大小写,建议全用小写,因为AssetBundle.GetAllAssetNames方法返回的资源名都是小写的。
2 Unity5打包AssetBundle时会自动处理依赖关系,但是在运行时加载的时候却不会,程序需要自己处理,先加载依赖包。
3 AssetBundle.CreateFromFile不能加载压缩过的AssetBundle,所以我们只能用WWW来异步加载AssetBundle。
4 在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平台不支持动态载入脚本。