# 1. 收集 BuildSetting 中配置的所有新路径

1
2
3
4
// 1️⃣ 收集所有在 BuildSetting 中配置的资源路径  
ms_CollectBuildSettingFileProfiler.Start();
HashSet<string> files = buildSetting.Collect();
ms_CollectBuildSettingFileProfiler.Stop();

# 2. 收集每个文件的依赖关系

1
2
3
4
// 2️⃣ 收集每个文件的依赖关系(.mat、.png、.anim等)  
ms_CollectDependencyProfiler.Start();
Dictionary<string, List<string>> dependencyDic = CollectDependency(files);
ms_CollectDependencyProfiler.Stop();

这里来讲一讲 CollectDependency 这个函数

# 2.1 管理进度条

1
2
float min = ms_GetDependencyProgress.x;  
float max = ms_GetDependencyProgress.y;

通过 ms_GetDependencyProgress 来管理进度条的进度

# 2.2 记录依赖关系并转化为列表,方便处理和拓展

1
2
3
4
5
// 记录依赖关系: key = 文件路径,value = 它依赖的文件列表  
Dictionary<string, List<string>> dependencyDic = new Dictionary<string, List<string>>(files.Count);

// 将输入集合转成 List 方便动态扩展
List<string> fileList = new List<string>(files);

# 2.3 获取依赖

既然已经获取了文件列表,下一步就是对文件列表进行遍历来得到各个文件依赖的函数了

# 2.3.1 更新进度条

1
2
3
4
5
6
// 每隔 10 个文件更新一次进度条  
if (i % 10 == 0)
{
float progress = min + (max - min) * ((float)i / (fileList.Count * 3));
EditorUtility.DisplayProgressBar($"{nameof(CollectDependency)}", "正在收集资源依赖关系...", progress);
}

# 2.3.2 通过 Unity 的 API 获取依赖并过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用 Unity 的 API 获取依赖文件  
string[] dependencies = AssetDatabase.GetDependencies(assetUrl, true);
List<string> dependencyList = new List<string>(dependencies.Length);

// 遍历所有依赖文件,过滤掉脚本等无关文件
for (int j = 0; j < dependencies.Length; j++)
{
string tempAssetUrl = dependencies[j];
string extension = Path.GetExtension(tempAssetUrl).ToLower();

// 忽略没有后缀或 .cs / .dll 文件
if (string.IsNullOrEmpty(extension) || extension == ".cs" || extension == ".dll")
continue;

dependencyList.Add(tempAssetUrl);

// 如果依赖文件之前未记录,则加入列表以便继续处理
if (!fileList.Contains(tempAssetUrl))
fileList.Add(tempAssetUrl);
}
//最后把这个依赖列表通过字典对应url路径
dependencyDic.Add(assetUrl, dependencyList);

# 3. 构建一个资源 → 类型映射表(Direct 或 Dependency)

1
2
3
4
5
6
7
8
9
10
11
12
Dictionary<string, EResourceType> assetDic = new Dictionary<string, EResourceType>();  
foreach (string url in files)
{
assetDic.Add(url, EResourceType.Direct); // 配置中直接列出的文件为 Direct
}

foreach (string url in dependencyDic.Keys)
{
if (!assetDic.ContainsKey(url))
{ assetDic.Add(url, EResourceType.Dependency); // 未直接配置但被依赖的文件为 Dependency
}
}

# 4. 保存 Bundle 保存的资源

1
2
3
4
//此字典保存的是Bundle保存的资源类型  
ms_CollectBundleProfiler.Start();
Dictionary<string,List<string>> bundleDic = CollectBundle(buildSetting, assetDic, dependencyDic);
ms_CollectBundleProfiler.Stop();

这里再来讲一讲 CollectBundle 这个函数

# 4.1 同样先记录进度条

1
2
3
4
5
6
7
8
9
float min = ms_CollectBundleInfoProgress.x;  
float max = ms_CollectBundleInfoProgress.y;

// 显示进度条(初始阶段)
EditorUtility.DisplayProgressBar(
$"{nameof(CollectBundle)}",
"正在收集资源Bundle信息...",
min
);

# 4.2 然后,根据 setting 的打包规则,对 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
foreach (KeyValuePair<string, EResourceType> pair in assetDic)  
{
index++;

string assetUrl = pair.Key; // 资源路径
EResourceType type = pair.Value; // 资源类型(Direct 或 Reference)

// 根据 BuildSetting 规则获取该资源对应的 Bundle 名称
// 这个函数内部通常会根据路径、文件后缀、配置规则等决定打进哪个包
string bundleName = setting.GetBundleName(assetUrl, type);

// 如果没有匹配到打包规则,则加入未命中列表
if (bundleName == null)
{ notInRuleList.Add(assetUrl);
continue;
}
// 获取该 bundle 下的资源列表
List<string> list;
if (!bundleDic.TryGetValue(bundleName, out list))
{ // 如果该 bundle 还没创建,则新建一个列表
list = new List<string>();
bundleDic.Add(bundleName, list);
}
// 把当前资源路径添加到对应的 bundle 列表中
list.Add(assetUrl);

// 每隔一部分资源更新一次进度条,避免进度条卡顿
EditorUtility.DisplayProgressBar(
$"{nameof(CollectBundle)}",
"正在收集资源Bundle信息...",
min + (max - min) * ((float)index / assetDic.Count)
);}

# 4.3 如果存在不在打包规则中的文件,报错并返回列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 如果存在未命中规则的资源,报错并停止打包  
if (notInRuleList.Count > 0)
{
string message = string.Empty;

// 拼接所有未命中的资源路径,方便报错查看
for (int i = 0; i < notInRuleList.Count; i++)
{ message += '\n' + notInRuleList[i];
}
// 清除进度条
EditorUtility.ClearProgressBar();

// 抛出异常提示,阻止继续执行
throw new Exception($"以下资源未命中任何打包规则:{message}");
}

# 5. 生成 manifest 文件

1
2
3
4
//生成Manifest文件  
ms_GenerateManifestProfiler.Start();
GenerateManifestFile(assetDic,bundleDic,dependencyDic);
ms_GenerateManifestProfiler.Stop();

Manifest 文件就像是打包的一个总帐本。因为打包之后的 AssetBundle 文件都是二进制的,无法直接阅读,所以我们要依靠 Manifest 文件来知道,我们想要某个 prefab 的话,需要加载哪个文件。

# 5.1 同样是,先加载进度条并生成临时文件夹

1
2
3
4
5
6
7
8
9
10
float min = ms_GenerateBuildInfoProgress.x;  
float max = ms_GenerateBuildInfoProgress.y;
EditorUtility.DisplayProgressBar($"{nameof(GenerateManifestFile)}", "正在生成打包信息...", min);
//生成临时存放文件的目录
if (!Directory.Exists(TempPath))
{
Directory.CreateDirectory(TempPath);
}
//资源映射id
Dictionary<string,ushort> assetIdDic = new Dictionary<string,ushort>();

# 5.2 生成资源描述信息

三个描述信息的 region 其实逻辑大体上都差不多,其实就是生成一个文本文件和一个二进制文件。

# 5.2.1

首先就是检查本地是否存在文件,如果存在,那就覆盖掉

1
2
3
4
5
6
7
8
9
10
11
//删除资源描述文本文件  
if(File.Exists(ResourcePath_Text))
File.Delete(ResourcePath_Text);
//删除资源描述二进制文件
if(File.Exists(ResourcePath_Binary))
File.Delete(ResourcePath_Binary);

//写入资源列表
StringBuilder resourceSb = new StringBuilder();
MemoryStream resourceMs = new MemoryStream();
BinaryWriter resourceBw = new BinaryWriter(File.Open(ResourcePath_Binary, FileMode.Create));

# 5.2.2 写入文件

然后,遍历 assetDic,就是存储着资源路径和类型的字典,将内容存入两个文件

1
2
3
4
5
6
7
8
9
10
11
//写入个数  
resourceBw.Write((ushort)assetDic.Count);
List<string> keys = new List<string>(assetDic.Keys);
keys.Sort();
for (ushort i = 0; i < keys.Count; i++)
{
string assetUrl = keys[i];
assetIdDic.Add(assetUrl,i);
resourceSb.AppendLine($"{i}\t{assetUrl}");
resourceBw.Write(assetUrl);
}

# 5.2.3 最后,清除临时区域并记录

1
2
3
4
5
6
resourceMs.Flush();  //把内存中的数据刷新
byte[] buffer = resourceMs.GetBuffer(); //拿到二进制数据
resourceBw.Close(); //关闭
//写入资源描述文件
File.WriteAllText(ResourcePath_Text, resourceSb.ToString(),Encoding.UTF8);
File.WriteAllBytes(ResourcePath_Binary, buffer);