AB包简介

AssetBundle(简称AB包)是一个资源压缩包,包括模型、贴图、预制体、声音甚至整个游戏场景,它可以在游戏运行时加载。AssetBundle能够自动处理资源的依赖关系,比如一个AB包的材质能够引用另一个AB包中的贴图。通过将游戏资源打包成AB包并上传服务器,这样可以避免所有资源都打包进APK,从而减少APK的大小,直到在游戏的运行过程中再按需加载AB包资源。

为了更有效的资源传输,Unity提供了LZMA和LZ4两种压缩算法,压缩后资源包的体积更小。Unity经历了从直接引用Assets目录资源,到Resources文件夹资源管理,再到AssetBundle资源管理的过程,AssetBundle资源管理是目前最佳的Unity资源管理方案。但我们在游戏开发期间仍然可以直接使用Resources文件夹进行资源管理,这样比较方便和节省时间。

AssetBundle是一个存在硬盘上的”文件夹“(AB包),里面包含了很多文件,分为Serialized file(序列化文件)和Resource file(源文件)。根据资源类型不同,打包成不同类型的文件。Serialized file是指很多资源被打碎放在一个对象中,最后统一被写进一个单独的文件(只有一个),如模型和预制体等必须在游戏场景中(Unity)才能看到的文件会打包为一个Serialized file。Resource files是指一些在电脑上能够不借助第三方特殊工具直接打开的文件,比如图片和声音等资源被单独保存,方便快速加载。使用AB包时,必须先在代码中通过AssetBundle对象来加载AB包,然后再从AB包中加载对应的资源。

在很多数据通信或者存储中,都需要序列化Serialize和反序列化Deserialize的过程。序列化,又称串行化,是指将对象转换成字节流Bytes,从而存储对象或将对象传输到内存、数据库或文件(二进制文件、XML文件或JSON文件)中。Unity中的模型和预制体就是序列化后的二进制文件,而在C#中通过Instantiate函数来实例化的过程就是反序列化,将二进制文件反序列化为一个游戏对象。而Unity中的声音和图片等源文件不需要序列化和反序列化,可以直接在代码中使用。

AssetBundle一般用于:非代码资源的热更新、减少游戏安装包大小和利用二进制文件进行资源加密,其中最主要的用途是非代码资源的热更新。比如,制作一个节日模型,直接放到AssetBundle然后上传服务器,存储AssetBundle包和它的版本号。在游戏启动检查资源更新时,就与服务器上的AB包和版本号进行比对,这样就可以只下载版本号变更的或者新上传的资源,即需要更新的资源,最后再解压使用。等第一次更新后,第二次启动时如果没有需要更新的资源就不会再下载AB包,从而保证了需要更新的资源只更新一次,更新下载的资源会保存在本地文件夹。

AB包使用

AssetBundle的使用步骤为:1)、指定资源的AssetBundle属性并添加资源后缀名。如果不添加资源后缀名,则打包出来的AB包也没有后缀名,可以正常加载。但为了资源的语义化和防止游戏被反编译后盗取素材,我们通常都要添加对应的资源后缀名。2)、构建AB包,通过在Editor文件夹下添加一个打包脚本来创建一个打包工具进行打包。3)、上传AB包至服务器。4)、加载AB包和包里的资源。打包脚本的具体代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using UnityEditor;
using System.IO;

public class CreateAssetBundles
{
    // 在Unity编辑器中Assets工具栏下的Build AssetBundles
    [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);
    }
}

Unity通过BuildPipeline类中BuildAssetBundles函数进行打包,BuildAssetBundles函数有三个参数,分别是AB包的输出路径,AB包的打包选项(是否压缩等),AB包使用的目标平台。其中,BuildAssetBundleOptions的三个选项如下:

1)、BuildAssetBundleOptions.None(LZMA):None表示默认选项,先使用LZMA算法压缩,压缩的包更小,但加载解压时间更长。如果要使用LZMA算法对应的压缩包中的一小部分资源,则需要整体解压,比较耗费时间。但一旦解压完成后,这个包又会使用LZ4重新压缩。LZ4算法对应的压缩包使用时不需要整体解压即可使用其中的一小部分资源。因此None选项表示资源打包时采用LZMA算法,这样下载的包更小,第一次整体解压后,就会使用LZ4算法压缩并保存在本地,这样加载的时间更快,因为不需要整体解压即可使用部分文件。

2)、BuildAssetBundleOptions.UncompressedAssetBundle:不压缩AssetBundle资源包,包大,但不需要解压,加载快。

3)、BuildAssetBundleOptions.ChunkBasedCompression(LZ4):使用LZ4压缩,压缩率没有LZMA高,压缩得到的包没有LZMA的小,但我们可以加载指定资源而不用解压全部。使用LZ4压缩可以获得跟不压缩的资源包差不多的加载速度,但包比不压缩的包更小。我们可以用Gzip对LZ4压缩包进行二次压缩,从而减少包的体积,但解压加载的速度不变。

AssetBundle能够自动处理资源之间的依赖关系,打包时会自动将该资源依赖的其他资源一同打包到该AB包。如果有一个资源被其他多个资源引用,则打包时该资源会被重复打包到多个不同的AB包。因此,通常将多个资源都依赖的公共资源单独打包,避免公共资源被重复打包浪费资源。如果公共资源被单独打包,则在其他AB包的Manifest文件中的Dependencies会显示依赖的AB包。相反,如果没有单独打包即只是隐式依赖,Manifest文件中将不会显示该依赖资源。

AssetBundle打包需要指定AB包名和后缀名,如果AB包名为"xxx/yyy",则表示”yyy“AB包生成在打包输出根目录下的xxx文件夹里。此外,AssetBundle采用增量式打包的方式,仅重新打包发生变化的资源或者需要打包的新资源,即只有当资源文件或者typetree发生变化时才会重新打包。如果清空已经打包好的资源,则Unity又会全部重新打包。Unity官方推出了AssetBundle Browser进行AB包管理,通过这个工具可以快速分析出资源之间的依赖关系,然后对其他资源依赖的公共资源进行单独打包,避免出现资源冗余,从而减少资源的浪费。

AssetBundle打包时会生成一个与资源对应的AB包和Manifest文件,Manifest文件包含了每一个AB包的相关配置信息。下面为一个AB包的Manifest文件:

 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
32
33
34
35
36
37
38
// Manifest文件的版本号
ManifestFileVersion: 0
// CRC文件校验码,确保传输文件的完整性。
CRC: 1509223660
// 用于LoadFromCacheOrDownload中加载AB包时的参数
Hashes:
  AssetFileHash:
    serializedVersion: 2
    Hash: bd4bbd44fb22d87e0d31782ed64fd436
  TypeTreeHash:
    serializedVersion: 2
    Hash: cf3f348e2267d190e6df40cbb42919b5
HashAppended: 0
ClassTypes:
- Class: 1
  Script: {instanceID: 0}
- Class: 4
  Script: {instanceID: 0}
- Class: 21
  Script: {instanceID: 0}
- Class: 23
  Script: {instanceID: 0}
- Class: 28
  Script: {instanceID: 0}
- Class: 33
  Script: {instanceID: 0}
- Class: 43
  Script: {instanceID: 0}
- Class: 48
  Script: {instanceID: 0}
- Class: 65
  Script: {instanceID: 0}
// 该AB包里包含的资源
Assets:
- Assets/Prefabs/CubeWall.prefab
// 依赖的AB包
Dependencies:
- /Users/neowyj/Unity/AssetBundleProject/Assets/AssetBundles/share.unity3d

除了单个资源对应的AB包和Manifest文件,每次打包时还会在输出目录下生成一个名为AssetBundles的AB包和对应的AssetBundles.manifest文件,AssetBundles.manifest文件记录了每次打包生成的所有AB包的依赖信息。

AB包加载

AB包加载最主要的方式有三种:LoadFromFile(Async)、WWW.LoadFromCacheOrDownload和UnityWebRequest。无论采用哪种加载方式,都有10-40 kb的额外内存开销,其中LoadFromFile(Async)加载速度最快,内存开销也最少。在加载AB包时,依赖资源的AB包必须先加载,加载后不需要做任何操作,因为其他依赖于该资源的AB包会自动获取依赖资源。下面是三种加载方式的介绍:

1)、LoadFromFile(Async),直接从本地文件中加载AB包,Async表示对应的异步加载方式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 先加载依赖的AB包,加载后不做任何操作。
AssetBundle share = AssetBundle.LoadFromFile("Assets/AssetBundles/share.unity3d");
// 加载cubewall资源的AB包
AssetBundle ab = AssetBundle.LoadFromFile("Assets/AssetBundles/cubewall.unity3d");

// 异步版本
AssetBundleCreateRequest request =
AssetBundle.LoadFromFileAsync("Assets/AssetBundles/cubewall.unity3d");
// yield表示等待异步加载完成后才执行下面的代码。
yield return request;
AssetBundle ab = request.assetBundle;

2)、WWW.LoadFromCacheOrDownload,从缓存中或者直接下载AB包,也可以从本地文件中加载AB包,只不过WWW类加载本地文件时使用的是绝对路径。WWW类适合在Unity2018以前使用,Unity2018及更新的版本使用UnityWebRequest替代WWW类。第一次下载AB包时,WWW会缓存这些资源,下次再加载相同的AB包时,就直接从缓存中获取,如果是新的AB包就重新下载对应的AB包。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
while (Caching.ready == false)
{
    // 暂停一帧,这一帧后面的代码不执行,直到下一帧再重新开始判断。
    yield return null; 
}
WWW www = WWW.LoadFromCacheOrDownload(@"file:///Users/neowyj/Unity/AssetBundleProject/Assets/AssetBundles/cubewall.unity3d", 1);
// Mac上没有一般不分盘,所以直接是file://+绝对路径。而Windows上有盘符路径为:file://盘符:+路径。
//WWW www = WWW.LoadFromCacheOrDownload(@"http://localhost:7000/AssetBundles/cubewall.unity3d", 1);
// WWW类即使有错误也不会报错停止运行,所以需要进行www错误检测,www.error不为空则表示有错。
if (!string.IsNullOrEmpty(www.error)) 
{
    Debug.Log(www.error);
    // 退出www类的下载过程,后面的代码不再执行。yield return只是针对当前帧进行等待操作。
    yield break; 
}
// 等待www下载完成再执行下面的代码
yield return www; 
AssetBundle ab = www.assetBundle;

3)、UnityWebRequest(重点),和WWW类功能相似,UnityWebRequest自身也有缓存系统,从Unity2018开始UnityWebRequest取代了WWW类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 使用UnityWebRequest从本地文件中加载AB包
// string uri = @"file:///Users/neowyj/Unity/AssetBundleProject/Assets/AssetBundles/cubewall.unity3d";
// 使用HTTP协议从服务器上下载AB包
string uri = @"http://localhost:8000/AssetBundles/cubewall.unity3d";
UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(uri);
// 发送资源请求,并等待下载完成。
yield return request.Send(); 
AssetBundle ab = DownloadHandlerAssetBundle.GetContent(request);
// 下面这种方式也可以获取AB包
// AssetBundle ab = (request.downloadHandler as DownloadHandlerAssetBundle).assetBundle;

从AB包里加载对应的资源也有三种方式:LoadAsset、LoadAssetAsync和LoadAllAssets。LoadAsset是最普通的加载资源方式,每次只能加载一个资源。在加载对应的资源时,必须确保对应的AB包先被加载出来。

1
2
3
4
// 从AB包里加载对应的游戏对象。注意,LoadAsset区分大小写,Unity也区分大小写,只是AssetBundle包名不区分。
GameObject CubeWallPrefab = ab.LoadAsset<GameObject>("CubeWall");
// 实例化该游戏对象,如果不指定对象实例化所需要的位置和旋转,则采用该游戏对象在Inspector面板中的默认位置和旋转。
Instantiate(CubeWallPrefab);

而LoadAssetAsync是LoadAsset的异步版本,需要用到AssetBundleRequest类,注意与异步加载AB包的类AssetBundleCreateRequest区分开。

1
2
3
4
5
AssetBundleRequest request = ab.LoadAssetAsync<GameObject>("CubeWall");
yield return request;
// request获取的asset就是实际的游戏对象或其他类型的资源
var loadedAsset = request.asset;
Instantiate(loadedAsset);

LoadAssets是加载AB包里的所有资源(不包括依赖的AB包中的资源)并返回一个Object数组。

1
2
3
4
5
6
7
Object[] objs = ab.LoadAllAssets(); 
foreach (Object o in objs)
{
    // Unity中的GameObject继承自Unity.Object
    Instantiate(o);
}
// 如果AB包里有父子关系的资源,则可以通过LoadSubAsset获取到子资源数组Object[]。

前面提到每次打包时都会生成一个AB包和对应的Manifest文件,但要想加载manifest文件首先需要加载对应的AB包,然后再从对应AB包里加载Manifest类型的资源。我们可以只加载总的AssetBundle.manifest文件,然后利用AssetBundleManifest对象的GetAllAssetBundles方法获取本次打包的所有AB包,然后再利用GetAllDependencies(AB包名)方法获取某个AB包依赖的AB包资源,这才是完整的公共资源包加载流程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// AssetBundles的AB包没有后缀,是系统自动创建的,其他资源打包时必须加后缀,除非打包时没有添加。
AssetBundle manifestAB = AssetBundle.LoadFromFile("Assets/AssetBundles/AssetBundles"); 
// 从AB包里加载对应的manifest文件,和加载GameObject资源一样,只是类型和资源名都是AssetBundleManifest。
AssetBundleManifest manifest = manifestAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
// 获取本次打包的所有AB包,返回一个存储AB包名字符串数组。
foreach (string name in manifest.GetAllAssetBundles()) 
{
    print(name);
}
// 先把依赖包找出来,然后再去加载对应的依赖包,这才是完整的公共资源包的加载流程。
// 获取AB包对应的依赖包时,需要传递一个AB包名,返回一个存储AB包名字符串数组。
string[] dependencies = manifest.GetAllDependencies("cubewall.unity3d"); 
foreach (string name in dependencies)
{
    print(name);
    // 只需要加载依赖的AB包,具体的依赖资源Unity会自动获取。
    AssetBundle.LoadFromFile("Assets/AssetBundles/"+ name);
}

AB包卸载

AB包的卸载要小心,因为如果卸载不当可能会造成资源重复或者资源丢失等问题。AB包的卸载有三种方式:AssetBundle.Unload(true)、AssetBundle.Unload(false)和Resource.UnloadUnusedAssets。每次AB包加载时都会先加载AB包的信息头,然后根据AB包的信息头去加载AB包的对象实例,AssetBundle.Unload()中的参数表示是否也卸载AB包的对象实例。

AssetBundle.Unload(true)表示AB包的信息头和对象实例都要一起卸载,即使该对象实例正在被使用。因此,使用AssetBundle.Unload(true)卸载AB包时,要确保所有资源都没有被使用,一般在关卡和场景切换时调用,因为前一关的所有资源都不会再被使用。对于大多数项目,都应该使用AssetBundle.Unload(true)卸载AB包,并且使用额外的方法来确保不会有重复的副本资源。一般常用的方法有两种:

1)、在应用生命周期中,在明确定义的时间点对临时的AssetBundle卸载,比如两个关卡或者场景切换时。

2)、维护单个物体的引用计数,并当组成AssetBundle的对象都未被加载时卸载AssetBundle。这允许应用卸载和重新加载对象而不会复制多余的内存。

AssetBundle.Unload(false)表示只卸载AB包的信息头和AB包里所有没在使用的资源,但被其他AB包使用的资源将不被卸载。假如A包里的材质M被B包使用,如果使用AssetBundle.Unload(false)卸载A包,A包中没在使用的资源会被卸载,但材质M不会被卸载,仍然可以在B包中使用。但这样会打破A包和材质M之间的依赖关系,即使重新加载了A包,它们之间的依赖关系也将丢失。

在重新加载A包后,由于依赖关系丢失,Unity会重新加载一个A包包括材质M的副本资源,从而导致系统中存在两个材质M。显然,AssetBundle.Unload(false)是不可取的,因此实际开发时很少使用。因为一旦AB包被卸载,依赖关系打破后且依赖资源仍在使用,当依赖资源不再被使用时,原来的AB包就无法卸载该资源。那么如何卸载没有使用且没有依赖关系的个别资源?那就是使用Resource.UnloadUnusedAssets。

Resource.UnloadUnusedAssets可以卸载AssetBundle和Resources文件夹中任何没有使用的资源。这个方法会在场景Application.LoadScene切换时会自动调用,我们也可以在脚本里手动调用它来卸载未使用的资源。

AB包分组

从AB包的加载和卸载可以看出,AB包的分组策略十分重要。只有在合理的分组策略下,我们才能更好地管理对AB包。常见的分组策略包括:按照逻辑实体分组、按照资源类型分组和按照使用时间分组。

按照逻辑实体分组:

1)、一个UI界面或者所有UI界面打成一个AB包,UI界面里的贴图和布局信息单独一个AB包。

2)、一个角色或者所有角色打成一个AB包,角色里面的模型和动画单独一个AB包。

3)、所有场景共享的部分打成一个AB包,包括场景中的模型和贴图。

按照资源类型分组:

所有声音资源打成一个AB包,所有Shader打成一个AB包,所有模型打成一个AB包,所有材质打成一个AB包。

按照使用时间分组:

把在某一个时间段内使用的所有资源打成一个AB包,可以按照关卡分,也可以按照场景分。在实际开发时,我们可以使用一个XML配置文件把当前场景中使用到的GameObject及它的位置、旋转和缩放等信息记录下来,然后根据XML配置文件使用Instantiate方法来动态加载场景中的GameObject。但在使用前必须先把当前场景中Hierarchy视图中GameObject制作成预制体,然后将该场景中的所有资源打成一个AB包。

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
using UnityEngine;
using UnityEditor;
using System.Xml;

// 把当前场景中的GameObject信息导出到XML文档中
public class Export
{
    // 编辑器扩展
    [MenuItem("Export/ExportSceneToXML")]
    static void ExportSceneToXML()
	{
        // 根据当前场景的路径获取场景名
        string scenePath = EditorApplication.currentScene;
        string sceneName = scenePath.Substring(scenePath.LastIndexOf("/", System.StringComparison.Ordinal)+1);
        sceneName = sceneName.Substring(0, sceneName.LastIndexOf(".", System.StringComparison.Ordinal));
        // 设置导出的XML文件路径
        string savePath = "Assets/SceneXML/" + sceneName + ".xml";

        // 新建一个XML文档
        XmlDocument xml = new XmlDocument();
        // 设置XML文档的根节点
        XmlElement scene = xml.CreateElement("Scene");
        // 设置根节点的属性:名字和保存路径
        scene.SetAttribute("Name", sceneName);
        scene.SetAttribute("Path", savePath);
        xml.AppendChild(scene);

        // 获取当前场景下的所有GameObject(包括Asset目录)
        Object[] objs = Resources.FindObjectsOfTypeAll(typeof(GameObject));
        foreach (GameObject go in objs)
        {
            // 只存储最顶层的GameObject,不考虑子对象,打AB包时也是如此,因为是一个AB包所以不存在公共资源问题。
            if (go.transform.parent == null)
            {
                // 记录GameObject的名字
                XmlElement gameObject = xml.CreateElement("GameObject");
                gameObject.SetAttribute("Name", go.name);
              
                // AssetBundle不能记录资源的位置、旋转和缩放等信息
                // 记录GameObject的位置信息
                XmlElement position = xml.CreateElement("Position");
                position.SetAttribute("x", go.transform.position.x.ToString());
                position.SetAttribute("y", go.transform.position.y.ToString());
                position.SetAttribute("z", go.transform.position.z.ToString());
                gameObject.AppendChild(position);

                // 记录GameObject的旋转信息
                XmlElement rotation = xml.CreateElement("Rotation");
                rotation.SetAttribute("x", go.transform.eulerAngles.x.ToString());
                rotation.SetAttribute("y", go.transform.eulerAngles.y.ToString());
                rotation.SetAttribute("z", go.transform.eulerAngles.z.ToString());
                gameObject.AppendChild(rotation);

                // 记录GameObject的缩放信息
                XmlElement scale = xml.CreateElement("Scale");
                scale.SetAttribute("x", go.transform.localScale.x.ToString());
                scale.SetAttribute("y", go.transform.localScale.y.ToString());
                scale.SetAttribute("z", go.transform.localScale.z.ToString());
                gameObject.AppendChild(scale);

                // 添加进XML文档
                scene.AppendChild(gameObject);
            }
        }

        // 保存为路径对应的XML文件
        xml.Save(savePath);
    }
}

接下来将当前场景中Hierarchy视图下的所有GameObject都制作成预制体Prefab并打成一个AB包,然后将它们从Hierarchy视图中全部删除,因为这时我们已经将当前场景的所有资源都打包了,以后加载场景时就直接从AB包里加载资源,最后再新建一个空GameObject用来挂载加载AB包的脚本。AB包打包完成后就上传服务器,等下次游戏启动时再下载到本地,从而实现动态加载场景资源。

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
using System;
using System.Xml;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Load : MonoBehaviour 
{
    // Use this for initialization
    void Start()
    {
        // AB包路径
        string abPath = @"http://localhost:9000/Assets/AssetBundles/main.unity3d";
        // XML文档路径
        string xmlPath = @"http://localhost:9000/Assets/SceneXML/Main.xml";
        // 开启动态加载场景协程
        IEnumerator routine = LoadDynamicScene(abPath, xmlPath);
        StartCoroutine(routine);
	  }
  
    // 动态加载场景的协程函数
    IEnumerator LoadDynamicScene(string abPath, string xmlPath)
    {
        // 如果缓存不可使用,直接跳过当前帧,直到可以使用时再执行后续代码。
        while (Caching.ready == false)
        {
            yield return null;
        }
        WWW www = WWW.LoadFromCacheOrDownload(abPath, 1);
        // 如果下载出错就直接退出协程函数,后续代码不再执行。
        if (!string.IsNullOrEmpty(www.error))
        {
            Debug.Log(www.error);
            yield break;
        }
        // 等www下载完之后再执行后续代码
        yield return www;
        // 从www获取AB包
        AssetBundle ab = www.assetBundle;

        // 读取XML文档
        XmlDocument xml = new XmlDocument();
        xml.Load(xmlPath);
        XmlElement root = xml.DocumentElement;
        if (root.Name == "Scene")
        {
            // 获取XML文档的根节点
            XmlNodeList nodes = root.SelectNodes("/Scene/GameObject");
            Vector3 position = Vector3.zero;
            Vector3 rotation = Vector3.zero;
            Vector3 scale = Vector3.zero;
            // 开始遍历XML文档节点,获取GameObject的信息
            foreach (XmlElement xe in nodes)
            {
                foreach (XmlElement xe2 in xe.ChildNodes)
                {
                    // 获取GameObject的位置信息
                    if (xe2.Name == "Position")
                    {
                        position = new Vector3(float.Parse(xe2.GetAttribute("x")), float.Parse(xe2.GetAttribute("y")), float.Parse(xe2.GetAttribute("z")));
                    }
                    // 获取GameObject的旋转信息
                    else if (xe2.Name == "Rotation")
                    {
                        rotation = new Vector3(float.Parse(xe2.GetAttribute("x")), float.Parse(xe2.GetAttribute("y")), float.Parse(xe2.GetAttribute("z")));
                    }
                    // 获取GameObject的缩放信息
                    else
                    {
                        scale = new Vector3(float.Parse(xe2.GetAttribute("x")), float.Parse(xe2.GetAttribute("y")), float.Parse(xe2.GetAttribute("z")));
                    }
                }
                // 根据GameObject的名字从AB包中加载对应的GameObject
                //GameObject go = ab.LoadAsset<GameObject>(xe.GetAttribute("Name"));
                GameObject go = ab.LoadAsset<GameObject>(xe.GetAttribute("Name"));
                // 如果GameObject不为空就用Instantiate函数实例化,并设置对应的Transform信息。
                if (go != null)
                {
                    GameObject go2 = Instantiate(go, position, Quaternion.Euler(rotation));
                    go2.transform.localScale = scale;
                }
            }
        }
    }

    void OnDestroy()
    {
        // 停止协程
        StopAllCoroutines();
    }
}

测试截图:

注意:

1)、把经常更新的资源打包在一起,跟不经常更新的资源分离,减少后期需要下载的AB包的大小。

2)、把需要同时加载的资源放在一个AB包里,避免同时下载多个AB包。

3)、把其他AB包共享的资源进行单独打包,比如多个预制体共用一个材质,材质就单独打一个包,每个预制体打一个包,各个预制体包都依赖于材质包,这样材质包就只需要打包一份,而不需要重复多次打包。

4)、把一些需要同时加载的”小资源“打成一个包,减少AB包的加载次数,节省加载时需要的内存开销。

5)、如果同一个资源有两个版本(即都有可能使用),可以考虑通过AB的后缀名来区分。

参考资料:AssetBundle 使用模式【译】Unity3D游戏开发之使用AssetBundle和Xml实现场景的动态加载