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,可能是上次打包出来的,而这一次没生成的。
原文地址:https://www.cnblogs.com/baolong-chen/p/13407454.html