【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 更新
AddressablesUnity 官方新系统,封装 AssetBundle推荐现代项目

3. 命名约定

  • Bundle 名characters_hero1_v1.0,版本化命名。
  • 资源路径:相对 Bundle 内部路径,如 textures/sword_diffuse.png
  • 标签系统:按类型分组,如 ui/prefabs/button.unity3d

打包流程详解

1. 资源准备与分组

  1. 创建文件夹结构
   Assets/
   ├── AssetBundles/
   │   ├── Characters/
   │   │   ├── HeroA.prefab
   │   │   └── HeroB.prefab
   │   ├── Environments/
   │   │   ├── Level1.unity
   │   │   └── Level2.unity
   │   └── UI/
   │       └── MainMenu.prefab
   └── Editor/
       └── BuildScript.cs
  1. 标记资源:选中资源 → 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
内存泄漏未卸载 BundleUnload(true) + GC.Collect
版本冲突缓存旧 Bundle清除 persistentDataPath
平台不兼容跨平台打包指定 BuildTarget

AssetBundle 是 Unity 资源管理的核心技术,掌握后可实现高效的 DLC 更新和内存优化。现代项目推荐 Addressables,但理解底层 Bundle 机制有助于深度优化!

如需特定场景的打包脚本或 Addressables 迁移指南,请提供更多细节!

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注