2、Unity资源打包基础:AssetBundle概念、AssetBundle的构建与加载、依赖管理
好,咱们进入正题。这一章聊的是Unity资源打包的根基——AssetBundle。说实话,我刚开始接触这玩意儿的时候,也觉得不就是打个包嘛,有啥难的?结果第一次上线就出了大问题,资源加载卡死、依赖丢失、包体冗余……嗯,从那以后我再也不敢小看它了。
2.1 什么是AssetBundle?
AssetBundle,说白了就是一个压缩包。它能把模型、贴图、音频、预制体等资源打包成一个文件,放在服务器上,让游戏运行时按需下载。你想想看,如果所有资源都塞进安装包,那包体得有多大?用户下载一次就够呛了。
我个人习惯把AssetBundle理解成「资源的快递箱」。你把资源分类装好,贴上标签(名字),用户需要哪个就拆哪个箱子。这样既节省了安装包体积,又能实现热更新——换贴图、改模型,都不用重新发版本。
核心要点:AssetBundle不是用来「存资源」的,而是用来「按需加载资源」的。这个思维转变很重要。
2.2 AssetBundle的构建
构建AssetBundle,说白了就是告诉Unity:「嘿,把这些资源打包成.assetbundle文件」。Unity提供了两种方式:编辑器手动操作和脚本自动化构建。
2.2.1 手动设置资源标签
在Inspector面板里,你可以给每个资源指定一个AssetBundle标签。比如把「角色模型」标为characters/hero,把「UI贴图」标为ui/icons。注意,标签名决定了打包后的文件名和目录结构。
我的经验:标签命名最好用「分类/子分类」的格式,比如scenes/level1。这样打包后文件结构清晰,维护起来不头疼。我曾经见过有人把所有资源都标成all,结果一个包几百兆……那场面,惨不忍睹。
2.2.2 脚本构建
手动打包太慢了,项目大了根本没法搞。我建议用脚本自动化。下面是一个最基础的构建脚本:
using UnityEditor;
using System.IO;
public class BuildAssetBundles
{
[MenuItem("Tools/Build AssetBundles")]
static void BuildAll()
{
string outputPath = "Assets/StreamingAssets/AssetBundles";
if (!Directory.Exists(outputPath))
Directory.CreateDirectory(outputPath);
BuildPipeline.BuildAssetBundles(outputPath,
BuildAssetBundleOptions.None,
BuildTarget.StandaloneWindows64);
Debug.Log("打包完成!路径:" + outputPath);
}
}
这段代码干了三件事:创建输出目录、调用打包API、打印日志。你把它放到Editor文件夹下,点击菜单栏的「Tools → Build AssetBundles」就能跑。
注意:打包目标平台一定要选对。你给Windows打的包,Android是读不了的。我有个同事就犯过这错,上线前才发现包体不对,连夜重打……
2.3 AssetBundle的加载
打包只是第一步,怎么加载才是关键。Unity提供了四种加载方式,我挑最常用的两种讲。
2.3.1 从本地加载
如果AssetBundle放在StreamingAssets目录下,可以直接用AssetBundle.LoadFromFile加载。这是最快的方式,因为它用了内存映射,不会把整个文件读到内存里。
IEnumerator LoadFromLocal()
{
string path = Application.streamingAssetsPath + "/AssetBundles/characters/hero";
AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(path);
yield return request;
AssetBundle bundle = request.assetBundle;
GameObject hero = bundle.LoadAsset<GameObject>("HeroPrefab");
Instantiate(hero);
}
注意我用的是异步加载LoadFromFileAsync。为什么?因为同步加载会卡主线程,玩家操作会顿一下。你想想看,打Boss的时候突然卡半秒,那体验得多糟糕。
2.3.2 从网络加载
热更新场景下,资源得从服务器下载。这时候用UnityWebRequestAssetBundle:
IEnumerator LoadFromWeb(string url)
{
UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(url);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
// 加载资源...
}
else
{
Debug.LogError("下载失败:" + request.error);
}
}
避坑指南:我曾经遇到过一个问题——下载完的AssetBundle加载后,资源引用全断了。后来发现是没等下载完成就读取了。记住,一定要等SendWebRequest协程跑完再操作。
2.4 依赖管理
这是AssetBundle最坑的地方,没有之一。你想想看,一个角色模型引用了贴图,贴图又引用了材质。如果它们被打包到了不同的AssetBundle里,加载角色时贴图没加载,那角色就是紫色的——嗯,俗称「紫皮怪」。
2.4.1 依赖是怎么产生的?
Unity在打包时会自动分析资源之间的引用关系。比如预制体A引用了材质B,材质B引用了贴图C。如果A、B、C分别被打包到三个不同的AssetBundle里,那它们之间就产生了依赖。
说白了,依赖就是「我中有你,你中有他」。加载A之前,必须先加载B和C。
2.4.2 如何管理依赖?
Unity提供了一个关键API——AssetBundleManifest。它记录了所有AssetBundle之间的依赖关系。加载资源前,先查一下这个清单,把依赖的包都加载了。
void LoadWithDependencies(string bundleName, string assetName)
{
// 先加载主清单
AssetBundle manifestBundle = AssetBundle.LoadFromFile(
Application.streamingAssetsPath + "/AssetBundles/AssetBundles");
AssetBundleManifest manifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
// 获取依赖列表
string[] dependencies = manifest.GetAllDependencies(bundleName);
foreach (string dep in dependencies)
{
AssetBundle.LoadFromFile(
Application.streamingAssetsPath + "/AssetBundles/" + dep);
}
// 再加载目标包
AssetBundle bundle = AssetBundle.LoadFromFile(
Application.streamingAssetsPath + "/AssetBundles/" + bundleName);
Object obj = bundle.LoadAsset(assetName);
}
关键点:依赖包的加载顺序不重要,但一定要在加载目标包之前全部加载完。Unity内部会维护一个引用计数,重复加载同一个包不会造成内存翻倍。
2.4.3 依赖管理的常见坑
- 循环依赖:两个AssetBundle互相引用。Unity打包时会报错,但有时候是隐式的——比如A引用了B的材质,B又引用了A的模型。解决办法是把公共资源抽出来单独打包。
- 冗余依赖:同一个资源被多个包引用,结果每个包里都复制了一份。我见过一个项目,一个1MB的贴图被复制了10次,包体直接多了9MB。解决方案是用
BuildAssetBundleOptions.DeterministicAssetBundle选项,让Unity自动去重。 - 卸载时机:千万别在资源还在使用时卸载它的依赖包。我曾经手抖写了个
Unload(true),结果场景里所有模型都变白了——材质被卸载了。正确的做法是用引用计数,或者等场景切换时统一卸载。
2.5 总结
AssetBundle这东西,说简单也简单,说复杂也复杂。核心就三件事:打包、加载、管依赖。但每一件都有不少细节。我个人建议,刚开始做项目时,先把依赖管理搞清楚,不然上线后出问题,排查起来真要命。
下一章我们会聊更高级的打包策略——比如按场景分包、按功能分包、以及增量更新。嗯,到时候再细说。