在网络游戏中,可能需要在运行时下载并更新资源,而Unity提供了AssetBundle组件,可以将指定的一部分资源构建成AssetBundle文件。如果需要下载,那么需要将这些AssetBundle文件上传到CDN上。

设置AssetBundle

选择需要构建的AssetBundle资源,在右下角写入资源名以及资源的后缀名,在代码中调用BuildPipeline.BuildAssetBundles()方法,只需提供一个输出的目录就可以构建AssetBundle了,最终将输出在AssetBundleFolder文件夹下

选择Prefab

设定名称和后缀

Editor文件夹内新建BuildAssetBundleScript脚本,注意一定要在Editor文件夹中

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

public class BuildAssetBundleScript
{
[MenuItem("Tools/BuildAssetBundles")]
static void BuildAssetbundles()
{
string outPath = Path.Combine(Application.dataPath, "StreamingAssets");

//如果目录已经存在,则删除它
if (Directory.Exists(outPath))
Directory.Delete(outPath,true);
Directory.CreateDirectory(outPath);

BuildPipeline.BuildAssetBundles(outPath, BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.StandaloneWindows);

AssetDatabase.Refresh();
}
}

执行BuildAssetBundles时一定要注意,如果场景中该放进Editor文件夹的脚本没有放在Editor文件夹里,就会全部报错

Unity - Scripting API: BuildPipeline.BuildAssetBundles (unity3d.com)

build结果

每个平台下的Bundle文件是不一样的,因此需要指定BuildTarget的构建平台。例如,在编辑模式下运行游戏,加载IOS或者Android的AssetBundle,尤其是Shader,都会显示错误,只能在真机上才能看到正确的效果

依赖关系

如果有两个Prefab都依赖了同一份材质和贴图文件,那么按照上面的打包方式,材质和贴图就会生成两份了。我们可以把它们依赖的材质和贴图也打成AssetBundle,这样两个Prefab就会自动依赖材质和贴图了。

材质打包

分别打开两个Prefab的关系文件(.manifest),就能清楚地看到它们共同依赖同一个材质。

显示依赖关系

通过脚本设置依赖关系

我们可以通过AssetBundleBuild,在脚本中指定打包

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

public class BuildAssetBundleScript
{
[MenuItem("Tools/BuildAssetBundles")]
static void BuildAssetbundles()
{
string outPath = Path.Combine(Application.dataPath, "StreamingAssets");

//如果目录已经存在,则删除它
if (Directory.Exists(outPath))
Directory.Delete(outPath,true);
Directory.CreateDirectory(outPath);

List<AssetBundleBuild> builds = new List<AssetBundleBuild>();
//设置AssetBundle名,将多个资源构建在同一个AssetBundle内
builds.Add(new AssetBundleBuild() { assetBundleName = "Cube.unity3d", assetNames = new string[] { "Assets/Resources/Cube.prefab", "Assets/Resources/Cube2.prefab" } });
builds.Add(new AssetBundleBuild() { assetBundleName = "cubesurf.unity3d", assetNames = new string[] { "Assets/Resources/Cubesurf.mat" } });

BuildPipeline.BuildAssetBundles(outPath, builds.ToArray(),BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.StandaloneWindows);

AssetDatabase.Refresh();
}
}

压缩格式

AssetBundle 压缩 - Unity 手册 (unity3d.com)

AssetBundle提供了如下3种可选的压缩格式。

  • LZMA压缩:如果不做指定,AssetBundle默认会以这种方式压缩。它的优点是Bundle会被压缩得非常小,缺点是每次使用都需要解压,可能会带来卡顿。因此,不建议在项目中使用。
  • BuildAssetBundleOptions.UncompressedAssetBundle 不压缩:它的缺点是构建出来的AB包比较大,优点是加载得非常快。可以将不压缩的AssetBundle构建出来,用第三方压缩算法压缩它,再将它上传到CDN上。这样下载的时候再用第三方解压算法把AssetBundle写在硬盘上,这样读取的时候就会非常快了。
  • BuildAssetBundleOptions.ChunkBasedCompression LZ4压缩:使用逐块(chunk)的压缩方式,和LZMA相比它不是把整个AssetBundle压缩,可以边解压边加载,是一种折中方案,建议在项目中使用。

加载包体内的AssetBundle

本机使用 AssetBundle - Unity 手册 (unity3d.com)从手册中查看从网络或内存中读取AssetBundle的方式

包体内的AssetBundle只能放在StreamingAssets目录下,别的目录是无法读取的。可以使用AssetBundle.LoadFromFile()或者AssetBundle.LoadFromFileAsync()方法同步或者异步加载,加载AssetBundle之前,需要使用AssetBundleManifest提取每个AssetBundle的相互依赖关系。

新建LoadAssetBundleLocal脚本

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

public class LoadAssetBundleLocal : MonoBehaviour
{
void Start()
{
AssetBundle assetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "StreamingAssets"));
AssetBundleManifest assetBundleManifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
//加载AssetBundle前,需要加载依赖的Bundle
foreach (var item in assetBundleManifest.GetAllDependencies("cube.unity3d"))
{
AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, item));
}
assetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "cube.unity3d"));

GameObject prefab = assetBundle.LoadAsset<GameObject>("Cube");
GameObject.Instantiate<GameObject>(prefab);
assetBundle.Unload(false);//卸载
}
}

使用Path.Combine(Application.streamingAssetsPath, "StreamingAssets")可以帮助我们获得含有AssetBundleManifest的AssetBundle,使用AssetBundleManifest.GetAllDependencies(assetbundlename)来获得需要加载的AssetBundle的所有依赖AssetBundle,把这些依赖AssetBundle先全都加载在内存里,然后我们再创建目标AssetBundle时就不会有一些AssetBundle依赖丢失的问题。比如说这里如果不加载依赖的话,会生成一个紫色的没有贴图的小方块。

异步加载

新建LoadAssetBundleAsync脚本

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

public class LoadAssetBundleAsync : MonoBehaviour
{
private IEnumerator Start()
{
var assetBundleRequest = AssetBundle.LoadFromFileAsync(Path.Combine(Application.streamingAssetsPath, "StreamingAssets"));
yield return assetBundleRequest;
var assetBundle = assetBundleRequest.assetBundle;
var assetBundleManifestRequest = assetBundle.LoadAssetAsync<AssetBundleManifest>("AssetBundleManifest");
yield return assetBundleManifestRequest;
var assetBundleManifest = assetBundleManifestRequest.asset as AssetBundleManifest;
foreach (var item in assetBundleManifest.GetAllDependencies("cube.unity3d"))
{
var dependencyBundleRequest = AssetBundle.LoadFromFileAsync(Path.Combine(Application.streamingAssetsPath, item));
yield return dependencyBundleRequest;
}
var myAssetBundleRequest = AssetBundle.LoadFromFileAsync(Path.Combine(Application.streamingAssetsPath, "cube.unity3d"));
yield return myAssetBundleRequest;
var myAssetBundle = myAssetBundleRequest.assetBundle;
var myGameObjectRequest = myAssetBundle.LoadAssetAsync<GameObject>("Cube");
yield return myGameObjectRequest;
var myGameObject = myGameObjectRequest.asset as GameObject;
Instantiate<GameObject>(myGameObject);
assetBundle.Unload(false);
}
}

Web下载AssetBundle

Unity提供了UnityWebRequestAssetBundle来专用下载AssetBundle,此时只需要提供一个URL下载地址即可,如果下载本地任意资源,则在前面加上file://即可。使用下面代码将其下载在Application.persistentDataPath下。

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

public class DownloadAssetBundle : MonoBehaviour
{
private IEnumerator Start()
{
string path = "file://" + Path.Combine(Application.streamingAssetsPath, "cube.unity3d");

//下载AssetBundle
UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(path, 0);
yield return request.SendWebRequest();
string name = Path.GetFileName(path);//返回文件名和拓展名,如果不需要拓展名,使用GetFileNameWithoutExtension。比如说Resources.Load(Path)中的Path指向的文件是不能有拓展名的
string filePath = Path.Combine(Application.persistentDataPath, name);
if (File.Exists(filePath))
File.Delete(filePath);

//下载结束后写入本地,这里使用已过时的WWW方法
//DownloadHandler abdata = request.downloadHandler;使用DownloadHandlerAssetBundle方法,Unity会自动缓存
//File.WriteAllBytes(filePath, abdata.data);不能使用此方法加载AssetBundle
WWW www = new WWW(path);
File.WriteAllBytes(filePath, www.bytes);
Debug.Log(filePath);

//从硬盘中读取Bundle
AssetBundleCreateRequest assetBundleRequest = AssetBundle.LoadFromFileAsync(filePath);
yield return assetBundleRequest;
AssetBundle myLoadedAssetBundle = assetBundleRequest.assetBundle;
if (myLoadedAssetBundle == null)
yield break;

//从Bundle中读取资源
//var myGameObjectRequest = myLoadedAssetBundle.LoadAssetAsync<GameObject>("MyObject");
//yield return myGameObjectRequest;
//GameObject myGameObject = myGameObjectRequest.asset as GameObject;//异步加载时会显示null,原因未知
GameObject myGameObject = myLoadedAssetBundle.LoadAsset<GameObject>("Cube");

//实例化资源
if(myGameObject)
Instantiate<GameObject>(myGameObject);

myLoadedAssetBundle.Unload(false);
}
}

加载场景

场景构建AssetBundle的方法和资源类似。而且从Bundle中加载场景不需要提前在Scenes In Build中添加场景。

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

public class LoadAssetBundleScene : MonoBehaviour
{
void Start()
{
AssetBundle assetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "testscene.unity3d"));
}

private void OnGUI()
{
if(GUILayout.Button("<size=50>加载AB包场景</size>"))
{
SceneManager.LoadScene("SceneForAssetBundle");
}
}
}

卸载AssetBundle

关于AssetBundle的包管理,可以使用Addressables | Addressables | 1.19.19 (unity3d.com)Unity官方提供的包管理系统。

在同一个AssetBundle文件中,可以同时构建多个Assets。正确地从AssetBundle中实例化一个Prefab的步骤是:

  1. 从硬盘中加载AssetBundle;
  2. 从AssetBundle中加载需要的Asset;
  3. 从Asset中读取Object,将其Instantiate。

此时内存中有三个对象,AssetBundle对象、Asset对应的对象(比如GameObject,Texture)、Instantiate的对象

  • GameObject.Destroy()只能卸载Instantiate的对象
  • Resources.UnloadUnusedAssets()无法卸载Asset对应的对象,因为它被AssetBundle对象引用着
  • AssetBundle.UnloadAllAssetBundles(true)方法可以卸载所有Asset及其引用的AssetBundle。推荐在确保AssetBundle和Asset都没用了(比如关卡结束或加载屏幕期间)调用。
  • AssetBundle.Unload(true)。维护一个Asset的引用计数,当确保这个Asset被引用的所有GameObject都不再被使用时调用此函数
  • AssetBundle.Unload(false)消除场景和代码中对不需要的对象的所有引用。完成后,调用Resources.UnloadUnusedAssets来销毁残留的Asset。当以非附加的方式加载一个场景时,会自动销毁当前中的所有对象并自动调用Resources.UnloadUnusedAssets