0%

Unity AssetBundle实战

AssetBundle实战(AB打包)

前言

AssetBundle打包实战项目,主要是AssetBundle怎么打包的,怎么加载以及卸载的过场和原理,随便倒了个资源包进来,进行实操。

还有对相关理论问题的一些总结

项目地址: 传送门, 演示视频在DEMO文件夹下。

编译器打包工具开发

打出的ab资源在工程同级的AssetBundle下

可以无缝切换编辑器和AB加载方式

注意AB模式下,改完资源记得Tools/ResBuild一下

支持同步异步等方式

具体的打包配置在BuildSetting.xml里配置即可,具体精细到文件还是文件夹都可以。

主要工程代码在AssetBundleFramework下,core和editor

现在菜单里创建打包工具,方便使用,大概方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#region Build MenuItem

[MenuItem("Tools/ResBuild/Windows")]
public static void BuildWindows()
{
Build();
}

[MenuItem("Tools/ResBuild/Android")]
public static void BuildAndroid()
{
Build();
}

[MenuItem("Tools/ResBuild/iOS")]
public static void BuildIos()
{
Build();
}

#endregion

后面相似的创建就不复述了

core里创建一个文件,Profiler,作为按键等操作的log输出。项目需要的是规范的log输出,记录干了什么,抛出错误,记录时间等等。代码自查吧,算是一个前置工作

再就是打包的基本配置,不同平台需要不同的配置,配置是xml文件,切换配置的函数:

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
public static void SwitchPlatform()
{
string platform = PLATFORM;

switch (platform)
{
case "windows":
EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows64);
break;
case "android":
EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android);
break;
case "ios":
EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.iOS, BuildTarget.iOS);
break;
}
}

private static BuildSetting LoadSetting(string settingPath)
{
buildSetting = XmlUtility.Read<BuildSetting>(settingPath);
if (buildSetting == null)
{
throw new Exception($"Load buildSetting failed,SettingPath:{settingPath}.");
}
(buildSetting as ISupportInitialize)?.EndInit();

buildPath = Path.GetFullPath(buildSetting.buildRoot).Replace("\\", "/");
if (buildPath.Length > 0 && buildPath[buildPath.Length - 1] != '/')
{
buildPath += "/";
}
buildPath += $"{PLATFORM}/";

return buildSetting;
}

启动切换:

1
2
3
4
5
6
7
#if UNITY_IOS
private const string PLATFORM = "iOS";
#elif UNITY_ANDROID
private const string PLATFORM = "Android";
#else
private const string PLATFORM = "Windows";
#endif

建一个BuilderSetting类,里面是序列化,配置xml文件用的,函数有什么处理后缀啊,存储,忽略列表,打包选项等等,核心就是能在主函数里配置这些东西去调用,具体代码自己看

还有一个用来解析xml的类,上面那个是操作文件,这个是解析xml,包括了读取、保存和传出来的方法,文件名XmlUtility,自己看吧

EbundleType枚举文件类型,ab粒度类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum EBundleType
{
/// <summary>
/// 以文件作为ab名字(最小粒度)
/// </summary>
File,

/// <summary>
/// 以目录作为ab的名字
/// </summary>
Directory,

/// <summary>
/// 以最上的
/// </summary>
All
}

还有资源类型函数EResourceType,合集写在BuildItem里,还包括资源后缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum EResourceType
{
/// <summary>
/// 在打包设置中分析到的资源
/// </summary>
Direct = 1,

/// <summary>
/// 依赖资源
/// </summary>
Dependency = 2,

/// <summary>
/// 生成的文件
/// </summary>
Ganerate = 3,
}

打包流程,就是主函数下的Build,中间过程注释写的很清楚了

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
private static void Build()
{
ms_BuildProfiler.Start();

ms_SwitchPlatformProfiler.Start();
SwitchPlatform();
ms_SwitchPlatformProfiler.Stop();

ms_LoadBuildSettingProfiler.Start();
buildSetting = LoadSetting(BuildSettingPath);
ms_LoadBuildSettingProfiler.Stop();

//搜集bundle信息
ms_CollectProfiler.Start();
Dictionary<string, List<string>> bundleDic = Collect();
ms_CollectProfiler.Stop();

//打包assetbundle
ms_BuildBundleProfiler.Start();
BuildBundle(bundleDic);
ms_BuildBundleProfiler.Stop();

//清空多余文件
ms_ClearBundleProfiler.Start();
ClearAssetBundle(buildPath, bundleDic);
ms_ClearBundleProfiler.Stop();

//把描述文件打包bundle
ms_BuildManifestBundleProfiler.Start();
BuildManifest();
ms_BuildManifestBundleProfiler.Stop();

EditorUtility.ClearProgressBar();

ms_BuildProfiler.Stop();

Debug.Log($"打包完成{ms_BuildProfiler}");
}

,先收集信息,将文件信息收集进主函数的方法:

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
private static Dictionary<string, List<string>> Collect()
{
//获取所有在打包设置的文件列表
ms_CollectBuildSettingFileProfiler.Start();
HashSet<string> files = buildSetting.Collect();
ms_CollectBuildSettingFileProfiler.Stop();

//搜集所有文件的依赖关系
ms_CollectDependencyProfiler.Start();
Dictionary<string, List<string>> dependencyDic = CollectDependency(files);
ms_CollectDependencyProfiler.Stop();

//标记所有资源的信息
Dictionary<string, EResourceType> assetDic = new Dictionary<string, EResourceType>();

//被打包配置分析到的直接设置为Direct
foreach (string url in files)
{
assetDic.Add(url, EResourceType.Direct);
}

//依赖的资源标记为Dependency,已经存在的说明是Direct的资源
foreach (string url in dependencyDic.Keys)
{
if (!assetDic.ContainsKey(url))
{
assetDic.Add(url, EResourceType.Dependency);
}
}

//该字典保存bundle对应的资源集合
ms_CollectBundleProfiler.Start();
Dictionary<string, List<string>> bundleDic = CollectBundle(buildSetting, assetDic, dependencyDic);
ms_CollectBundleProfiler.Stop();

//生成Manifest文件
ms_GenerateManifestProfiler.Start();
GenerateManifest(assetDic, bundleDic, dependencyDic);
ms_GenerateManifestProfiler.Stop();

return bundleDic;
}

里面调用的两个函数的功能为收集指定文件集合所有的依赖信息和搜集bundle对应的ab名字,自己看函数吧

Collect最后还需要一个生成Manifest的函数,写在下面了,主要三步,生成资源描述信息,生成bundle描述信息和生成资源依赖描述信息,代码太长,不贴了

信息收集完以后,打包assetbundle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static AssetBundleManifest BuildBundle(Dictionary<string, List<string>> bundleDic)
{
float min = ms_BuildBundleProgress.x;
float max = ms_BuildBundleProgress.y;

EditorUtility.DisplayProgressBar($"{nameof(BuildBundle)}", "打包AssetBundle", min);

if (!Directory.Exists(buildPath))
Directory.CreateDirectory(buildPath);

AssetBundleManifest manifest = BuildPipeline.BuildAssetBundles(buildPath, GetBuilds(bundleDic), BuildAssetBundleOptions, EditorUserBuildSettings.activeBuildTarget);

EditorUtility.DisplayProgressBar($"{nameof(BuildBundle)}", "打包AssetBundle", max);

return manifest;
}

和获取所有需要打包的AssetBundleBuild

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static AssetBundleBuild[] GetBuilds(Dictionary<string, List<string>> bundleTable)
{
int index = 0;
AssetBundleBuild[] assetBundleBuilds = new AssetBundleBuild[bundleTable.Count];
foreach (KeyValuePair<string, List<string>> pair in bundleTable)
{
assetBundleBuilds[index++] = new AssetBundleBuild()
{
assetBundleName = pair.Key,
assetNames = pair.Value.ToArray(),
};
}

return assetBundleBuilds;
}

清理多余的AssetBundle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void ClearAssetBundle(string path, Dictionary<string, List<string>> bundleDic)
{
float min = ms_ClearBundleProgress.x;
float max = ms_ClearBundleProgress.y;

EditorUtility.DisplayProgressBar($"{nameof(ClearAssetBundle)}", "清除多余的AssetBundle文件", min);

List<string> fileList = GetFiles(path, null, null);
HashSet<string> fileSet = new HashSet<string>(fileList);

foreach (string bundle in bundleDic.Keys)
{
fileSet.Remove($"{path}{bundle}");
fileSet.Remove($"{path}{bundle}{BUNDLE_MANIFEST_SUFFIX}");
}

fileSet.Remove($"{path}{PLATFORM}");
fileSet.Remove($"{path}{PLATFORM}{BUNDLE_MANIFEST_SUFFIX}");

Parallel.ForEach(fileSet, ParallelOptions, File.Delete);

EditorUtility.DisplayProgressBar($"{nameof(ClearAssetBundle)}", "清除多余的AssetBundle文件", max);
}

最后,把描述文件打包bundle

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
private static void BuildManifest()
{
float min = ms_BuildManifestProgress.x;
float max = ms_BuildManifestProgress.y;

EditorUtility.DisplayProgressBar($"{nameof(BuildManifest)}", "将Manifest打包成AssetBundle", min);

if (!Directory.Exists(TempBuildPath))
Directory.CreateDirectory(TempBuildPath);

string prefix = Application.dataPath.Replace("/Assets", "/").Replace("\\", "/");

AssetBundleBuild manifest = new AssetBundleBuild();
manifest.assetBundleName = $"{MANIFEST}{BUNDLE_SUFFIX}";
manifest.assetNames = new string[3]
{
ResourcePath_Binary.Replace(prefix,""),
BundlePath_Binary.Replace(prefix,""),
DependencyPath_Binary.Replace(prefix,""),
};

EditorUtility.DisplayProgressBar($"{nameof(BuildManifest)}", "将Manifest打包成AssetBundle", min + (max - min) * 0.5f);

AssetBundleManifest assetBundleManifest = BuildPipeline.BuildAssetBundles(TempBuildPath, new AssetBundleBuild[] { manifest }, BuildAssetBundleOptions, EditorUserBuildSettings.activeBuildTarget);

//把文件copy到build目录
if (assetBundleManifest)
{
string manifestFile = $"{TempBuildPath}/{MANIFEST}{BUNDLE_SUFFIX}";
string target = $"{buildPath}/{MANIFEST}{BUNDLE_SUFFIX}";
if (File.Exists(manifestFile))
{
File.Copy(manifestFile, target);
}
}

//删除临时目录
if (Directory.Exists(TempBuildPath))
Directory.Delete(TempBuildPath, true);

EditorUtility.DisplayProgressBar($"{nameof(BuildManifest)}", "将Manifest打包成AssetBundle", max);
}

把buildsetting里的获取BundleName的函数补一下,现在就已经能跑了,会在项目外面生成几个.ab的文件,那些就是打包的文件。

接着要考虑怎么加载的问题,写在Demo下的Test_Callback里

主要是这个函数,剩下都是找文件名和拼接字符串的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void Initialize()
{
ResourceManager.instance.LoadWithCallback("Assets/AssetBundle/UI/UIRoot.prefab", true, uiRootResource =>
{
uiRootResource.Instantiate();

Transform uiParent = GameObject.Find("Canvas").transform;

ResourceManager.instance.LoadWithCallback("Assets/AssetBundle/UI/TestUI.prefab", true, testUIResource =>
{
testUIResource.Instantiate(uiParent, false);
});
});
}

要达成加载的目的,也就引出了下一个函数,就是我们需要一个ResourceManager来管理这些加载项,在Core里的Resource下,里面有ResourceManager

代码太长不贴了,大概就是读取资源信息,读取bundle信息,读取资源依赖信息,加载这些信息,和最后释放这些信息

bundle加载,在Bundle文件夹下,同步异步加载资源,Demo里有个Test_Callback,会调用加载

在lateupdate里写卸载函数时要注意,不断轮询确保及时卸载不用的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
public void LateUpdate()
{
if (m_NeedUnloadList.Count == 0)
return;

while (m_NeedUnloadList.Count > 0)
{
ABundle bundle = m_NeedUnloadList.First.Value;
m_NeedUnloadList.RemoveFirst();
if (bundle == null)
continue;

m_BundleDic.Remove(bundle.url);

if (!bundle.done && bundle is BundleAsync)
{
BundleAsync bundleAsync = bundle as BundleAsync;
if (m_AsyncList.Contains(bundleAsync))
m_AsyncList.Remove(bundleAsync);
}

bundle.UnLoad();

//依赖引用-1
if (bundle.dependencies != null)
{
for (int i = 0; i < bundle.dependencies.Length; i++)
{
ABundle temp = bundle.dependencies[i];
UnLoad(temp);
}
}
}
}

同样,在加载的时候,要确保打包没有循环依赖即可

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
private ABundle LoadInternal(string url, bool async)
{
ABundle bundle;
if (m_BundleDic.TryGetValue(url, out bundle))
{
if (bundle.reference == 0)
{
m_NeedUnloadList.Remove(bundle);
}

//从缓存中取并引用+1
bundle.AddReference();

return bundle;
}

//创建ab
if (async)
{
bundle = new BundleAsync();
bundle.url = url;
m_AsyncList.Add(bundle as ABundleAsync);
}
else
{
bundle = new Bundle();
bundle.url = url;
}

m_BundleDic.Add(url, bundle);

//加载依赖
string[] dependencies = m_AssetBundleManifest.GetDirectDependencies(url);
if (dependencies.Length > 0)
{
bundle.dependencies = new ABundle[dependencies.Length];
for (int i = 0; i < dependencies.Length; i++)
{
string dependencyUrl = dependencies[i];
ABundle dependencyBundle = LoadInternal(dependencyUrl, async);
bundle.dependencies[i] = dependencyBundle;
}
}

bundle.AddReference();

bundle.Load();

return bundle;
}