Unity AB包学习笔记

前言

        这个笔记是我看b站教程所记录下来的笔记。大家可以去b站看一下这个教程:b站教程

了解什么是AB包

        AB包就是特定于平台的资产压缩包,有点类似压缩文件。这些资产包括∶模型、贴图、预设体、音效、材质球等等。但是它不支持将代码一并打包。

AB包的作用

        AB包相对于Resources加载,它可以更好的管理资源。Resources在项目出包的时候就被定死了,它是只读且无法修改。在Resources文件下的文件,无论是否被工程使用到它都会被打包。而AB包存储位置可自定义,压缩方式自定义且后期可以动态更新。而且AB包可以减少包体大小。这是因为AB包本身就有压缩资源,如果我们将AB包放置到网络上然后让玩家在包体安装好后再去下载AB包。那么初始包的包体大小就会减少了。(之所以减少包体大小是因为不同的包体大小每次宣发量是不一样的。我之前工作的时候听运营聊过100mb的包和100mb以下的包,宣发的钱差多了。)除此之外,AB包也支持热更新。

生成AB包资源文件

PS:我使用的Unity版本为2019.4.40f1,并且我在项目中已经安装好了Asset Bundle Browser1.7.0。

        生成AB包资源文件有两种方法(视频里面只说了两种):我们使用Unity编译器开发自定义打包工具(公司比较大或者是公司比较老就可能有这样的方法)和我们使用Unity官方提供的打包工具Asset Bundle Browser来进行打包。

        在开始前,我们首先要让资源和AB包关联。首先我们选中任意一个预制体,然后在Inspector窗口下选中AssetBundle选项并且new一个名字。如下图:

ConnectAB

这是用来表明这个预制体会打包到model这个包中。

        此时大家可以点击Windows→AssetBundle Browser来查看当前项目中AB包的信息。大家可以在它的Configure页签下看到一个model的选项,再次点击这个选项就可以看到我们之前设置的模型了。

        另一个页签Build就是我们用来打包的。Build Target是用来指定我们要打包到哪里的平台。Output Path是我们打包成功后输出的路径。Clear Folders表示是否要请空Output Path路径下的文件夹。Copy to StreamingAssets表示我们是否要将其复制到StreamingAssets文件夹中。Advanced Setting下Comperssion是压缩类型,LZMA压缩的大小最小但是每次加载要将全部的资源都解压出来,LZ4压缩的大小较大但是我们可以要一个资源解压一个。Exclude Type Information在资源包中不包含资源的类型信息。Force Rebuild重新打包时需要重新构建包,但是它和ClearFolders不同,它不会删除不再存在的包。lgnore Type Tree Changes增量构建检查时,忽略类型数的更改。Append Hash 将文件哈希值附加到资源包名上。Strict Mode严格模式,如果打包时报错了,则打包直接失败无法成功。Dry Run Build运行时构建。我们设置完后点击Build按钮就可以进行打包。

        打包出来后,大家可以看到一个主包和你设定的AB包的配置信息和资源(配置信息是文件后缀名为.manifest,资源是那些没有后缀名的二进制文件)。主包名和你设定文件名称相同。manifest包含了AB包的文件信息。当我们加载的时候它提供了关键信息:资源信息、依赖关系和版本信息等。

        最后一个页签Inspector是用来看包的信息。

使用代码加载AB包文件

同步和异步加载AB包

同步加载AB包代码如下:

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

namespace MyGame
{
public class ABTest : MonoBehaviour
{
void Start()
{
// 加载AB包
AssetBundle ab = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "model"));
GameObject obj = ab.LoadAsset<GameObject>("Cube");
GameObject.Instantiate(obj);
}
}
}

因为我们之前设定文件在项目打包后是不能打进包中的,所以我们才用streamingAssets文件存储。实际在项目中,AB包资源会存放在服务器中然后我们从服务器中下载。

        我们不能重复加载AB包比如下面的这段代码就会报错:The AssetBundle 'StreamingAssets' can't be loaded because another AssetBundle with the same files is already loaded.

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 System.IO;
using UnityEngine;

namespace MyGame
{
public class ABTest : MonoBehaviour
{
void Start()
{
// 加载AB包
AssetBundle ab = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "model"));
GameObject obj = ab.LoadAsset<GameObject>("Cube");
GameObject.Instantiate(obj);
AssetBundle ab2 = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "model"));
}
}
}

异步加载代码:

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
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace MyGame
{
public class ABTest : MonoBehaviour
{
void Start()
{
StartCoroutine(LoadABAsync("model", "Cube"));
}

// 异步加载
private IEnumerator LoadABAsync(string abName, string resName)
{
// 加载ab包
AssetBundleCreateRequest abcr = AssetBundle.LoadFromFileAsync(Path.Combine(Application.streamingAssetsPath, abName));
yield return abcr; // 等待ab包加载完
// 异步加载资源
AssetBundleRequest abr = abcr.assetBundle.LoadAssetAsync<GameObject>(resName);
yield return abr;
GameObject.Instantiate(abr.asset as GameObject);
yield return null;
}
}
}

我们可以调用AssetBundle.UnloadAllAssetBundles来卸载全部加载过的AB包。但是如果我们只想卸载个别的AB包,那么可以按照下面的代码执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace MyGame
{
public class ABTest : MonoBehaviour
{
void Start()
{
// 加载AB包
AssetBundle ab = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "model"));
ab.Unload(false);
}
}
}

更多关于AssetBundle的API大家可以查看官网的介绍:AssetBundle - Unity 脚本 API

代码加载AB包中的依赖

        我创建了一个新的材质命名为CubeMat,然后我让它被打包在mat包中。那么在我们加载model包之前,我们就要先让mat包被加载或者同时加载。而包之间的依赖,我们可以通过主包来获得。具体代码如下:

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
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace MyGame
{
public class ABTest : MonoBehaviour
{
void Start()
{
LoadAB();
}

private void LoadAB()
{
// 加载主包
AssetBundle mainAB = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "StandaloneWindows"));
// 在主包中默认就有这个存在
AssetBundleManifest manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
// 获取所有的依赖
string[] strs = manifest.GetAllDependencies("model");
foreach(string str in strs)
{
AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, str));
}
AssetBundle ab = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "model"));
GameObject obj = ab.LoadAsset<GameObject>("Cube");
GameObject.Instantiate(obj);
}
    }
}

AB包管理器

        视频中使用了单例模式,但是我感觉更本没有这个必要的直接静态类应该也可以的。这里我就贴一个简单的单例实现就是了。我看视频中在调用的时候出现了一个DontDestroyOnLoad,我想这个单例大概是继承了Test : MonoBehaviour。所以单例模式如下:

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Common
{
public class MonoSingle<T> : MonoBehaviour where T : MonoSingle<T>
{
private static T instance;
private static Object lockflag = new Object();
public static T Instance
{
get
{
if (instance == null)
{
lock (lockflag)
{
if (instance == null)
{
instance = FindObjectOfType<T>();
if (instance == null)
{
new GameObject(typeof(T).Name).AddComponent<T>();
}
instance.Init();
}
}
}
return instance;
}
}
void Awake()
{
if (instance == null)
{
instance = this.transform.GetComponent<T>();
instance.Init();
}
else Destroy(this.gameObject);
}

public virtual void Init()
{

}
}
}

具体的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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
using Common;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace MyGame
{
public class ABManager : MonoSingle<ABManager>
{
// 存储已经加载过的AB包
private Dictionary<string, AssetBundle> loadedAB = new Dictionary<string, AssetBundle>();

private string pathUrl = Application.streamingAssetsPath;

private string MainPackName
{
get
{
#if UNITY_IOS
return "IOS";
#elif UNITY_ANDROID
return "android";
#else
return "StandaloneWindows";
#endif
}
}

private AssetBundle mainPackage = null;
private AssetBundleManifest manifest = null;

// 加载依赖包
private void LoadDependencies(string abName)
{
if (loadedAB.ContainsKey(abName))
{
return;
}
if (mainPackage == null)
{
// 加载主包
mainPackage = AssetBundle.LoadFromFile(Path.Combine(pathUrl, MainPackName));
// 在主包中默认就有这个存在
manifest = mainPackage.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
}

// 获取所有的依赖
string[] strs = manifest.GetAllDependencies(abName);
foreach (string str in strs)
{
if (!loadedAB.ContainsKey(str))
{
loadedAB.Add(str, AssetBundle.LoadFromFile(Path.Combine(pathUrl, str)));
}
}
}

//同步加载
public Object loadRes(string abName, string resName)
{
if (loadedAB.ContainsKey(abName))
{
return loadedAB[abName].LoadAsset(resName);
}
LoadDependencies(abName);
loadedAB.Add(abName, AssetBundle.LoadFromFile(Path.Combine(pathUrl, abName)));
return loadedAB[abName].LoadAsset(resName);
}

public Object loadRes(string abName, string resName, System.Type type)
{
if (loadedAB.ContainsKey(abName))
{
return loadedAB[abName].LoadAsset(resName, type);
}
LoadDependencies(abName);
loadedAB.Add(abName, AssetBundle.LoadFromFile(Path.Combine(pathUrl, abName)));
return loadedAB[abName].LoadAsset(resName, type);
}

public T loadRes<T>(string abName, string resName, System.Type type) where T : UnityEngine.Object
{
if (loadedAB.ContainsKey(abName))
{
return loadedAB[abName].LoadAsset<T>(resName);
}
LoadDependencies(abName);
loadedAB.Add(abName, AssetBundle.LoadFromFile(Path.Combine(pathUrl, abName)));
return loadedAB[abName].LoadAsset<T>(resName);
}

// 卸载包
public void UnLoadAB(string abName, bool unLoadAllLoadedObj = false)
{
if (loadedAB.ContainsKey(abName))
{
loadedAB[abName].Unload(unLoadAllLoadedObj);
loadedAB.Remove(abName);
}
}
public void UnLoadAllAB(bool unLoadAllLoadedObj = false)
{
loadedAB.Clear();
AssetBundle.UnloadAllAssetBundles(unLoadAllLoadedObj);
mainPackage = null;
manifest = null;
}
}
}

视频是存在异步加载,但是他只是讲了异步加载AB包资源,而AB包加载还是同步的。所以我就没有加上去。以后我自己要是遇到这种加载方法,那我再加上去。