【Unity】一文搞懂Unity AssetBundle打包场景、资源与动态加载流程
引言
AssetBundle 是 Unity 提供的资源打包与动态加载机制,允许开发者将场景、模型、纹理、音频等资源打包成独立文件,实现增量更新、按需加载和内存优化。相比 Resources 文件夹,AssetBundle 支持运行时下载、版本控制和卸载,特别适用于手游(如王者荣耀的英雄皮肤更新)和大型 PC 游戏。
核心流程:打包 → 上传 → 下载 → 加载 → 卸载。支持 Unity 2018.4+,推荐 Unity 2022.3 LTS。本文详解完整工作流,包括 BuildPipeline、AssetBundleManifest、异步加载和内存管理。时间复杂度:打包 O(n),加载 O(log n)(依赖文件大小)。
AssetBundle 核心概念
1. 资源依赖关系
- 直接依赖:A 资源引用 B 纹理,加载 A 时自动加载 B。
- 间接依赖:多级引用链,Unity 自动解析。
- Manifest 文件:记录所有 Bundle 间的依赖关系,加载时确保完整性。
2. 打包模式
模式 | 描述 | 适用场景 |
---|---|---|
Resources | 编译到 APK,运行时 Resources.Load | 小型项目,快速原型 |
AssetBundle | 独立文件,可下载 | 大型游戏,DLC 更新 |
Addressables | Unity 官方新系统,封装 AssetBundle | 推荐现代项目 |
3. 命名约定
- Bundle 名:
characters_hero1_v1.0
,版本化命名。 - 资源路径:相对 Bundle 内部路径,如
textures/sword_diffuse.png
。 - 标签系统:按类型分组,如
ui/prefabs/button.unity3d
。
打包流程详解
1. 资源准备与分组
- 创建文件夹结构:
Assets/
├── AssetBundles/
│ ├── Characters/
│ │ ├── HeroA.prefab
│ │ └── HeroB.prefab
│ ├── Environments/
│ │ ├── Level1.unity
│ │ └── Level2.unity
│ └── UI/
│ └── MainMenu.prefab
└── Editor/
└── BuildScript.cs
- 标记资源:选中资源 → Inspector → AssetBundle 下拉 → 选择或创建 Bundle 名。
2. 打包脚本实现
创建 Assets/Editor/BuildAssetBundles.cs
:
using UnityEngine;
using UnityEditor;
using System.IO;
public class BuildAssetBundles
{
[MenuItem("AssetBundle/Build All Bundles")]
public static void BuildAllAssetBundles()
{
string outputPath = Path.Combine(Application.streamingAssetsPath, "AssetBundles");
if (!Directory.Exists(outputPath))
Directory.CreateDirectory(outputPath);
// 构建选项:压缩、增量构建
BuildAssetBundleOptions options = BuildAssetBundleOptions.None |
BuildAssetBundleOptions.ChunkBasedCompression |
BuildAssetBundleOptions.StrictMode |
BuildAssetBundleOptions.DeterministicAssetBundle;
BuildPipeline.BuildAssetBundles(outputPath, options, EditorUserBuildSettings.activeBuildTarget);
// 生成 Manifest 文件
AssetBundleManifest manifest = BuildPipeline.BuildAssetBundleManifest(outputPath, null, options);
Debug.Log($"AssetBundles built to: {outputPath}");
Debug.Log($"Manifest contains {manifest.GetAllAssetBundles().Length} bundles");
AssetDatabase.Refresh();
}
[MenuItem("AssetBundle/Build Scene Bundles")]
public static void BuildSceneBundles()
{
// 场景打包(独立模式)
string[] scenePaths = new[] { "Assets/Scenes/Level1.unity", "Assets/Scenes/Level2.unity" };
BuildPipeline.BuildPlayer(scenePaths, "Builds/SceneBundle",
BuildTarget.StandaloneWindows64,
BuildOptions.BuildAdditionalStreamedScenes);
}
}
关键参数:
- ChunkBasedCompression:LZ4 压缩,加载快(推荐手游)。
- StrictMode:严格依赖检查,防止遗漏。
- DeterministicAssetBundle:相同输入生成相同 Bundle(CI/CD 友好)。
3. 场景打包特殊处理
场景文件 .unity
打包为 AssetBundle:
// 打包场景 Bundle
EditorBuildSettingsScene[] scenes = EditorBuildSettings.scenes;
string[] scenePaths = new string[scenes.Length];
for (int i = 0; i < scenes.Length; i++)
scenePaths[i] = scenes[i].path;
BuildPipeline.BuildAssetBundles(outputPath, BuildAssetBundleOptions.ChunkBasedCompression,
BuildTarget.Android); // 指定目标平台
注意:场景 Bundle 加载使用 SceneManager.LoadSceneAsync
+ AllowSceneActivation
。
依赖管理与 Manifest
4. AssetBundleManifest 使用
// 运行时加载 Manifest
AssetBundle manifestBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "AssetBundles"));
AssetBundleManifest manifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] allBundles = manifest.GetAllAssetBundles();
string[] dependencies = manifest.GetAllDependencies("characters_hero1");
// 按依赖顺序加载
foreach (string dep in dependencies)
{
LoadAssetBundle(dep, false); // 不加载 Manifest 依赖
}
manifestBundle.Unload(true);
5. 自动依赖解析
public class BundleDependencyManager
{
private AssetBundleManifest manifest;
private Dictionary<string, AssetBundle> loadedBundles = new Dictionary<string, AssetBundle>();
public void LoadBundleWithDependencies(string bundleName)
{
string[] dependencies = manifest.GetAllDependencies(bundleName);
// 先加载依赖
foreach (string dep in dependencies)
{
if (!loadedBundles.ContainsKey(dep))
LoadAssetBundle(dep, false);
}
// 加载主 Bundle
LoadAssetBundle(bundleName, true);
}
private void LoadAssetBundle(string bundleName, bool isMain)
{
string path = Path.Combine(Application.persistentDataPath, bundleName);
AssetBundle bundle = AssetBundle.LoadFromFile(path);
if (bundle != null)
{
loadedBundles[bundleName] = bundle;
Debug.Log($"Loaded bundle: {bundleName}");
}
}
}
动态加载流程
6. 异步加载实现
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;
public class AssetBundleLoader : MonoBehaviour
{
public string bundleName = "characters_hero1";
public string assetName = "HeroA";
void Start()
{
StartCoroutine(LoadAssetBundleAsync());
}
IEnumerator LoadAssetBundleAsync()
{
// 1. 下载 Bundle(网络情况)
string url = "https://example.com/bundles/" + bundleName;
using (WWW www = new WWW(url))
{
yield return www;
if (www.error != null)
{
Debug.LogError("Download failed: " + www.error);
yield break;
}
AssetBundle bundle = www.assetBundle;
if (bundle == null)
{
Debug.LogError("Bundle is null");
yield break;
}
// 2. 加载资源
AssetBundleRequest request = bundle.LoadAssetAsync<GameObject>(assetName);
yield return request;
GameObject asset = request.asset as GameObject;
if (asset != null)
{
Instantiate(asset);
}
// 3. 卸载 Bundle(保留内存)
bundle.Unload(false); // false = 保留已加载资源
}
}
// 现代 API:UnityWebRequest
IEnumerator LoadWithUnityWebRequest()
{
string bundlePath = Path.Combine(Application.streamingAssetsPath, bundleName);
UnityWebRequest request = UnityWebRequestAssetBundle.GetAssetBundle(bundlePath);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
// 加载逻辑同上
bundle.Unload(false);
}
}
}
7. 场景异步加载
IEnumerator LoadSceneAsync(string sceneBundleName, string sceneName)
{
// 加载场景 Bundle
AssetBundle sceneBundle = AssetBundle.LoadFromFile(GetBundlePath(sceneBundleName));
AssetBundleRequest sceneRequest = sceneBundle.LoadAssetAsync<Scene>(sceneName);
yield return sceneRequest;
// 激活场景
Scene scene = sceneRequest.asset as Scene;
SceneManager.LoadSceneAsync(scene.path);
sceneBundle.Unload(true); // 场景加载后可完全卸载
}
内存管理与卸载
8. 资源引用计数
public class AssetBundleCache : MonoBehaviour
{
private Dictionary<string, AssetBundle> bundleCache = new Dictionary<string, AssetBundle>();
private Dictionary<Object, string> assetToBundleMap = new Dictionary<Object, string>();
public T LoadAsset<T>(string bundleName, string assetName) where T : Object
{
if (!bundleCache.ContainsKey(bundleName))
bundleCache[bundleName] = LoadBundle(bundleName);
T asset = bundleCache[bundleName].LoadAsset<T>(assetName);
if (asset != null)
assetToBundleMap[asset] = bundleName;
return asset;
}
public void UnloadAsset(Object asset)
{
if (assetToBundleMap.TryGetValue(asset, out string bundleName))
{
// 减少引用计数逻辑
if (CanUnloadBundle(bundleName))
{
bundleCache[bundleName].Unload(true);
bundleCache.Remove(bundleName);
}
assetToBundleMap.Remove(asset);
}
}
private bool CanUnloadBundle(string bundleName)
{
// 检查 Bundle 内剩余资源引用
return bundleCache[bundleName].GetAllAssetNames().Length == 0;
}
}
9. 垃圾回收触发
public static void ForceGarbageCollection()
{
// 手动触发 GC,谨慎使用
Resources.UnloadUnusedAssets();
System.GC.Collect();
Debug.Log("Garbage collection completed");
}
更新与版本控制
10. 增量更新系统
public class BundleUpdater : MonoBehaviour
{
private AssetBundleManifest currentManifest;
private string remoteManifestURL = "https://example.com/manifest";
public void CheckForUpdates()
{
StartCoroutine(DownloadAndCompareManifest());
}
IEnumerator DownloadAndCompareManifest()
{
UnityWebRequest manifestRequest = UnityWebRequest.Get(remoteManifestURL);
yield return manifestRequest.SendWebRequest();
if (manifestRequest.result == UnityWebRequest.Result.Success)
{
// 解析远程 Manifest,比较版本
AssetBundle remoteManifestBundle = DownloadHandlerAssetBundle.GetContent(manifestRequest);
AssetBundleManifest remoteManifest = remoteManifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
// 比较依赖和哈希值
string[] outdatedBundles = GetOutdatedBundles(remoteManifest);
foreach (string bundle in outdatedBundles)
{
DownloadBundle(bundle);
UnloadOldBundle(bundle);
}
remoteManifestBundle.Unload(true);
}
}
}
最佳实践与优化
11. 打包优化
- 资源合并:相同纹理合并为 Atlas,减少 Bundle 数量。
- 压缩选择:LZ4(快速加载)vs LZMA(小文件大小)。
- 依赖最小化:避免跨 Bundle 引用,使用共享 Bundle(如 CommonAssets)。
12. 加载优化
- 预加载:游戏启动时预加载常用资源。
- 缓存策略:LRU 缓存,优先卸载不常用 Bundle。
- 异步优先:所有加载操作使用协程,避免卡顿。
13. 错误处理
public class BundleLoadException : System.Exception
{
public BundleLoadException(string bundleName, string error)
: base($"Failed to load bundle {bundleName}: {error}") { }
}
// 使用 try-catch 包装加载
try
{
AssetBundle bundle = AssetBundle.LoadFromFile(path);
if (bundle == null) throw new BundleLoadException(bundleName, "Bundle is null");
}
catch (BundleLoadException e)
{
Debug.LogError(e.Message);
// 降级方案:使用默认资源
}
Addressables 现代替代方案
Unity 推荐 Addressables 系统(封装 AssetBundle):
// Addressables 使用示例
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class AddressableLoader : MonoBehaviour
{
public string addressableKey = "HeroA";
void Start()
{
Addressables.LoadAssetAsync<GameObject>(addressableKey)
.Completed += OnAssetLoaded;
}
void OnAssetLoaded(AsyncOperationHandle<GameObject> handle)
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
Instantiate(handle.Result);
// 自动管理依赖和卸载
}
}
}
Addressables 优势:
- 自动依赖管理,无需手动 Manifest。
- 云构建集成,CI/CD 友好。
- 运行时资源重定向,支持 A/B 测试。
完整工作流示例
// 1. 打包阶段(Editor)
BuildAssetBundles.BuildAllAssetBundles();
// 2. 运行时加载
public class GameResourceManager : MonoBehaviour
{
void StartGame()
{
// 加载 Manifest
LoadManifest();
// 异步加载关卡
StartCoroutine(LoadLevelAsync("level1"));
// 动态加载角色
LoadCharacter("hero1");
}
// 3. 场景切换
void LoadNextLevel()
{
StartCoroutine(UnloadCurrentLevel());
StartCoroutine(LoadLevelAsync("level2"));
}
// 4. 退出清理
void OnApplicationQuit()
{
UnloadAllBundles();
Resources.UnloadUnusedAssets();
}
}
性能监控与调试
工具推荐
- Memory Profiler:监控 Bundle 内存占用。
- AssetBundle Browser:Unity Package Manager 插件,可视化依赖。
- Frame Debugger:检查加载时机对帧率影响。
关键指标
指标 | 目标值 | 优化方法 |
---|---|---|
加载时间 | < 2s | 异步加载 + LZ4 压缩 |
内存峰值 | < 500MB | 及时卸载 + 引用计数 |
Bundle 大小 | < 50MB | 资源合并 + 压缩 |
常见问题解决
问题 | 原因 | 解决方案 |
---|---|---|
依赖加载失败 | 遗漏依赖 Bundle | 使用 Manifest.GetAllDependencies |
内存泄漏 | 未卸载 Bundle | Unload(true) + GC.Collect |
版本冲突 | 缓存旧 Bundle | 清除 persistentDataPath |
平台不兼容 | 跨平台打包 | 指定 BuildTarget |
AssetBundle 是 Unity 资源管理的核心技术,掌握后可实现高效的 DLC 更新和内存优化。现代项目推荐 Addressables,但理解底层 Bundle 机制有助于深度优化!
如需特定场景的打包脚本或 Addressables 迁移指南,请提供更多细节!