极简教程3:Assetbundle的依赖分析与分组打包

如果你不能用最简单的语言来描述,那你就是没有真正领悟。——爱因斯坦

一、依赖关系概述

我们首先通过三张图来了解一下什么是资源依赖。

图一:
在这里插入图片描述
在这张图中,模型A使用了一个材质B,而材质B中又包含了一个贴图C。那么他们的实际依赖关系便是:模型A——》材质B——》贴图C。
换言之,如果我们想在游戏中使用模型A,那么我们必须提前加载材质B。但是如果我们想要使用材质B,那么我们又必须提前加载贴图C。
这就是一个最简单的依赖模型。

我们继续看下一张图。
图二:
在这里插入图片描述

通过与图一的对比,我们很容看出这里所表达的依赖关系,即:模型B同时依赖材质B与材质C,而材质B又依赖于贴图D。

闲言少叙,我们即将进入正题,请看图三:

在这里插入图片描述

没错,图三就是将图一与图二结合在了一起。通过观察,我们发现模型A与模型B共用了相同的材质B。
那么问题来了,我们在对模型A与模型B进行Assetbundle的打包时,应该如何处理这部分共用的资源呢?
这就是我们今天的核心内容:资源的依赖打包。

根据以往的开发经验,我们很容易想到,如果我们忽略了这部分的共用资源,而将它们各自打包进模型A与模型B,势必会增加整体的资源大小,造成不必要的浪费,同时也增加了用户所需加载的资源量,这种做法非常不可取。所以,我们在制作Assetbundle包的同时,就有必要对资源的依赖关系进行分析,在根据每个资源被依赖的数量而采取不同的打包策略。

二、依赖资源的打包策略

有关Assetbundle的打包策略并不是唯一固定的,它是需要根据项目中的实际情况进行不断的调整与优化,以期达到一个合理的平衡。在这里,我仅给出那些被依赖的资源的打包策略的一些思路。

我们回到图三。
在这里插入图片描述假设一:当模型A与模型B相互独立,没有任何关系。换句话说,它们分别在两个不同的项目。
这时,我们完全不需要考虑他们是否存在公共资源,这时由于材质B只被当前唯一的一个模型所依赖,所以,此时的材质B以及其他的资源是可以和当前的模型A或者模型B打包在一起的。

假设二:当模型A与模型B在同一个项目时,且会被同时使用的时候,就非常有必要将材质B进行独立打包。

但是,请注意:模型A所引用的贴图C由于只被模型A一个资源所使用,因此,在我即将提供的打包方案中,对于这种情况,我是将贴图C与模型A打包在一起的

对于模型B所引用的贴图D亦是如此,贴图D将与模型B打包在一起。

三、打包算法概述

为了简单起见,我在这里设定了几条规则,当然这些规则要根据你的项目的实际情况进行调整。

规则1:

所有将被打包的文件都将被放置在统一的 待打包资源根路径(ResPath) 下,可以根据模块建立子目录。

规则2:

只有处于ResPath下的文件才可以被标记为父级资源

规则3:

每一个处于ResPath下的文件将被独立打包成assetbundle。

规则4:

非父资源被大于等于2的父级资源所依赖时,当前非父资源将被独立打包。否则,当前非父资源将与自己所归属的父级资源合并打包。请参考上文中有关图三的假设二的描述。

规则5:

所有的贴图/图集资源均将被独立打包。

好,如果您已经确保理解了这五点规则,那么我们就可以开始编写代码了。

四、打包算法

在这里,我一共使用了两个类来完成打包逻辑,代码内注释完整,我就不再赘述。请结合上文的五个规则,对我的代码进行阅读吧。

AssetBundleMgr.cs

入口文件

/*
 * @Author: 千樽江月
 * @Date: 2020-03-08 14:04:36
 * @LastEditors: 千樽江月
 * @LastEditTime: 2020-03-09 14:50:02
 * @description: 资源打包管理器
 */
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using System;
using SunixLib;


namespace EditorFrameWork
{
    /// <summary>
    /// 资源打包管理器
    /// </summary>
    public class AssetBundleMgr
    {
        //待处理预设文件列表
        private static List<FileInfo> toPackFileList = new List<FileInfo>();
        //资源节点集合;
        public static Dictionary<string, AssetNodeData> assetNodeDataDict = new Dictionary<string, AssetNodeData>();

        /// <summary>
        /// 启动打包
        /// </summary>
        [MenuItem("资源打包工具/资源打包", false)]
        public static void StartAssetbundle()
        {
            //获取当前assetBundle打包时输出的目录;
            string outPath = PathHelper.GetBundleOutPath();

            Debug.Log("预设根路径:" + Constant.PrefabPath);
            Debug.Log("输出路径:" + outPath);

            //清除已有资源路径,并重建
            ClearBundleFolder(outPath);
            //遍历预设根路径,获取合规资源,准备进行下一步依赖查找
            ListFile(new DirectoryInfo(Constant.PrefabPath));
            //获取依赖分析
            CollectDependencies();
            //设置资源标签名称,该步骤是打包的核心逻辑以及必要前提
            SetAssetsBundleName();
            //将保存写入硬盘;
            AssetDatabase.SaveAssets();
            //执行打包;
            BuildPipeline.BuildAssetBundles(outPath,BuildAssetBundleOptions.ChunkBasedCompression, GetTarget());
            //刷新面板;
            AssetDatabase.Refresh();
        }

        [MenuItem("资源打包工具/清理资源标签")]
        static void CleaarAllABNames()
        {
            string[] abnames = AssetDatabase.GetAllAssetBundleNames();
            foreach (var name in abnames)
                AssetDatabase.RemoveAssetBundleName(name, true);
        }

        /// <summary>
        /// 清除已有资源发布路径,并重建
        /// </summary>
        private static void ClearBundleFolder(string outPath)
        {
            if (Directory.Exists(outPath))
                Directory.Delete(outPath, true);
            Directory.CreateDirectory(outPath);
        }

        /// <summary>
        /// 获取当前打包的平台;
        /// </summary>
        /// <returns></returns>
        private static BuildTarget GetTarget()
        {
            BuildTarget target;
#if UNITY_IOS
            target = BuildTarget.iOS;
#elif UNITY_ANDROID
            target = BuildTarget.Android;
#elif UNITY_STANDALONE_WIN
            target = BuildTarget.StandaloneWindows64;
#endif
            return target;
        }

        /// <summary>
        /// 获取依赖分析
        /// </summary>
        private static void CollectDependencies()
        {
            for (int i = 0; i < toPackFileList.Count; i++)
            {
                FileInfo file = toPackFileList[i];
                //判断资源路径是否包含Assets,即是否为真实资源而非内置资源
                int index = file.FullName.IndexOf(Constant.PathStart);
                if (index != -1)
                {
                    //获取当前文件的相对路径,即以Assets开头
                    string assetPath = file.FullName.Substring(index);
                    //获取资源节点对象,当前资源都未根节点资源
                    AssetNodeData node = GetAssetsNodeData(assetPath,true);
                    //开始分析依赖树
                    AnalizyDependencies(node);
                }
            }
        }

        /// <summary>
        /// 设置资源包名称;
        /// </summary>
        private static void SetAssetsBundleName()
        {
            foreach (AssetNodeData node in assetNodeDataDict.Values)
                node.SetAssetBundleName(2);
        }

        /// <summary>
        /// 分析依赖关系
        /// </summary>
        private static void AnalizyDependencies(AssetNodeData curNode)
        {
            //获取资源对象;
            UnityEngine.Object asset = curNode.GetAsset();
            //获取hash数据,判断是否发生了资源变更;
            //TODO 可以根据这个值得变化判断是否要进行依赖检测,提高效率
            Hash128 code = AssetDatabase.GetAssetDependencyHash(curNode.AssetPath);
            string[] paths  = AssetDatabase.GetDependencies(curNode.AssetPath, true);
            foreach (string path in paths)
            {
                AssetNodeData node = GetAssetsNodeData(path);
                node.AddParent(curNode);
            }
            //立即释放资源;
            EditorUtility.UnloadUnusedAssetsImmediate();
        }

        /// <summary>
        /// 获取资源节点
        /// </summary>
        /// <param name="path">资源路径</param>
        /// <param name="beRoot">是否为根资源</param>
        /// <returns></returns>
        private static AssetNodeData GetAssetsNodeData(string path,bool beParent = false)
        {
            //把win系统中的路径分割符修改为Unity类型
            path = path.Replace("\\", "/");
            AssetNodeData node = null;
            if (!assetNodeDataDict.ContainsKey(path))
            {
                node = new AssetNodeData(path, beParent);
                assetNodeDataDict.Add(path, node);
            }
            node = assetNodeDataDict[path];
            node.beParent = beParent;
            return node;
        }

        /// <summary>
        /// 遍历当前文件夹,获取合规资源,准备进行下一步依赖查找
        /// </summary>
        /// <param name="fileSystemInfo"></param>
        /// <param name="fileList"></param>
        private static void ListFile(FileSystemInfo fileSystemInfo)
        {
            //将文件系统转为目录系统
            DirectoryInfo dirInfo = fileSystemInfo as DirectoryInfo;
            //获取文件夹下所有文件信息(含文件夹、文件)
            FileSystemInfo[] fileSystemInfos = dirInfo.GetFileSystemInfos();
            //遍历目录下所有项目
            foreach (FileSystemInfo item in fileSystemInfos)
            {
                //将项目转化为文件
                FileInfo fileInfo = item as FileInfo;
                //证明这是一个文件;
                if (fileInfo != null)
                {
                    //把win系统中的路径分割符修改为Unity类型
                    string strFileFullName = fileInfo.FullName.Replace("\\", "/");
                    //获取文件扩展名
                    string fileExt = Path.GetExtension(strFileFullName);
                    //判断当前后缀是否有效
                    int index = Array.IndexOf(Constant.UnableExtArray, fileExt);
                    if (index > -1)
                        continue;
                    else
                    {
                        //将符合条件的文件放入文件列表;
                        toPackFileList.Add(fileInfo);
                    }
                }
                //这是一个目录
                else
                    ListFile(item);
            }
        }
    }
}

AssetNodeData.cs

辅助数据类,用来对待打包的资源进行引用计算

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;

/*
 * @Author: 千樽江月
 * @Date: 2020-03-08 21:22:25
 * @LastEditors: 千樽江月
 * @LastEditTime: 2020-03-09 14:57:05
 * @description: 打包用资源文件;
 */


namespace EditorFrameWork
{
    /// <summary>
    /// 资源数据对象;
    /// </summary>
    public class AssetNodeData
    {
        //是否为父级资源;
        private bool _beParent = false;
        //当前资源被引用的资源集合,即我被多少个资源引用
        private List<AssetNodeData> parentDataSet = new List<AssetNodeData>();

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="assetPath">资源路径</param>
        /// <param name="beRoot">是否为根资源</param>
        public AssetNodeData(string assetPath, bool beParent = false)
        {
            this.beParent = beParent;
            this.AssetPath = assetPath;
        }

        /// <summary>
        /// 获取当前资源的父级数量;
        /// </summary>
        public int GetRootParentNum()
        {
            return parentDataSet.Count;
        }

        /// <summary>
        /// 获取父节点;
        /// </summary>
        /// <returns></returns>
        public AssetNodeData GetParent()
        {
            //理论上,调用该方法的对象一定只有一个父节点
            return parentDataSet[0];
        }

        /// <summary>
        /// 获取当前资源实体
        /// </summary>
        /// <returns></returns>
        public Object GetAsset()
        {
            return AssetDatabase.LoadMainAssetAtPath(AssetPath);
        }

        /// <summary>
        /// 将指定节点添加为我的父节点
        /// </summary>
        /// <param name="parent">父类节点</param>
        public void AddParent(AssetNodeData parent)
        {
            if (parent == this || parent == null || HasParent(parent))
                return;
            parentDataSet.Add(parent);
        }

        /// <summary>
        /// 判断此节点是否为我的父类节点
        /// </summary>
        /// <param name="targetParent">指定节点</param>
        /// <returns></returns>
        private bool HasParent(AssetNodeData node)
        {
            //判断我当前父类集合是否含有指定对象;
            if (parentDataSet.Contains(node))
                return true;
            //判断父类的父类集合是否含有指定对象
            var e = parentDataSet.GetEnumerator();
            while (e.MoveNext())
            {
                if (e.Current.HasParent(node))
                    return true;
            }
            return false;
        }

        /// <summary>
        /// 设置标签名称
        /// </summary>
        /// <param name="pieceThreshold"> 打包碎片粒度</param>
        public void SetAssetBundleName(int pieceThreshold)
        {
            //针对图集/贴图的处理,是将每张图集/贴图独立打包
            if (AssetsImporter is TextureImporter)
                SetTextureBundleName();
            else
            {
                //获取当前根级父容器的数量;
                int count = GetRootParentNum();
                if (this.beParent)
                    //设置父级对象的打包名称
                    SetAloneBundleName();
                else if (count >= pieceThreshold)
                    //设置被多重引用的资源打包名称;
                    SetAloneBundleName();   
                else
                    //非根节点,但是只被引用了一次,所以应该设置成父节点的abName;
                    SetSonBundleName();
            }
        }

        /// <summary>
        /// 设置图集的打包,每张贴图独立打包;
        /// </summary>
        private void SetTextureBundleName()
        {
            TextureImporter tai = AssetsImporter as TextureImporter;
            tai.spritePackingTag = AbName;
            //AssetBundleName和spritePackingTag保持一致
            Debug.Log("<color=#6501AB>" + "设置ab,我是贴图: " + this.AssetPath + " abname:" + tai.spritePackingTag + "</color>");
            tai.SetAssetBundleNameAndVariant(tai.spritePackingTag, string.Empty);
        }

        /// <summary>
        /// 设置被多重引用的资源打包名称;
        /// </summary>
        private void SetAloneBundleName()
        {
            AssetsImporter.SetAssetBundleNameAndVariant(AbName, string.Empty);
            Debug.Log("<color=#6501AB>" + "设置ab,我是根资源或者被多资源引用: " + this.AssetPath + " abname:" + AbName + "</color>");
        }

        /// <summary>
        /// 对只被引用了一次的子类对象进行打包名称的设置;
        /// 
        /// 该类型资源应该与父类资源打在一起;
        /// </summary>
        private void SetSonBundleName()
        {
            //获取父级对象
            AssetNodeData parent = GetParent();
            //设置打包名称
            AssetsImporter.SetAssetBundleNameAndVariant(AbName, string.Empty);
            Debug.Log("<color=#DBAF00>" + "设置, 仅有1个引用: " + this.AssetPath + " abname:" + AbName + "</color>");
        }

        /// <summary>
        /// 获取当前资源的bundleName
        /// </summary>
        private string AbName
        {
            get { 
                return this.AssetPath.Replace("/", "_") + ".ab";
            }
        }

        /// <summary>
        /// 获取资源导入器
        /// </summary>
        public AssetImporter AssetsImporter {
            get { return AssetImporter.GetAtPath(this.AssetPath); }
        }

        /// <summary>
        /// 当前资源路径;
        /// </summary>
        /// <value></value>
        public string AssetPath { get; private set; }

        /// <summary>
        /// 是否为父级对象;
        /// </summary>
        public bool beParent { 
            get { return _beParent; } 
            //如果当前已经是父级对象了,就不允许再被标记成非父级对象
            set {
                if (!_beParent)
                    _beParent = value;
            } 
        }

    }
}

至此,资源的依赖分析与分组打包就完成了。有关如何在项目中对存在依赖关系的资源进行加载,我将在下篇文章进行详细的叙述。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值