博客文章

Unity资源管理从AssetBundle到资源、代码热更(一)

作者: Andy.      时间: 2021-12-20 23:35:55

这是一个系列文章,将叙述一套完成的资源管理。包括资源包(AssetBundle)的生成管理,资源的同步/异步加载、释放管理。资源包热更生成,资源包的加密,资源包的下载(完全保证下载无问题,避免因为下载不完整等原因导致进入游戏出错),C#代码的热更。

因为上述的功能都是按照正常的流程排列的,所以第一篇文章主要围绕Asset Bundle的生成展开。

对于Asset Bundle的内存分布,网上有很多示例图片,这里就不详细叙述了。我们知道对于Asset Bundle,需要先将其加载到内存中获得一个UnityEngine.AssetBundle对象,然后再通过这个对象的LoadAsset或者LoadAssetAsync获得对象。所以要将同一模块相关的资源放在同一个AssetBundle里面。所以我们这里设置一个规则:

1、  将通用资源放在一个文件夹,将这个文件夹打成一个Asset Bundle。

2、 将每个prefab打成一个Asset Bundle。

因为要做到尽量多的资源和功能都能热更,简单来说即游戏中的所有东西都能热更。所以我们还要添加规则:

3、 游戏中的场景放在一个文件夹下,生成一个Asset Bundle。

这样,资源应该划分到哪个Asset Bundle就完成了。

 

如果我们在Unity Editor里面用Asset Labels对资源设置Asset Bundle的名称的话,这将发生灾难性的问题。比如Prefab太多,一个个设置太麻烦,要想修改划分也要一个个手动操作,如果有人做过测试修改后,不小心将资源上传到了版本库等等一系列问题都将导致不可预期的错误。所以选择写配置文件,配置要生成Asset Bundle的文件夹和文件最终生成到那个Asset Bundle里面。然后通过代码设置他们的Asset Bundle名称。

 

解决了资源划分到Asset Bundle的问题后,将面临新的问题:Asset Bundle依赖。这里遵守一个方案:

1、  需要整个文件夹生成Prefab的情况,配置整个文件夹的Asset Bundle。

2、 获取Prefab的依赖项,如果依赖已经存在于某个文件夹的Asset Bundle,则跳过。如果不存在,则与该Prefab放在同一个Asset Bundle。

这样我们就比较完美的完成了Asset Bundle的划分。另外,在最后我们要将资源信息、依赖项写入到一个配置文件。加载资源的时候,根据这个配置文件加载Asset Bundle,最后再获取资源。这部分内容将会放到后面的文章。

 

所以我们可以得到大体的制作流程:

1、  完成一个配置文件,配置Asset Bundle的划分。

2、 打包时,先加载这个配置文件。

3、 先设置文件夹的Asset Bundle。

4、 获取Prefab的依赖,设置依赖和Prefab的Asset Bundle。

5、 将资源信息和其依赖信息写入配置文件。

6、 调用BuildAssetBundles完成Asset Bundle的生成。

 

最后编码:

其中导出,帮助加载释放资源的配置放在下篇文章。

临时储存变量:

// dictionary to A AssetBundle.
public static Dictionary<string, string> AllFileDir = new Dictionary<string, string>();
// prefab to A AssetBundle. this contain all dependencies of this prefab.
public static Dictionary<string, List<string>> AllPrefabDir = new Dictionary<string, List<string>>();
// cache, prevent have added to list for set bundle name and which asset be added again.
public static List<string> HaveAddedToAssetBundle = new List<string>();
// cache, only add asset which config in ABConfigPath to our config file.
public static List<string> validPath = new List<string>();

新建一个类集成ScriptableObject,用以配置如何划分Asset Bundle。

[CreateAssetMenu(fileName = "AssetBundleConfig", menuName = "AssetBundleConfig", order = 0)]
public class AssetBundleConfig: ScriptableObject
{
    // find all prefabs in this directory
    public List<string> AllPrefabPath = new List<string>();
    // one director gernate one bundle.
    public List<FileDirName> AllFileDirAssetBundle = new List<FileDirName>();

    [System.Serializable]
    public struct FileDirName
    {
        public string AssetBundleName;
        public string Path;
    }
}

在生成Asset Bundle的时候加载这个配置:

var abConfig = AssetDatabase.LoadAssetAtPath<AssetBundleConfig>(ABConfigPath);

获取文件夹对应的Asset Bundle:

// Add directory.
foreach (var item in abConfig.AllFileDirAssetBundle)
{
    if (AllFileDir.ContainsKey(item.AssetBundleName))
    {
        Debug.LogError("AB name is duplicate.");
        return;
    }
    else
    {
        AllFileDir.Add(item.AssetBundleName, item.Path);
        HaveAddedToAssetBundle.Add(item.Path);
        validPath.Add(item.Path);
    }
}

获取Prefab、依赖对应的Asset Bundle:

var listPrefabsGUIDs = AssetDatabase.FindAssets("t:Prefab", abConfig.AllPrefabPath.ToArray());
for (int i = 0; i < listPrefabsGUIDs.Length; i++)
{
    string path = AssetDatabase.GUIDToAssetPath(listPrefabsGUIDs[i]);
    validPath.Add(path);
    EditorUtility.DisplayProgressBar("Search prefabs", path, (float)i / (float)listPrefabsGUIDs.Length);

    // Add Prefab denpencies.
    var allDependencies = AssetDatabase.GetDependencies(path);
    List<string> currentPrefabDependencies = new List<string>();
    for (int j = 0; j < allDependencies.Length; j++)
    {
        if (HaveAddedToBundle(allDependencies[j]) || allDependencies[j].EndsWith(".cs"))
            continue;
        currentPrefabDependencies.Add(allDependencies[j]);
        HaveAddedToAssetBundle.Add(allDependencies[j]);
    }
    var prefabName = Path.GetFileNameWithoutExtension(path);
    if (AllPrefabDir.ContainsKey(prefabName))
    {
        Debug.LogError(string.Format("Containe prefab: {0}", prefabName));
        return;
    }
    else
    {
        AllPrefabDir.Add(prefabName, currentPrefabDependencies);
    }
}

应用这些设置:

foreach (var item in AllFileDir)
{
    SetAssetBundleName(item.Key, item.Value);
}
foreach (var item in AllPrefabDir)
{
    SetAssetBundleName(item.Key, item.Value);
}


static void SetAssetBundleName(string name, List<string> paths)
{
    foreach (var path in paths)
    {
        SetAssetBundleName(name, path);
    }
}

static void SetAssetBundleName(string name, string path)
{
    AssetImporter assetImporter = AssetImporter.GetAtPath(path);
    if (assetImporter == null)
    {
        Debug.LogError(string.Format("Error when set assetbundle name at path: {0}", path));
        return;
    }
    assetImporter.assetBundleName = name;
}

生成最终结果:

static void BuildAssetBundle()
{
    string[] allBundleNames = AssetDatabase.GetAllAssetBundleNames();
    Dictionary<string, string> pathToBundleName = new Dictionary<string, string>();
    for (int i = 0; i < allBundleNames.Length; i++)
    {
        var assetPath = AssetDatabase.GetAssetPathsFromAssetBundle(allBundleNames[i]);
        for (int j = 0; j < assetPath.Length; j++)
        {
            if (allBundleNames[i].StartsWith(".cs"))
                continue;
            pathToBundleName.Add(assetPath[j], allBundleNames[i]);
        }
    }

    // If incremental build, need to delete unuse bundle.
    if (Directory.Exists(BundlePath))
    {
        Directory.Delete(BundlePath, true);
    }
    Directory.CreateDirectory(BundlePath);

    WriteAssetInfoToFile(pathToBundleName);

    BuildPipeline.BuildAssetBundles(BundlePath, BuildAssetBundleOptions.None, EditorUserBuildSettings.activeBuildTarget);
}