zoukankan      html  css  js  c++  java
  • Unity资源Tree-资源打包

    1 基本API

    1.1 打包唯一API

    public static AssetBundleManifest BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform)
    

    调用BuildPipeline.BuildAssetBundles,引擎将自动根据资源的assetbundleName属性(以下简称abName)批量打包,自动建立Bundle以及资源之间的依赖关系。

    1.2 收集依赖API

    AssetDatabase.GetDependencies(string path, [default bool recursive = true]);

    默认开启,true会收集依赖及其间接依赖,false只会收集直接依赖。

    1.3 打包选项API

    ForceRebuildAssetBundle
    用于强制重打所有AssetBundle文件;
    
    IgnoreTypeTreeChanges
    用于判断AssetBundle更新时,是否忽略TypeTree的变化;
    
    AppendHashToAssetBundleName
    用于将Hash值添加在AssetBundle文件名之后,开启这个选项包名变为{xxx_hash},就可以直接通过文件名来判断哪些Bundle的内容进行了更新(4.x下普遍需要通过比较二进制等方法来判断,但在某些情况下即使内容不变重新打包,Bundle的二进制也会变化)。
    

    5.x下默认会将TypeTree信息写入AssetBundle,因此在移动平台上DisableWriteTypeTree选项可优化。

    1.4 Manifest文件

    namespace UnityEngine
    {
      public class AssetBundleManifest : Object
      {
        public extern string[] GetAllAssetBundles();
        public extern string[] GetAllAssetBundlesWithVariant();
        public Hash128 GetAssetBundleHash(string assetBundleName);
        public extern string[] GetDirectDependencies(string assetBundleName);
        public extern string[] GetAllDependencies(string assetBundleName);
      }
    }

    在打包后生成的文件夹中,每个Bundle都会对应一个manifest文件,记录了Bundle的一些信息,但这类manifest只在增量式打包时才用到;同时,根目录下还会生成一个同名manifest文件及其对应的Bundle文件,通过该Bundle可以在运行时得到一个AssetbundleManifest对象,而所有的Bundle以及各自依赖的Bundle都可以通过该对象提供的接口进行获取。

    1.5 Variant参数

    在Inspector界面最下方,除了可以指定abName,在其后方还可以指定Variant。打包时,Variant会作为后缀添加在Bundle名字之后。相同abName,不同variant的Bundle中,资源必须是一一对应的,且他们在Bundle中的ID也是相同的,从而可以起到相互替换的作用。

    image

    variant可以根据不同性能设备设置不同分辨率的资源,借助Variant的特性,只需创建两个文件夹,分别放置两套不同的资源,且资源名一一对应,然后给两个文件夹设置相同的abName和不同的variant。在加载该abName的时候指定对应的Variant即可。

    1.6 包体结构

    image

    image

    Unity官网也给出的详细说明,上面普通包体结构文件分为两个部分:序列化信息和资源信息。下面是场景包体结构:Unity说了该图Main scene和Shared data的内容是相反的

    序列化信息包含标识符、压缩类型和内容清单。清单是一个以Objects name为键的查找表。每个条目都提供一个字节索引,用来指示该Objects在AssetBundle数据段的位置。在加载AssetBundle时会优先加载序列化信息。

    资源信息包含通过序列化AssetBundle中的Assets而生成的二进制原始数据。如果指定LZMA压缩,则对所有序列化Assets后的完整字节数组进行压缩。如果指定了LZ4,则分段压缩每个Assets的字节。如果不使用压缩,数据段将保持为原始字节流。

    2 打包需要注意的问题

    2.1 资源冗余问题

    这里说的资源冗余是指同一份资源被打包到多个AB里,这样就造成了存在多份同样资源。在Unity运行时加载后内存中会出现多份同样的资源,造成内存开销

    资源冗余造成的问题

    1. 冗余造成内存中加载多份同样的资源占用内存。
    2. 同一份资源通过多次加载,IO性能消耗。
    3. 导致包体过大。

    解决冗余方案是依赖打包

    依赖打包是指资源间虽有依赖关系,但该资源被多个assetbundle重复依赖,在打包时要将被重复依赖的资源单独打成assetbundle,这样就形成了AB与AB之间的依赖。Unity4.x版本提供的API是:

    BuildPipeline.PushAssetDependencies
    BuildPipeline.PopAssetDependencies

    Unity5.x及以后提供的是指定AssetBundle Name的形式以及增量打包,Note:关于增量打包,Unity维护了一张全局.manifest文件,它能让我们知道更新了哪些AB,但是并没有告诉更新的AB中用到了哪些Assets,所以用到哪些Assets依然需要自己解决,因为只有知道了一个AB存储了哪些Asset信息,我们才能在加载AB的时候对特定的Asset做缓存、释放等操作。毕竟只有在知道包名的前提下才能加载所需的Asset出现这种问题,一般都是人为的拖拽了某个Asset到了新目录、或者删除某些Asset但是代码却没有更新。这都是隐藏的问题需要注意。针对这个问题也有解决办法,见下文。

    第二个就是增量打包也是热更新涉及的范畴。

    2.2 打包策略问题

    虽然可以对AssetBundle打包策略自由的进行规划,但是在进行项目的资源管理的时候,Unity官网提供了一些建议可以:

    2.2.1 依据逻辑实体进行分组

      这种资源分类方式是依据资源的功能进行分类,例如 UI/角色/场景/code等具体各自功能规划的部分来进行资源分组,可以将所有的textures都打入UI相关分类中,可以将所有的模型和动画都打入角色相关的资源中,将场景相关的贴图和模型都打入场景资源中。

      采用逻辑实体分组,对于资源的下载更新更为有利,由于资源的分类,可以在进行资源更新的时候,只更新对应的资源,而无需更新冗余的其他资源。

      使用这种分类方式最合适的策略,就是将资源进行详细的分类

    2.2.2 类型分组

      主要依据资源的类型来进行分组,这样对于不同的应用平台都具有一定的适用性。比如对于audio文件的压缩设置,在mac和windows上都是一致的,那么可以将audio文件都归类为一类文件,实现文件资源的复用(不同平台的打包设置),对于shaders而言,对于不同平台需要不同的编译设置,那么就需要分类处理。这类分类方法,对于在不同的版本中变动频率较低的代码文件和prefabs显得更有优势。

    2.2.3 相互关联的内容分组

      这种策略的,就是将需要同时进行加载的资源都归类为一个分组,例如将不同场景中的角色都依据场景来进行分组,这就要求单独一个场景中的资源只能用于该场景,各个分组之间没有互相关联的关系。这种分类方式,对于资源的加载时间有较大的缩减,这种分类方式的使用场合主要在场景资源中,在不同的场景资源中,其包含的资源各自互相不关联。

    在一个项目中,可以将上述的几种策略都交互使用,对应具体的应用需求来灵活的采用分组策略,当然unity也提供了一些资源分组的tips:

    • 分离高频和低频更新的资源;
    • 将需要同时下载的资源合并进一个组,例如Model以及其关联的animations;
    • 如果出现多个bundle中的多个object都依赖于另一个完全不同的bundle,那么将这些依赖关系都移动到一个单独的bundle,这样可以降低依赖关系的复杂度;多个bundle均依赖于另一个bundle中的资源,那么将这些bundle以及其依赖的资源归类到一个资源,这样可以降低资源的重复率(避免一份资源被拷贝到多个不同的bundle中);
    • 不可能同时加载的资源,需要归类的各自的assetbundle中,例如标准和高配的资源;
    • 如果一个assetbundle中资源在加载的时候低于50%需要被加载,那么可以考虑将这些需要被加载的资源单独分类为一个资源(避免冗余的加载);
    • 如果一组Objects对应的是一个资源的不同版本,那么可以考虑assetbundle variants

    上面三点也仅是给出了一个参考方向,具体项目需要具体分析。

    2.3 AssetBundle压缩格式问题

    AB压缩不压缩问题,主要考虑的点如下:

    1. 加载时间
    2. 加载速度
    3. 资源包体大小
    4. 打包时间
    5. 下载AB时间

    到底要不要压缩,采用LZMA还是LZ4,也是需要具体项目具体分析。LZMA适合从服务器下载。

    3 实施打包

    3.1 给定入口目录,指定根资产

    入口目录和根资产决定一个包体。为什么要指定根资产?这涉及到AB包的命名,而根资产也是包体内的mainAsset。下面给出一个自定义的数据结构

    public class AssetInfo : System.IComparable<AssetTarget>
    {
            // 目标Object
            public Object asset;
            // 文件路径
            public FileInfo file;
            // 相对目录
            public string assetPath;
            // 是否是内置的
            public bool isBuiltin = false;
            // 资产类型
            public AssetBundleBuildType buildType = AssetBundleBuildType.Asset;
            // 保存地址
            public string bundleSavePath;
            // BundleName
            public string bundleName;
            // 短名
            public string bundleShortName;
            // 目标文件是否已改变
            private bool _isFileChanged = false;
            // 是否已分析过依赖
            private bool _isAnalyzed = false;
            // 依赖树是否改变(用于增量打包)
            private bool _isDepTreeChanged = false;
            // 上次打包的信息(用于增量打包)
            private AssetCacheInfo _cacheInfo;
            // .meta 文件的Hash
            private string _metaHash;
            // 上次打好的AB的CRC值(用于增量打包)
            private string _bundleCrc;
            // 是否是第一次打包
            private bool _isNewBuild;
            // 我依赖的项
            private HashSet<AssetTarget> _imDependSet = new HashSet<AssetTarget>();
            // 依赖我的项
            private HashSet<AssetTarget> _dependMeSet = new HashSet<AssetTarget>();
    }

    既然有了根资产(Root),那就能确定该Root的所有依赖资产(普通资产DepAsset),进而就能确定哪些资产能组成一个包。这就涉及到了依赖分析,见下。

    3.2 依赖分析

    上面提到了根资产Root、普通资产DepAsset能组成一个包,没错。但是由于冗余问题,DepAsset可能会被多个Root、或多个DepAsset依赖,也就是会被多个AB包依赖,那么该资产就要单独打包。伪代码

    void Analyze()
    {
      第一步
      //先获取该asset的所有依赖,它可能是root、也可能是普通depAsset
      Object[] deps = EditorUtility.CollectDependencies(new Object[] { asset });
      //然后过滤一次脚本、光照内置资源,这里也可以过滤shader资源
      list<Object> realDeps;
      for(i < deps.count)
      {
         Object o = deps[i];
         if(o is 脚本 || o is 光照 || /*o is shader*/) continue;
         //过滤内置资源 Resources/builtin 与 Library/builtin
         path = getAssetPath(o);
         if(path.StartWith("Resources" || "Library"))  continue;
         realDeps.add(o);
      }
      第二步
      //拿到所有依赖项之后,再分析依赖项的依赖
      for(i < realDeps.count)
      {
        //把object转换为assetInfo
        depAsset = Load(realDep[i]);
        //双向依赖添加
        //添加asset的"我依赖的项",这里面不能自己依赖自己
        asset._imDependSet.add(depAsset);
        //添加depAsset的"依赖我的项"
        depAsset._dependMeSet.add(Asset);
        //分析depAsset依赖。 就这样循环下去,分析出所有
        depAsset.Analyze();
      }
    }

    上面分析出了以根资产为入口的依赖,及其依赖的依赖。 接下来要分析重复依赖,伪代码

    void 合并依赖()
    {
      //该assetInfo的”依赖我的项”个数是否大于x?这里x可以配置,有些项目可能允许一定的重复打包。这里先x=1.
      //单独成包
      if _dependMeSet.count > x
      {
        //依赖我的项就只直接依赖我,而不能再间接依赖我依赖的项,就需要从它们的“我依赖的项”中删除那一部分,且双向删除
        //换句话讲,A依赖C,B依赖C,C依赖D。那么依赖C的A和B的“我依赖的项”就不需要D只要C了。A与B删除我依赖的项D,D删除依赖我的项A与B。
        //这样C单独成包,就只有包与包直接的依赖
        //取出直接依赖我的项parent 
        foreach(parent dependMeSet)
        {
          //取出我直接依赖的项
          foreach(child imDependSet)
          {
            parent._imDependSet.remove(child);
            child._depentMeSet.remove(parent);
          }
        }
      }
    }

    上面合并了依赖之后,就要识别普通depAsset是否被依赖了多次,是就要单独成包。然后执行打包。

    3.3 缓存增量打包数据

    增量打包需要记录版本号;然后是每个包的hash,以及它的依赖包。

    增量打包也涉及到资源热更新。

    if 第一次打包
      缓存所有包的hash信息,及其他必要信息
    else
      后续打包,识别hash是否更改
    
    if hash有更改
      单独记录下此包信息
    
    //收集完所有增量信息后,就可以写上传至资源服务器等相关逻辑

    3.4 缓存所有依赖信息

    缓存AB包信息,缓存的依赖信息在加载时有用

    //asset path
    //bundle name
    //rootAsset name
    //hash
    //依赖项个数
    //依赖项
    最后再删除未使用的AB,可能是上次打包出来的,而这一次没生成的。
  • 相关阅读:
    集合中的3个经典练习题
    创建泛类集合List以及数组转集合,集合转数组的应用
    添加一个txt文件(例如在桌面),利用后台对文件写入内容
    File类的创建,删除文件
    集合中存放随机数的问题之解析
    集合中的类型转化 以及求集合中元素的最大值,平均值
    response.sendRedirect()使用注意事项
    request.getContextPath是为了解决相对路径的问题,可返回站点的根路径
    java中instanceof的用法
    getParameter 与 getAttribute的区别
  • 原文地址:https://www.cnblogs.com/baolong-chen/p/13407454.html
Copyright © 2011-2022 走看看