Unity中的资源管理-AssetBundle(1)

本文分享Unity中的资源管理-AssetBundle(1)

在上一篇文章中, 我们简单介绍了Unity中的资源和基本的使用, 今天我们详细介绍下使用AssetBundle来管理资源.

AssetBundle介绍

AssetBundle, 下面简称Ab, 本身的概念十分的简单, 顾名思义, 就是一系列资材打成的包而已.

相比Resources文件夹来组织资源的方式, Ab将使用某种方式将资源按照"包"的形式组织.

我们可以将某个文件夹内的所有资源组织为一个Ab, 也可以将某类资源组织为一个Ab, 还可以将某个文件夹下一部分资源组织为一个Ab, 甚至将来自不同文件夹的部分资源组织成为一个Ab.

具体怎么组织, 不同的项目需求不同, 比如有些项目比较小, 会将资源按照类型分类, 每种资源一个Ab. 而有些项目比较大, 会将每个单独的系统组织为各自组织为一个Ab, 甚至再细一点, 每个系统再按照资源类型组织成更多的Ab, 我们会在后面的文章详细介绍各种方案和其优缺点.

不管怎么组织, Ab的基本概念和使用方式基本是一致的, 我们今天只介绍这一部分.

首先, Ab的主要流程为:

  • 打包: 在Editor下根据配置打出Ab
  • 加载包: 运行时从指定位置将包加载到内存
  • 加载资源: 从包中加载资源使用
  • 卸载资源: 卸载从包中加载出来的资源
  • 卸载包: 从内存中卸载包

接下来我们会分几篇文件分别介绍.

打包

Unity提供了两种组织Ab方式, 一种是Asset Labels, 一种是AssetBundleBuild. 前者使用简单, 后者使用灵活.

官方AssetBundle插件: AssetBundle Browser

官方提供了Ab的插件, 降低了使用门槛, 可以从这个地址下载, 导入项目后在Window->AssetBundle Browser打开窗口.

通过Asset Labels设置好Ab包, 就可以使用插件进行管理和打包, 当然, 直接将文件夹或者资材往插件窗口拖动也可以自动设置Ab.

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图1展示当前项目所有的Ab包, 每个Ab包含哪些资材.

图2展示选定Ab的大小, 依赖等信息.

图3展示打包的参数如输出路径, 压缩方式等.

在一些小型项目中或者新手来说, 这个插件简直就是福音了.

Asset Labels

在每个资材(包含文件夹)的属性面板下方, 可以看到资材标签的配置:
在这里插入图片描述
黄色框框是Ab包名, 蓝色框框是Ab变体(Varient).

包名

如果设置文件夹的Asset Labels, 那么这个文件夹下所有的资材都会被加到该Ab中, 除非某个资材显示指定了Ab, 那么会从文件夹代表的Ab中移除.

Varient

变体是针对同名资源的不同配置, 最常使用的就是同一批资源可以根据不同的平台或者不同的精度有不同的变体.

变体要求包含的资源类型和数量还有名称都保持一致, 说起来很复杂, 给大家看张图就明白了, 下方的变体配置代表同样是几个纹理, 针对不同的清晰度有不同的变体:

在这里插入图片描述
在这里插入图片描述

也就是同样的图片要准备两套, 一套高清, 一套低清, 保持结构, 名称, 后缀一致, 最后会产生两个Ab, 在运行时根据需求使用.

在图1中, 我们也能清晰看到, texture有两个变体Ab, 内部结构一致.

打包命令

如果不使用插件, 可以使用代码进行打包: BuildPipeline.BuildAssetBundles(OutputPath, BuildAssetBundleOptions, BuildTarget);

返回值是一个清单信息: AssetBundleManifest, 通过清单信息可以拿到所有Ab信息和其依赖.

构建结果为输出目录下的所有Ab, 并且附加一个清单Ab来描述所有的Ab和其依赖关系, 即AssetBundleManifest也被打成了一个Ab, 这个清单Ab的名字和输出目录一致.

如果使用构建命令:

// 当前项目只有一个Ab, 包名为prefab
BuildPipeline.BuildAssetBundles("Assets/Output/AssetBundle/AllAb", BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64);

则其结果如下图所示:

在这里插入图片描述
每个Ab分为一个数据文件和一个清单文件(以.manifest)结尾.

附加Ab的清单文件内容为输出目录下所有Ab名称和其依赖(不包含自己), 如下图(AllAb.manifest):

在这里插入图片描述

其它Ab的清单文件包含本Ab的Hash信息, 所有的资材, 还有所有资材用到的组件信息和依赖关系等, 如下图(prefab.manifest)

在这里插入图片描述

Unity根据组件信息来反序列化组件, 比如Class: 0ObjectClass: 1GameObject, 具体的信息可以在Unity官网上查询.

参数说明:

  • OutputPath: 输出目录
  • BuildAssetBundleOptions: 构建选项(只列出常用的), 可以通过逻辑与(|)组合使用
    • None: 无, 使用LZMA压缩.
    • UncompressedAssetBundle: 不压缩.
    • ForceRebuildAssetBundle: 强制重新打包, 如果没有这个选项, 会采用增加打包的方式, 没有修改就略过打包.
    • AppendHashToAssetBundleName: 将Ab的hash值追加到Ab名字后面, 如: abtest_a616fc6a2ca2fb086b75525b5f6e2635, 下划线后面是hash值.
    • ChunkBasedCompression: 使用基于LZ4的压缩算法打包.
    • StrictMode: 严格模式, 只要任何报错则停止打包.
    • DryRunBuild: 空运行, 就是不实际打包, 只是返回所有包的清单信息.
    • DisableLoadAssetByFileName/DisableLoadAssetByFileNameWithExtension: 禁止使用资材文件名和文件名加扩展名的方式加载资材, 默认情况下, 可以通过以下方式加载资材(如果使用文件名或者文件名加扩展名的方式获取资材, 一旦有重名的文件就有可能获取错误的资材, 具体会获取哪个文件未知):
      • 资材全路径: "Assets/Res/Prefabs/obj.prefab"
      • 资材名称: "obj"
      • 资材名称加扩展名: "obj.prefab"
  • BuildTarget: 构建Ab支持的平台, 这里只列出常用的, 也可以通过EditorUserBuildSettings.activeBuildTarget获得当前激活的平台
    • NoTarget: 不指定平台
    • StandaloneOSX: Mac
    • StandaloneWindows/StandaloneWindows64: Windows
    • iOS: iOS
    • Android: Android

LZMA和LZ4压缩

LZMA采用流压缩方式(stream-based), 将所有的资材按照结构, 依次排序后构成数据流后再进行压缩.

优缺点如下:

  • 优点: 压缩比比较高, 产出的文件更小, 压缩速度更快
  • 缺点: 有顺序要求, 需要全部解压后才能加载资材, 加载时速度慢, 解压速度更慢, 不管有没有使用都内存中都会存在资材

LZ4采用块压缩方式(chunk-based), 将所有的资材按照结构分块后进行压缩.

优缺点如下:

  • 优点: 加载Ab时只需要加载头信息, 加载极快, 实际使用资材时才解压特定资材, 内存占用小
  • 缺点: 相比LZMA来说输出文件更大

默认情况下, Ab使用LZMA压缩, 如果游戏体量不大, 可以一次性在进入游戏时一次性加载所有的资源, 后续游戏过程中体验会比较流程.

AssetBundleBuild

在小型项目, 或者拥有良好结构的项目中, 使用标签的方式打包是最简单的, 设置好标签, 直接打包即可. Unity会为我们处理好依赖等麻烦.

使用标签很简单, 但是却不是很灵活, 而且一旦升级版本也有丢失的风险, 总之, 有各式各样的原因, 致使我们在大型项目中更多的使用自定义配置的方式进行打包, 也就是通过配置AssetBundleBuild打包.

使用自定义配置打包和标签打包是各自独立的, 也就是说同一个资源, 即使做了标签, 我们也可以将其打包到其它Ab之下.

使用方式也很简单, 每一个Ab对应了一个AssetBundleBuild, 我们在打包之前自己组织好所有的AssetBundleBuild, 最后交给构建管线(BuildPipline)打包即可.

[MenuItem("Test/BuildAb", false, 100)]
public static void TestBuild() {
    var assetBundleBuild = new AssetBundleBuild{
        assetBundleName = "abtest"
    };

    assetBundleBuild.assetNames = new[]{
        "Assets/Res/Prefabs/obj.prefab",
        "Assets/Res/Prefabs/test4.prefab",
    };

    var outputPath = "Assets/Output/AssetBundle/AllAb";
    var lst = new[]{assetBundleBuild};

    if (Directory.Exists(outputPath)) {
        Directory.Delete(outputPath);
    }

    Directory.CreateDirectory(outputPath);

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

AssetBundleBuild代表一个Ab的打包配置, assetBundleName属性为包名, assetNames为该包容纳的资材文件全路径数组.

最后在构建管线构建包的接口中将AssetBundleBuild数组加入第二个参数即可.

你以为这样就完了? 天真.

上面的打包表面上没什么问题, 但实际上有两个严重的问题:

  • 如果同一个资材出现在多个包怎么办?
  • 如果一个资材要使用另一个资材, 但是该资材不在本包怎么办?

使用标签打包时, Unity会为我们自动处理上面的问题, 而现在我们需要自己处理了.

处理重复和依赖

在打包时, 我们使用一个数据结构来保存所有已经在某个包中的资材标识, 以保证所有的文件不会重复出现在多个包中.

在将资材加入包的构建列表时, 通过接口查询该资材所使用的所有依赖资源, 如果之前未处理, 则加入到当前包中.

说起来比较复杂, 直接上代码:

[MenuItem("Test/Build2")]
public static void Build2() {
    var allAbPath = new[]{
        new[]{"Texture", "Assets/Res/Texture"},
        new[]{"Shaders", "Assets/Res/Shaders"},
        new[]{"Model", "Assets/Res/Model"},
        new[]{"Prefabs", "Assets/Res/Prefabs"},
    };

    var assetBundleBuildLst = new List<AssetBundleBuild>();
    var cachedAssets = new HashSet<int>(); // 保存资材名称字符串的hashcode, 用来过滤已被处理的文件

    // 处理资材文件
    void HandleFile(string path, in List<string> lst) {
        // 过滤部分不支持的资材
        if (path.EndsWith(".meta") || path.EndsWith(".cs") || path.EndsWith(".max")) {
            return;
        }

        // 路径可能是带有盘符的绝对路径, 去除盘符和反斜杠的差异
        var outPath = path.Substring(path.IndexOf("Assets", StringComparison.Ordinal));
        outPath = outPath.Replace("\\", "/");

        // 过滤已处理的资材
        if (cachedAssets.Contains(outPath.GetHashCode())) {
            return;
        }

        CollectDependencies(outPath, lst);
    }

    // 收集依赖
    void CollectDependencies(string path, in List<string> lst) {
        lst.Add(path);
        cachedAssets.Add(path.GetHashCode());

        // 收集依赖
        // 依赖包含自身, 延迟到HandleFile过滤
        var dependencies = AssetDatabase.GetDependencies(path);
        foreach(var dependency in dependencies) {
            HandleFile(dependency, lst);
        }
    }

    // 处理Ab和遍历目录
    foreach(var dirInfo in allAbPath) {
        var abName = dirInfo[0];
        var dir = dirInfo[1];

        // 收集每个Ab包含的所有资材
        var assetPathLst = new List<string>();
        var info = new DirectoryInfo(dir);

        foreach(var fileInfo in info.GetFiles("*", SearchOption.AllDirectories)) {
            HandleFile(fileInfo.FullName, assetPathLst);
        }

        assetBundleBuildLst.Add(new AssetBundleBuild {assetBundleName = abName, assetNames = assetPathLst.ToArray()});

        var sb = new StringBuilder(abName + " =====================>\n");
        assetPathLst.ForEach((s) => {
            sb.Append(s);
            sb.Append('\n');
        });

        Debug.Log(sb);
    }

    var outputDir = "Assets/Output/AssetBundle/AllAb";
    if (Directory.Exists(outputDir)) {
        Directory.Delete(outputDir, true);
    }

    Directory.CreateDirectory(outputDir);

    BuildPipeline.BuildAssetBundles(outputDir, assetBundleBuildLst.ToArray(), BuildAssetBundleOptions.ChunkBasedCompression, EditorUserBuildSettings.activeBuildTarget);
    AssetDatabase.SaveAssets();
    AssetDatabase.Refresh();
}

注释已经写的很清楚了, 这里不再赘述.

在组织Ab的时, 尽量将不引用或者少引用的包放在前面处理, 从而尽量保证资材处于与文件夹结构相同的位置.

打包策略

根据项目的类型, 项目的大小, 项目的文件组织结构, 会有很多不同的打包策略, 下面我们介绍一些常见的策略:

  • 所有的资源打成一个ab包(针对小型项目)
  • 所有资源按照类型分类, 比如: texture, prefab, model, shader, material, animation, config等(针对小型项目)
  • 按照功能, 将资源分为启动, 公共各个系统资源三个部分(针对大型项目):
    • 启动资源为正常启动的必要资源:
      • 如开屏动画, 初始界面UI, 热更新UI等,
      • 这一类资源可以放入Resources目录, 加速启动. 热更新完成后可从内存卸载.
    • 公共资源按照类型或者功能性划分为多个包, 可以选择性加载和卸载
    • 每个系统各自一个包, 或者大型系统进一步划分为多个子系统分为多个包, 可以选择性加载和卸载

总结

今天给大家介绍了Ab的基础知识还有打包方式和打包策略, 针对不同的项目可能会有不同的方式, 大家按需使用.

下一篇文章会介绍Ab同步加载和异步加载, 感兴趣的同学可以持续关注.

好了, 今天内容就是这么多, 希望对大家有所帮助.

猜你喜欢

转载自blog.csdn.net/woodengm/article/details/122091922