Version: 6000.3
语言: 中文
从AssetBundles加载资产
优化资产包

处理 AssetBundle 之间的依赖关系

当资产包的对象引用另一个包中的对象时,资产包就会依赖于其他对象。如果引用的对象未分配给任何 AssetBundle,Unity 会在构建期间将其复制并嵌入到依赖的 Bundle 中。如果多个捆绑包引用同一个未分配的对象,则每个捆绑包包含自己的副本,从而增加内存使用量。

要加载依赖的 AssetBundle,请确保在访问依赖 Bundle 之前将依赖项加载到内存中。例如,如果 Bundle 1 包含引用 Bundle 2 中纹理的材质,请先将 Bundle 2 加载到内存中,然后再从 Bundle 1 访问材质。Unity 不会自动解析依赖项。要在运行时管理依赖项,您可以使用AssetBundleManifest.有关更多信息,请参阅从AssetBundle加载资产。

避免跨 AssetBundle 重复信息

默认情况下,Unity 不会优化 AssetBundle 中的重复数据。例如,如果两个 AssetBundle 每个 AssetBundle 都包含预制件:一种资产类型,允许您存储包含组件和属性的游戏对象。预制件充当模板,你可以从中在场景中创建新的对象实例。更多信息
请参阅术语表
引用相同的未分配材质时,Unity 会在两个捆绑包中嵌入该材质的副本。这会增加安装大小、运行时内存使用量并影响批处理,因为 Unity 将每个副本视为唯一。

优化技巧

将共享资产分配给公共资产包以避免重复。在构建过程中,Unity 会自动在分配的 AssetBundle 中包含依赖项。这大大减小了其他资产包的大小。例如:

  • 将共享材质及其依赖项提取到modulesmaterialsAssetBundle 中。
  • 然后,预制件包仅引用modulesmaterialsAssetBundle,减小其大小。

运行时加载

将通用AssetBundle用于共享资产时,请先将其加载到内存中,然后再加载依赖于它的AssetBundle。在以下示例中,共享材质被正确加载,因为其资产包(materialsAB) 首先加载:

using System.IO;
using UnityEngine;

public class InstantiateAssetBundles : MonoBehaviour
{
    void Start()
    {
        var materialsAB = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, Path.Combine("AssetBundles", "modulesmaterials")));
        var moduleAB = AssetBundle.LoadFromFile(Path.Combine(Application.dataPath, Path.Combine("AssetBundles", "example-prefab")));

        if (materialsAB == null || moduleAB == null)
        {
            Debug.Log("Failed to load AssetBundle!");
            return;
        }
        var prefab = moduleAB.LoadAsset<GameObject>("example-prefab");
        Instantiate(prefab);
    }
}

资产包卸载

卸载 AssetBundle 时正确管理依赖项,以防止崩溃或未定义的行为。卸载依赖项资产包后,依赖资产包不得保持加载状态。单独重新加载依赖项也可能导致问题。

卸载策略示例:引用计数

实现引用计数系统,仅在资产包不再使用时跟踪并安全卸载它们。

以下示例跟踪依赖项并安全地卸载未使用的 AssetBundle:

using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class AssetBundleManager
{
    private string assetBundlesDirectory;
    private AssetBundleManifest assetBundleManifest;
    private Dictionary<string, int> assetBundleReferenceCounts = new Dictionary<string, int>();
    private Dictionary<string, AssetBundle> loadedAssetBundles = new Dictionary<string, AssetBundle>();

    public void Initialize(string manifestBundlePath, string assetBundlesDirectory)
    {
        this.assetBundlesDirectory = assetBundlesDirectory;
        AssetBundle manifestBundle = AssetBundle.LoadFromFile(manifestBundlePath);
        assetBundleManifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        manifestBundle.Unload(false);
    }

    public AssetBundle LoadBundle(string bundlePath)
    {
        AssetBundle bundle = LoadAssetBundleIfNotLoaded(bundlePath);
        IncrementReferenceCount(bundle.name);

        string[] dependencyBundleNames = assetBundleManifest.GetAllDependencies(bundle.name);
        foreach (string dependency in dependencyBundleNames)
        {
            string dependencyBundlePath = getAssetBundlePathFromName(dependency);
            LoadAssetBundleIfNotLoaded(dependencyBundlePath);
            IncrementReferenceCount(dependency);
        }

        return bundle;
    }

    private AssetBundle LoadAssetBundleIfNotLoaded(string bundlePath)
    {
        if (!loadedAssetBundles.TryGetValue(bundlePath, out AssetBundle bundle))
        {
            // For simplicity, this example only shows the case of synchronous loading, but support for
            // LoadFromFileAsync() and the other load methods could also be added with similar code.
            bundle = AssetBundle.LoadFromFile(bundlePath);
            
            if (bundle == null)
            {
                throw new System.Exception($"Failed to load AssetBundle at path {bundlePath}");
            }
            loadedAssetBundles.Add(bundlePath, bundle);
        }

        return bundle;
    }


    public void UnloadBundle(AssetBundle bundle)
    {
        string[] dependencyBundleNames = assetBundleManifest.GetAllDependencies(bundle.name);

        DecrementRefCount(bundle.name);
        foreach (string dependency in dependencyBundleNames)
        {
            DecrementRefCount(dependency);
        }

        List<string> bundlesToUnload = new List<string>();
        foreach (KeyValuePair<string, AssetBundle> loadedBundleEntry in loadedAssetBundles)
        {
            if (assetBundleReferenceCounts[loadedBundleEntry.Value.name] <= 0)
            {
                bundlesToUnload.Add(loadedBundleEntry.Key);
            }
        }

        foreach (string bundlePath in bundlesToUnload)
        {
            loadedAssetBundles[bundlePath].Unload(true);
            loadedAssetBundles.Remove(bundlePath);
        }
    }

    private string getAssetBundlePathFromName(string name)
    {
        return Path.Combine(assetBundlesDirectory, name);
    }

    private void IncrementReferenceCount(string bundleName)
    {
        if (assetBundleReferenceCounts.ContainsKey(bundleName))
        {
            assetBundleReferenceCounts[bundleName]++;
        }
        else
        {
            assetBundleReferenceCounts[bundleName] = 1;
        }
    }

    private void DecrementRefCount(string bundleName)
    {
        if (assetBundleReferenceCounts.ContainsKey(bundleName))
        {
            assetBundleReferenceCounts[bundleName]--;
        }
        else {
            string errorMessage = $"Attempted to decrement reference count for non-existent bundle: {bundleName}";
            throw new KeyNotFoundException(errorMessage);
        }
    }
}

注意:当使用 LZ4 压缩和未压缩的 AssetBundles 时,AssetBundle.LoadFromFile仅在内存中加载其内容的目录,而不加载内容本身。要检查是否发生这种情况,请使用内存分析器包检查内存使用情况。

其他资源

从AssetBundles加载资产
优化资产包