U3D客户端框架(资源管理篇)之自动化打Assetbundle包管理器

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: AssetBundle是将资源使用Unity提供的一种用于存储资源的压缩格式打包后的集合,它可以存储任何一种Unity可以识别的资源,如模型,纹理图,音频,场景等资源。也可以加载开发者自定义的二进制文件。他们的文件类型是.assetbundle/.unity3d,他们先前被设计好,很容易就下载到我们的游戏或者场景当中。

一、AssetBundle介绍


AssetBundle是将资源使用Unity提供的一种用于存储资源的压缩格式打包后的集合,它可以存储任何一种Unity可以识别的资源,如模型,纹理图,音频,场景等资源。也可以加载开发者自定义的二进制文件。他们的文件类型是.assetbundle/.unity3d,他们先前被设计好,很容易就下载到我们的游戏或者场景当中。


一般情况下AssetBundle的具体开发流程如下:

(1)创建Asset bundle,开发者在unity编辑器中通过脚本将所需要的资源打包成AssetBundle文件。

(2)上传服务器。开发者将打包好的AssetBundle文件上传至服务器中。使得游戏客户端能够获取当前的资源,进行游戏的更新。

(3)下载AssetBundle,首先将其下载到本地设备中,然后再通过AsstBudle的加载模块将资源加到游戏之中。

(4)加载,通过Unity提供的API可以加载资源里面包含的模型、纹理图、音频、动画、场景等来更新游戏客户端。

(5)卸载AssetBundle,卸载之后可以节省内存资源,并且要保证资源的正常更新。


二、AssetBundle多平台打包


2.1使用Unity自带的编辑器打包AssetBundle


(1)只有在Asset窗口中的资源才可以打包,我们单击GameObject->Cube,然后在Asset窗口创建一个预设体,命名为cubeasset,讲Cube拖到该预设体上。


(2)单击刚创建的预制件cubeasset,在编辑器界面右下角的属性窗口底部有一个名为”AssetBundle”的创建工具。接下来创建即可,空的可以通过单击菜单选项”New…”来创建,将其命名为”cubebundle”,名称固定为小写,如果使用了大写字母之后,系统会自动转换为小写格式。


fc032fd97d9e406582d3554c78301914.png


2.2 使用自定义的打包器 打包AssetBundle


我们使用的是自定义的Assetbundle资源打包器,没有使用unity自带的那个打标签的那种方式。因为使用自定义打包管理器方式有如下优点:1.对AssetBundle打包功能更方便拓展;2.可控性跟强一些;3.效率更高打包速度更快;


文件夹是否为一个资源包:如果勾选一个文件打包成一个ab包;否则每个文件打包成一个ab包


是否是初始资源:在文件


是否加密:


bd870de2ef12455dbc8ce1dbc96fcaa1.png


三、代码实现


代码实现部分是按照流程去讲的。很细致末节的函数这里不会写出来,在完整代码部分可以找到这些引用到的但是又没有写出来的函数。主要会详细讲解每一个步骤的做的事情。会详细讲这个流程/这个函数,是做什么的,做了什么,函数的功能。


1.缓存打包资源信息 BuildAssetBundleForPath


函数主要功能:添加打包资源信息(设置:包名、后缀、变体名。把打包信息缓存下来);根据编辑器内的设置,决定一个文件夹打成一个包,还是每个文件打成一个包,在设置打包文件的时候过滤掉所有的Meta文件;可以根据项目的需求决定是否要设置包的变体:变体的目的主要是为了作用之一是为了在低中高端机器上加载不同的包,以达到优化资源的效果;重新设置路径:让文件从Assets/目录开始(unity API打包需要),最后把过滤后的文件信息添加到编译链表里。


  private void BuildAssetBundleForPath(string path, bool overall)
    {
        //拼接完整路径
        string fullPath = string.Format("{0}/{1}", Application.dataPath, path);
        //1.拿到文件夹下的所有文件
        DirectoryInfo directoryInfo = new DirectoryInfo(fullPath);
        //拿到文件夹下所有文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*", SearchOption.AllDirectories);
        if (overall)
        {
            //打成一个资源包
            AssetBundleBuild build = new AssetBundleBuild();
            //ab name = 相对路径.ab
            build.assetBundleName = path + ".ab";
            //build.assetBundleVariant = "y";
            //过滤掉meta文件;
            //把目录更改成相对目录:以Assets开始(可能是unity打包流水线的要求)
            string[] arr = GetValidateFiles(arrFiles);
            build.assetNames = arr;
            builds.Add(build);
        }
        else
        {
            //每个文件打成一个包
            string[] arr = GetValidateFiles(arrFiles);
            for (int i = 0; i < arr.Length; ++i)
            {
                AssetBundleBuild build = new AssetBundleBuild();
                build.assetBundleName = arr[i].Substring(0, arr[i].LastIndexOf(".")).Replace("Assets/", "") + ".ab";
                //build.assetBundleVariant = "y";
                build.assetNames = new string[] { arr[i] };
                //add到builds里面
                builds.Add(build);
            }
        }
    }


2.使用Unity API 打包资源 BuildAssetBundles


根据用户在编辑器内的设置,把路径信息、打包信息、AssetBundle打包设置、打包平台,传至UnityAPI,然后调用BuildPipeline.BuildAssetBundles 函数进行打包。


//调用unity 自带的API 把builds里的路径都打包放到TempPath里 
BuildPipeline.BuildAssetBundles(TempPath, builds.ToArray(), Options, GetBuildTarget());


3.拷贝文件 临时目录拷贝到正式目录 CopyFile


用户的输入总是不安全的,不能让用户指定最终保存的目录和文件名,所以首先得找个地方存,然后通过代码代码里指定目标路径和文件名,所以最终总是要有一个move的操作。


    private void CopyFile(string oldPath)
    {
        //如果输出目录存在,先删掉
        if (Directory.Exists(OutPath))
        {
            Directory.Delete(OutPath, true);
        }
        //从临时目录拷贝到正式目录
        IOUtil.CopyDirectory(oldPath, OutPath);
        //拿到文件夹信息
        DirectoryInfo directoryInfo = new DirectoryInfo(OutPath);
        //拿到文件夹下的所有文件,搜索.y文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*.ab", SearchOption.AllDirectories);
        int len = arrFiles.Length;
        for (int i = 0; i < len; ++i)
        {
            //C#没有直接修改文件名字的方法,只能通过file.move去进行移动式修改
            FileInfo fileInfo = arrFiles[i];
            File.Move(fileInfo.FullName, fileInfo.FullName.Replace(".ab", ".assetbundle"));
        }
    }


4.加密资源包 AssetBundleEncrypt


根据用户在编辑器内的设置,决定这个资源包是否要加密。又分为加密文件夹和文件,加密文件夹时会拿到加该目录内的所有函数,最终还是会调用加密单文件函数。然后读取出来文件流(bytes数组),对文件流使用异或因子进行异或加密,把加密过的文件流数据重新写入文件,加密完成。


private void AssetBundleEncrypt()
    {
        int len = Datas.Length;
        for (int i = 0; i < len; ++i)
        {
            AssetBundleData assetBundleData = Datas[i];
            if (assetBundleData.IsEncrypt)
            {
                for (int j = 0; j < assetBundleData.Path.Length; ++j)
                {
                    string path = OutPath + "/" + assetBundleData.Path[j];
                    //打成一个包
                    if (assetBundleData.Overall)
                    {
                        //不是遍历文件夹打包,这个路径就是一个包
                        path = path + ".assetbundle";
                        AssetBundleEncryptFile(path);
                    }
                    else
                    {
                        AssetBundleEncryptFolder(path);
                    }
                }
            }
        }
    }


5.生成依赖关系文件 CreateDependenciesFile


函数主要作用是缓存好资源包的依赖关系,加载时候可根据文件内的配置决定先后加载顺序。最终会生成两个文件:json文件:会记录{文件信息:依赖列表{a,b,c}};这个主要是用于web访问的时候使用,直接通过http获取文件依赖信息,也方便调试的时候查看依赖信息;bytes二进制文件:这个的主要作用是放到本地,在比对资源的时候加载。


   private void CreateDependenciesFile()
    {
        //第一次循环   把所有的Asset存储到一个列表里
        //临时列表
        List<AssetEntity> lstTemp = new List<AssetEntity>();
        //循环设置文件夹包括子文件里面的项
        int len = Datas.Length;
        for (int i = 0; i < len; ++i)
        {
            AssetBundleData assetBundleData = Datas[i];
            for (int j = 0; j < assetBundleData.Path.Length; ++j)
            {
                //assets为根目录
                string path = Application.dataPath + "/" + assetBundleData.Path[j];
                //把所有的文件存到一个链表中
                CollectFileInfo(lstTemp, path);
            }
        }
        //获取临时链表的长度
        len = lstTemp.Count;
        //资源链表,会遍历临时list,然后在临时链表的基础上 加上这个文件的依赖信息
        List<AssetEntity> assetList = new List<AssetEntity>();
        for (int i = 0; i < len; ++i)
        {
            AssetEntity entity = lstTemp[i];
            //new一个新的AssetEntity
            AssetEntity newEntity = new AssetEntity();
            newEntity.Category = entity.Category;
            //找到最后一个/,因为最后一个/后面就是Asset的名字
            newEntity.AssetName = entity.AssetFullName.Substring(entity.AssetFullName.LastIndexOf("/") + 1);
            //去掉拓展名
            int iLastIndexOf = newEntity.AssetName.LastIndexOf(".");
            if (iLastIndexOf > -1)
            {
                newEntity.AssetName = newEntity.AssetName.Substring(0, newEntity.AssetName.LastIndexOf("."));
            } 
            newEntity.AssetFullName = entity.AssetFullName;
            newEntity.AssetBundleName = entity.AssetBundleName;
            assetList.Add(newEntity);
            //场景不需要检查依赖项
            if (entity.Category == AssetCategory.Scenes)
                continue;
            newEntity.ListDependsAsset = new List<AssetDependsEntity>();
            //string fullAssetPath = entity.AssetFullName;
            string[] arr = AssetDatabase.GetDependencies(entity.AssetFullName);
            foreach (string str in arr)
            {
                //依赖的assetbundle是AssetFullName,并且是Asset
                if (!str.Equals(newEntity.AssetFullName, StringComparison.CurrentCultureIgnoreCase) &&
                    IsContainAssetList(lstTemp, str))
                {
                    AssetDependsEntity assetDepend = new AssetDependsEntity();
                    assetDepend.Category = GetAssetCategory(str);
                    assetDepend.AssetFullName = str;
                    //把依赖的资源 加入到资源依赖列表中
                    newEntity.ListDependsAsset.Add(assetDepend);
                }
            }
        }
        //生成一个json文件
        string targetPath = OutPath;
        if (!Directory.Exists(targetPath))
            Directory.CreateDirectory(targetPath);
        //版本文件路径+AssetInfo.json
        string strJsonFilePath = targetPath + "/AssetInfo.json";
        IOUtil.CreateTextFile(strJsonFilePath, LitJson.JsonMapper.ToJson(assetList));
        //生成log
        Debug.Log("生成 AssetInfo.json完毕");
        //生成二进制数据
        MMO_MemoryStream ms = new MMO_MemoryStream();
        //assetList的长度
        len = assetList.Count;
        ms.WriteInt(len);
        for (int i = 0; i < len; ++i)
        {
            AssetEntity entity = assetList[i];
            ms.WriteByte((byte)entity.Category);
            ms.WriteUTF8String(entity.AssetFullName);
            ms.WriteUTF8String(entity.AssetBundleName);
            if (entity.ListDependsAsset != null)
            {
                //添加依赖资源
                //获取依赖资源数量
                int depLen = entity.ListDependsAsset.Count;
                for (int j = 0; j < depLen; ++j)
                {
                    //依赖资源的信息
                    AssetDependsEntity assetDependsEntity = entity.ListDependsAsset[j];
                    ms.WriteByte((byte)assetDependsEntity.Category);
                    ms.WriteUTF8String(assetDependsEntity.AssetFullName);
                }
            }
            else
            {
                ms.WriteInt(0);
            }
            string filePath = targetPath + "/AssetInfo.bytes";
            byte[] buffer = ms.ToArray();
            buffer = ZlibHelper.CompressBytes(buffer);
            FileStream fs = new FileStream(filePath, FileMode.Create);
            fs.Write(buffer, 0, buffer.Length);
            fs.Close();
            fs.Dispose();
            Debug.Log("生成AssetInfo.bytes文件 完毕");
        }
    }


6.生成版本文件 CreateVersionFile


收集资源包信息,把资源包的名字、资源名MD5、字节大小、是否是母包资源、是否加密信息写成一行数据写入到版本文件中。在资源热更时需要使用到资源包的MD5和CDN上的文件MD5做一致性匹配,如果不匹配会优先使用CDN中的资源下载到本地,所以版本文件是必须需要的文件,在打包时候就会生成,然后写入到打包目录中。


private void CreateVersionFile()
    {
        string path = OutPath;
        if (!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
        }
        //拼接版本文件路径
        string strVersionFilePath = path + "/VersionFile.txt";
        //如果存在版本文件,则删除
        IOUtil.DeleteFile(strVersionFilePath);
        StringBuilder sbContent = new StringBuilder();
        DirectoryInfo directoryInfo = new DirectoryInfo(path);
        //拿到文件夹下所有文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*",SearchOption.AllDirectories);
        //开始append信息
        //append资源版本信息
        sbContent.AppendLine(this.ResourceVersion);
        //1.循环所有可写区的文件,保存文件信息到sbContent
        for (int i = 0; i < arrFiles.Length; ++i)
        {
            FileInfo file = arrFiles[i];
            //manifest文件跳过
            if (file.Extension == ".manifest")
                continue;
            //包路径的全名
            string fullName = file.FullName;
            //相对路径
            //在打包路径下,搜索当前平台的路径
            //去除掉IOS/、Android/、Windows/ 路径字符串
            string name = fullName.Substring(fullName.IndexOf(CurrBuildTarget.ToString()) + CurrBuildTarget.ToString().Length + 1);
            //计算文件完整路径的md5
            string md5 = EncryptUtil.Md5(fullName);
            if (null == md5)
                continue;
            //计算文件大小(字节)
            string size = file.Length.ToString();
            //是否是初始化数据
            bool isFirstData = false;
            //该文件是否加密
            bool isEncrypt = false;
            //break标记
            bool isBreak = false;
            for (int j = 0; j < Datas.Length; ++j)
            {
                foreach (string tempPath in Datas[j].Path)
                {
                    //\\ rep to /
                    name = name.Replace("\\","/");
                    //在相对平台名字里面,找资源看路径
                    //拿到打包信息
                    if (name.IndexOf(tempPath, StringComparison.CurrentCultureIgnoreCase) != -1)
                    {
                        isFirstData = Datas[j].IsFirstData;
                        isEncrypt = Datas[j].IsEncrypt;
                        //如果在打包配置中找到了配置的文件信息,获取完有用的信息后,就跳出内两层循环
                        isBreak = true;
                    }
                }
                //在打包配置中找到了这个文件的配置信息
                if (isBreak)
                    break;
            }
            //format assetbundle文件信息
            string strLine = string.Format("{0}|{1}|{2}|{3}|{4}", name, md5, size, isFirstData ? 1 : 0,isEncrypt?1:0) ;
            sbContent.AppendLine(strLine);        
        }
        //2.循环完成,已经保存完所有文件的 assetName、md5、size、isFirstData、isEncryptData
        //创建文件,把所有的文件信息写入VersionFile.txt文件中
        IOUtil.CreateTextFile(strVersionFilePath,sbContent.ToString());
        MMO_MemoryStream ms = new MMO_MemoryStream();
        //删除掉多余的空格
        string str = sbContent.ToString().Trim();
        //使用\n 切割内容到一个数组里
        string[] arr = str.Split('\n');
        int len = arr.Length;
        //长度信息写入内存流
        ms.WriteInt(len);
        //循环保存
        for (int i = 0; i < len; ++i)
        {
            //第一个是版本信息 单独处理
            if (0 == i)
            {
                ms.WriteUTF8String(arr[i]);
            }
            else
            {
                //其他的都是文件信息,再次分割,拿到具体信息
                string[] arrInner = arr[i].Split('|');
                string name= arrInner[0];
                string md5= arrInner[1];
                ulong size = ulong.Parse(arrInner[2]);
                byte isFirstData = byte.Parse(arrInner[3]);
                byte isEncrypt = byte.Parse(arrInner[4]);
                //写成二进制数据
                ms.WriteUTF8String(name);
                ms.WriteUTF8String(md5);
                ms.WriteULong(size);
                ms.WriteByte(isFirstData);
                ms.WriteByte(isEncrypt);
            }
        }
        //版本文件路径
        string filePath = path + "/VersionFile.bytes";
        //拿到字节数组
        byte[] buffer = ms.ToArray();
        ms.Dispose();
        ms.Close();
        //对字节数组压缩
        buffer = ZlibHelper.CompressBytes(buffer);
        //将压缩过的二进制数据流,写入文件
        using (FileStream fs = new FileStream(filePath, FileMode.Create))
        {
            //写入文件
            fs.Write(buffer,0,buffer.Length);
            //关闭文件占用
            fs.Close();
            //释放fs内的资源
            fs.Dispose();
        }
    }


完整代码


[CreateAssetMenu]
class AssetBundleSetting : SerializedScriptableObject
{
    //必须加上可序列化标记
    [Serializable]
    public class AssetBundleData
    {
        //assetBundle的名称
        [LabelText("名称")]
        public string Name;
        //是否把文件夹打包成一个资源包(Overall:总的,全面的;又或者说 打包的这个是不是一个文件夹)
        [LabelText("文件夹为一个资源包")]
        public bool Overall;
        //这个assetbundle是否是初始资源
        [LabelText("是否是初始资源")]
        public bool IsFirstData;
        //是否加密(用啥加密算法)
        [LabelText("是否加密")]
        public bool IsEncrypt;
        //资源根节点路径(一个目录打多个包)
        [FolderPath(ParentFolder = "Assets")]
        public string[] Path;
    }
    //自定义打包平台
    //只支持打包自定义的这些平台
    public enum CusBuildTarget
    {
        Windows,
        Android,
        IOS,
    }
    //资源版本号
    [HorizontalGroup("Common", LabelWidth = 70)]
    [VerticalGroup("Common/Left")]
    [LabelText("资源版本号")]
    public string ResourceVersion = "1.0.1";
    //打包平台枚举
    [PropertySpace(10)]
    [VerticalGroup("Common/Left")]
    [LabelText("目标平台")]
    public CusBuildTarget CurrBuildTarget;
    //参数设置
    [PropertySpace(10)]
    [VerticalGroup("Common/Left")]
    [LabelText("参数")]
    public BuildAssetBundleOptions Options;
    //资源包保存路径
    [LabelText("资源包保存路径")]
    [FolderPath]
    public string AssetBundleSavePath;
    //编辑开关
    [LabelText("勾选进行编辑")]
    public bool IsCanEditor;
    //assetBundle设置
    [EnableIf("IsCanEditor")]
    [BoxGroup("AssetBundleSettings")]
    public AssetBundleData[] Datas;
    //要收集的资源包
    List<AssetBundleBuild> builds = new List<AssetBundleBuild>();
    #region 临时变量
    //临时目录
    public string TempPath
    {
        get
        {
            return Application.dataPath + "/../" + AssetBundleSavePath + "/" + ResourceVersion + "_Temp/" + CurrBuildTarget;
        }
    }
    //输出目录(就是临时目录去掉temp)
    public string OutPath
    {
        get
        {
            return TempPath.Replace("_Temp", "");
        }
    }
    #endregion
    public BuildTarget GetBuildTarget()
    {
        switch (CurrBuildTarget)
        {
            default:
            case CusBuildTarget.Windows:
                return BuildTarget.StandaloneWindows;
            case CusBuildTarget.Android:
                return BuildTarget.Android;
            case CusBuildTarget.IOS:
                return BuildTarget.iOS;
        }
    }
    //更新版本号(点击之后版本号+1)
    [VerticalGroup("Common/Right")]
    [Button(ButtonSizes.Medium)]
    [LabelText("更新版本号")]
    public void UpdateResourceVersion()
    {
        //拿到完整版本字符串
        //分割成三部分
        string version = ResourceVersion;
        string[] arr = version.Split('.');
        int shortVersion = 0;
        //拿到第三个参数,解析出来,转换成int类型
        int.TryParse(arr[2], out shortVersion);
        version = string.Format("{0}.{1}.{2}", arr[0], arr[1], ++shortVersion);
        ResourceVersion = version;
    }
    #region 打包函数
    //验证文件
    //这个相当于一个过滤器函数:1.过滤掉meta文件;2.让文件从Assets/目录开始(unity API打包需要)
    private string[] GetValidateFiles(FileInfo[] arrFiles)
    {
        List<string> lst = new List<string>();
        int iLen = arrFiles.Length;
        for (int i = 0; i < iLen; ++i)
        {
            FileInfo file = arrFiles[i];
            if (!file.Extension.Equals(".meta", StringComparison.CurrentCultureIgnoreCase))
            {
                //1.先把\\替换成/
                //2.把dataPath删除 D:/XXX/Assets/ 删除这个路径
                //3.拼接上Assets
                lst.Add("Assets" + file.FullName.Replace("\\", "/").Replace(Application.dataPath, ""));
            }
        }
        return lst.ToArray();
    }
    //加密文件
    private void AssetBundleEncryptFile(string filePath, bool isDelete = false)
    {
        FileInfo fileInfo = new FileInfo(filePath);
        byte[] buffer = null;
        //打开文件
        //拿到字节流(文件字节数据)
        using (FileStream fs = new FileStream(filePath, FileMode.Open))
        {
            buffer = new byte[fs.Length];
            fs.Read(buffer, 0, buffer.Length);
        }
        //对数据进行Xor运算
        buffer = SecurityUtil.Xor(buffer);
        //重新把字节流数据进文件(二进制流数据)
        using (FileStream fs = new FileStream(filePath, FileMode.Create))
        {
            fs.Write(buffer, 0, buffer.Length);
            fs.Flush();
        }
    }
    //加密文件夹下所有文件
    private void AssetBundleEncryptFolder(string folderPath, bool isDelete = false)
    {
        //文件夹信息
        DirectoryInfo directoryInfo = new DirectoryInfo(folderPath);
        //拿到文件夹下的所有文件信息
        FileInfo[] arrFiles = directoryInfo.GetFiles("*", SearchOption.AllDirectories);
        foreach (FileInfo fileInfo in arrFiles)
        {
            AssetBundleEncryptFile(fileInfo.FullName, isDelete);
        }
    }
    //获取资源分类(自定义的资源分类)
    private AssetCategory GetAssetCategory(string filePath)
    {
        AssetCategory category = AssetCategory.None;
        if (filePath.IndexOf("Reporter", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.Reporter;
        }
        else if (filePath.IndexOf("Audio", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.Audio;
        }
        else if (filePath.IndexOf("CusShaders", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.CusShaders;
        }
        else if (filePath.IndexOf("DataTable", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.DataTable;
        }
        else if (filePath.IndexOf("EffectSources", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.EffectSources;
        }
        else if (filePath.IndexOf("RoleEffectPrefab", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.RoleEffectPrefab;
        }
        else if (filePath.IndexOf("UIEffectPrefab", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.UIEffectPrefab;
        }
        else if (filePath.IndexOf("RolePrefab", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.RolePrefab;
        }
        else if (filePath.IndexOf("RoleSources", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.RoleSources;
        }
        else if (filePath.IndexOf("Scenes", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.Scenes;
        }
        else if (filePath.IndexOf("UIFont", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.UIFont;
        }
        else if (filePath.IndexOf("UIPrefab", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.UIPrefab;
        }
        else if (filePath.IndexOf("UIRes", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.UIRes;
        }
        else if (filePath.IndexOf("xLuaLogic", StringComparison.CurrentCultureIgnoreCase) != -1)
        {
            category = AssetCategory.xLuaLogic;
        }
        return category;
    }
    //获取资源包的名称
    private string GetAssetBundleName(string newPath)
    {
        // \\ -> /
        string path = newPath.Replace("\\", "/");
        int len = Datas.Length;
        for (int i = 0; i < len; ++i)
        {
            AssetBundleData assetBundleData = Datas[i];
            for (int j = 0; j < assetBundleData.Path.Length; ++j)
            {
                //path里包含 assetBundleData.path 吗
                if (path.IndexOf(assetBundleData.Path[j], StringComparison.CurrentCultureIgnoreCase) > -1)
                {
                    if (assetBundleData.Overall)
                    {
                        return assetBundleData.Path[j].ToLower();
                    }
                    else
                    {
                        return path.Substring(0, path.LastIndexOf('.')).ToLower().Replace("assets/", "");
                    }
                }
            }
        }
        return null;
    }
    //收集文件信息
    private void CollectFileInfo(List<AssetEntity> lstTemp, string folderPath)
    {
        DirectoryInfo directoryInfo = new DirectoryInfo(folderPath);
        //拿到文件夹下的所有文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*", SearchOption.AllDirectories);
        for (int i = 0; i < arrFiles.Length; ++i)
        {
            FileInfo file = arrFiles[i];
            if (file.Extension == ".meta")
                continue;
            //拿到完整路径
            string filePath = file.FullName;//全名
            //找到asset\\的开始位置
            int idx = filePath.IndexOf("Assets\\", StringComparison.CurrentCultureIgnoreCase);
            //删除Assets\\ 前面的路径
            //拿到新路径
            string newPath = filePath.Substring(idx);
            if (newPath.IndexOf(".idea") != -1)
                continue;
            AssetEntity entity = new AssetEntity();
            entity.AssetFullName = newPath.Replace("\\", "/");
            entity.Category = GetAssetCategory(newPath.Replace(file.Name, ""));
            entity.AssetFullName = GetAssetBundleName(newPath);
            //push到临时链表里去
            lstTemp.Add(entity);
        }
    }
    //判断某个资源是否存在于资源列表中
    private bool IsContainAssetList(List<AssetEntity> lstTemp, string assetFullName)
    {
        int len = lstTemp.Count;
        for (int i = 0; i < len; ++i)
        {
            AssetEntity entity = lstTemp[i];
            if (entity.AssetFullName.Equals(assetFullName, StringComparison.CurrentCultureIgnoreCase))
            {
                return true;
            }
        }
        return false;
    }
    //步骤:
    /*
     * 函数功能:
     * 1.添加打包资源信息(设置:包名、后缀、变体名。把打包信息缓存下来)
     * path:资源相对路径
     * overall:达成一个资源包
     */
    private void BuildAssetBundleForPath(string path, bool overall)
    {
        //拼接完整路径
        string fullPath = string.Format("{0}/{1}", Application.dataPath, path);
        //1.拿到文件夹下的所有文件
        DirectoryInfo directoryInfo = new DirectoryInfo(fullPath);
        //拿到文件夹下所有文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*", SearchOption.AllDirectories);
        if (overall)
        {
            //打成一个资源包
            AssetBundleBuild build = new AssetBundleBuild();
            //ab name = 相对路径.ab
            build.assetBundleName = path + ".ab";
            //过滤掉meta文件;
            //把目录更改成相对目录:以Assets开始(可能是unity打包流水线的要求)
            string[] arr = GetValidateFiles(arrFiles);
            build.assetNames = arr;
            builds.Add(build);
        }
        else
        {
            //每个文件打成一个包
            string[] arr = GetValidateFiles(arrFiles);
            for (int i = 0; i < arr.Length; ++i)
            {
                AssetBundleBuild build = new AssetBundleBuild();
                //里面拼接上了Asset
                //外面为啥要把Asset/去了
                //1.先把拓展名删除;2.删除Asset/前缀(估计是直接用文件名asset/a/c a/c作为包名);3.拓展名改成.ab
                build.assetBundleName = arr[i].Substring(0, arr[i].LastIndexOf(".")).Replace("Assets/", "") + ".ab";
                //build.assetBundleVariant = "y";
                build.assetNames = new string[] { arr[i] };
                //add到builds里面
                builds.Add(build);
            }
        }
    }
    //2.拷贝文件
    //从临时路径 拷贝文件 到正式目录 
    //顺便把ab.y 后缀改成.assetbundle
    private void CopyFile(string oldPath)
    {
        //如果输出目录存在,先删掉
        if (Directory.Exists(OutPath))
        {
            Directory.Delete(OutPath, true);
        }
        //从临时目录拷贝到正式目录
        IOUtil.CopyDirectory(oldPath, OutPath);
        //拿到文件夹信息
        DirectoryInfo directoryInfo = new DirectoryInfo(OutPath);
        //拿到文件夹下的所有文件,搜索.y文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*.ab", SearchOption.AllDirectories);
        int len = arrFiles.Length;
        for (int i = 0; i < len; ++i)
        {
            //C#没有直接修改文件名字的方法,只能通过file.move去进行移动式修改
            FileInfo fileInfo = arrFiles[i];
            File.Move(fileInfo.FullName, fileInfo.FullName.Replace(".ab", ".assetbundle"));
        }
    }
    //3.资源加密
    private void AssetBundleEncrypt()
    {
        int len = Datas.Length;
        for (int i = 0; i < len; ++i)
        {
            AssetBundleData assetBundleData = Datas[i];
            //加密想要机密的
            if (assetBundleData.IsEncrypt)
            {
                //加密想要加密的
                for (int j = 0; j < assetBundleData.Path.Length; ++j)
                {
                    string path = OutPath + "/" + assetBundleData.Path[j];
                    //打成一个包
                    if (assetBundleData.Overall)
                    {
                        //不是遍历文件夹打包,这个路径就是一个包
                        path = path + ".assetbundle";
                        AssetBundleEncryptFile(path);
                    }
                    else
                    {
                        AssetBundleEncryptFolder(path);
                    }
                }
            }
        }
    }
    private void CreateDependenciesFile()
    {
        //第一次循环   把所有的Asset存储到一个列表里
        //临时列表
        List<AssetEntity> lstTemp = new List<AssetEntity>();
        //循环设置文件夹包括子文件里面的项
        int len = Datas.Length;
        for (int i = 0; i < len; ++i)
        {
            AssetBundleData assetBundleData = Datas[i];
            for (int j = 0; j < assetBundleData.Path.Length; ++j)
            {
                //assets为根目录
                string path = Application.dataPath + "/" + assetBundleData.Path[j];
                //把所有的文件存到一个链表中
                CollectFileInfo(lstTemp, path);
            }
        }
        //获取临时链表的长度
        len = lstTemp.Count;
        //资源链表,会遍历临时list,然后在临时链表的基础上 加上这个文件的依赖信息
        List<AssetEntity> assetList = new List<AssetEntity>();
        for (int i = 0; i < len; ++i)
        {
            AssetEntity entity = lstTemp[i];
            //new一个新的AssetEntity
            AssetEntity newEntity = new AssetEntity();
            newEntity.Category = entity.Category;
            //找到最后一个/,因为最后一个/后面就是Asset的名字
            newEntity.AssetName = entity.AssetFullName.Substring(entity.AssetFullName.LastIndexOf("/") + 1);
            //去掉拓展名
            int iLastIndexOf = newEntity.AssetName.LastIndexOf(".");
            if (iLastIndexOf > -1)
            {
                newEntity.AssetName = newEntity.AssetName.Substring(0, newEntity.AssetName.LastIndexOf("."));
            }
            else
            {
                int x1 = 11;
            }
            newEntity.AssetFullName = entity.AssetFullName;
            newEntity.AssetBundleName = entity.AssetBundleName;
            assetList.Add(newEntity);
            //场景不需要检查依赖项
            if (entity.Category == AssetCategory.Scenes)
                continue;
            newEntity.ListDependsAsset = new List<AssetDependsEntity>();
            //string fullAssetPath = entity.AssetFullName;
            string[] arr = AssetDatabase.GetDependencies(entity.AssetFullName);
            foreach (string str in arr)
            {
                //依赖的assetbundle是AssetFullName,并且是Asset
                if (!str.Equals(newEntity.AssetFullName, StringComparison.CurrentCultureIgnoreCase) &&
                    IsContainAssetList(lstTemp, str))
                {
                    AssetDependsEntity assetDepend = new AssetDependsEntity();
                    assetDepend.Category = GetAssetCategory(str);
                    assetDepend.AssetFullName = str;
                    //把依赖的资源 加入到资源依赖列表中
                    newEntity.ListDependsAsset.Add(assetDepend);
                }
            }
        }
        //生成一个json文件
        string targetPath = OutPath;
        if (!Directory.Exists(targetPath))
            Directory.CreateDirectory(targetPath);
        //版本文件路径+AssetInfo.json
        string strJsonFilePath = targetPath + "/AssetInfo.json";
        IOUtil.CreateTextFile(strJsonFilePath, LitJson.JsonMapper.ToJson(assetList));
        //生成log
        Debug.Log("生成 AssetInfo.json完毕");
        //生成二进制数据
        MMO_MemoryStream ms = new MMO_MemoryStream();
        //assetList的长度
        len = assetList.Count;
        ms.WriteInt(len);
        for (int i = 0; i < len; ++i)
        {
            AssetEntity entity = assetList[i];
            ms.WriteByte((byte)entity.Category);
            ms.WriteUTF8String(entity.AssetFullName);
            ms.WriteUTF8String(entity.AssetBundleName);
            if (entity.ListDependsAsset != null)
            {
                //添加依赖资源
                //获取依赖资源数量
                int depLen = entity.ListDependsAsset.Count;
                for (int j = 0; j < depLen; ++j)
                {
                    //依赖资源的信息
                    AssetDependsEntity assetDependsEntity = entity.ListDependsAsset[j];
                    ms.WriteByte((byte)assetDependsEntity.Category);
                    ms.WriteUTF8String(assetDependsEntity.AssetFullName);
                }
            }
            else
            {
                ms.WriteInt(0);
            }
            //生成AssetInfo.bytes文件
            string filePath = targetPath + "/AssetInfo.bytes";
            byte[] buffer = ms.ToArray();
            buffer = ZlibHelper.CompressBytes(buffer);
            FileStream fs = new FileStream(filePath, FileMode.Create);
            fs.Write(buffer, 0, buffer.Length);
            fs.Close();
            fs.Dispose();
            Debug.Log("生成AssetInfo.bytes文件 完毕");
        }
    }
    //5.生成版本文件
    private void CreateVersionFile()
    {
        string path = OutPath;
        if (!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
        }
        //拼接版本文件路径
        string strVersionFilePath = path + "/VersionFile.txt";
        //如果存在版本文件,则删除
        IOUtil.DeleteFile(strVersionFilePath);
        StringBuilder sbContent = new StringBuilder();
        DirectoryInfo directoryInfo = new DirectoryInfo(path);
        //拿到文件夹下所有文件
        FileInfo[] arrFiles = directoryInfo.GetFiles("*",SearchOption.AllDirectories);
        //开始append信息
        //append资源版本信息
        sbContent.AppendLine(this.ResourceVersion);
        //1.循环所有可写区的文件,保存文件信息到sbContent
        for (int i = 0; i < arrFiles.Length; ++i)
        {
            FileInfo file = arrFiles[i];
            //manifest文件跳过
            if (file.Extension == ".manifest")
                continue;
            //包路径的全名
            string fullName = file.FullName;
            //相对路径
            //在打包路径下,搜索当前平台的路径
            //去除掉IOS/、Android/、Windows/ 路径字符串
            string name = fullName.Substring(fullName.IndexOf(CurrBuildTarget.ToString()) + CurrBuildTarget.ToString().Length + 1);
            //计算文件完整路径的md5
            string md5 = EncryptUtil.Md5(fullName);
            if (null == md5)
                continue;
            //计算文件大小(字节)
            string size = file.Length.ToString();
            //是否是初始化数据
            bool isFirstData = false;
            //该文件是否加密
            bool isEncrypt = false;
            //break标记
            bool isBreak = false;
            for (int j = 0; j < Datas.Length; ++j)
            {
                foreach (string tempPath in Datas[j].Path)
                {
                    //\\ rep to /
                    name = name.Replace("\\","/");
                    //在相对平台名字里面,找资源看路径
                    //拿到打包信息
                    if (name.IndexOf(tempPath, StringComparison.CurrentCultureIgnoreCase) != -1)
                    {
                        isFirstData = Datas[j].IsFirstData;
                        isEncrypt = Datas[j].IsEncrypt;
                        //如果在打包配置中找到了配置的文件信息,获取完有用的信息后,就跳出内两层循环
                        isBreak = true;
                    }
                }
                //在打包配置中找到了这个文件的配置信息
                if (isBreak)
                    break;
            }
            //format assetbundle文件信息
            string strLine = string.Format("{0}|{1}|{2}|{3}|{4}", name, md5, size, isFirstData ? 1 : 0,isEncrypt?1:0) ;
            sbContent.AppendLine(strLine);        
        }
        IOUtil.CreateTextFile(strVersionFilePath,sbContent.ToString());
        MMO_MemoryStream ms = new MMO_MemoryStream();
        //删除掉多余的空格
        string str = sbContent.ToString().Trim();
        //使用\n 切割内容到一个数组里
        string[] arr = str.Split('\n');
        int len = arr.Length;
        //长度信息写入内存流
        ms.WriteInt(len);
        //循环保存
        for (int i = 0; i < len; ++i)
        {
            //第一个是版本信息 单独处理
            if (0 == i)
            {
                ms.WriteUTF8String(arr[i]);
            }
            else
            {
                //其他的都是文件信息,再次分割,拿到具体信息
                string[] arrInner = arr[i].Split('|');
                string name= arrInner[0];
                string md5= arrInner[1];
                ulong size = ulong.Parse(arrInner[2]);
                byte isFirstData = byte.Parse(arrInner[3]);
                byte isEncrypt = byte.Parse(arrInner[4]);
                //写成二进制数据
                ms.WriteUTF8String(name);
                ms.WriteUTF8String(md5);
                ms.WriteULong(size);
                ms.WriteByte(isFirstData);
                ms.WriteByte(isEncrypt);
            }
        }
        //版本文件路径
        string filePath = path + "/VersionFile.bytes";
        //拿到字节数组
        byte[] buffer = ms.ToArray();
        ms.Dispose();
        ms.Close();
        //对字节数组压缩
        buffer = ZlibHelper.CompressBytes(buffer);
        //将压缩过的二进制数据流,写入文件
        using (FileStream fs = new FileStream(filePath, FileMode.Create))
        {
            //写入文件
            fs.Write(buffer,0,buffer.Length);
            //关闭文件占用
            fs.Close();
            //释放fs内的资源
            fs.Dispose();
        }
    }
    #endregion
    //清空资源包
    [VerticalGroup("Common/Right")]
    [Button(ButtonSizes.Medium)]
    [LabelText("清空资源包")]
    public void ClearAssetBundle()
    {
        if (Directory.Exists(TempPath))
        {
            Directory.Delete(TempPath, true);
        }
        EditorUtility.DisplayDialog("", "清空完毕", "确定");
    }
    [VerticalGroup("Common/Right")]
    [Button(ButtonSizes.Medium)]
    [LabelText("打包")]
    public void BuildAssetBundle()
    {
        //每次打包前,先clear assetBundleBuild信息。防止打包两次
        builds.Clear();
        //拿到配置里的assetBundle的长度
        int len = Datas.Length;
        //打包
        //如果写了多路径,又写了Overall,配置中的路径会每个路径打一个包
        //如果写了多路径,没写Overall,会把这些路径里的所有的文件都打包成assetbundle包
        for (int i = 0; i < len; ++i)
        {
            AssetBundleData assetBundleData = Datas[i];
            int lenPath = assetBundleData.Path.Length;
            //一个设置,可以设置打多个包
            for (int j = 0; j < lenPath; ++j)
            {
                //打包路径/文件
                string path = assetBundleData.Path[j];
                //1.往builds里添加打包信息
                BuildAssetBundleForPath(path, assetBundleData.Overall);
            }
        }
        //如果不存在临时写入目录,就创建一个
        if (!Directory.Exists(TempPath))
        {
            Directory.CreateDirectory(TempPath);
        }
        if (builds.Count == 0)
        {
            Debug.Log("未找到需要打包的内容");
            return;
        }
        Debug.Log(" builds count:" + builds.Count);
        //2.调用unity 自带的API 把builds里的路径都打包放到TempPath里 
        BuildPipeline.BuildAssetBundles(TempPath, builds.ToArray(), Options, GetBuildTarget());
        Debug.Log("临时资源打包完毕");
        //3.拷贝文件
        CopyFile(TempPath);
        Debug.Log("文件拷贝到输出目录完毕");
        //4.使用异或因子加密资源包
        AssetBundleEncrypt();
        Debug.Log("资源包加密完毕");
        //5.生成依赖关系文件
        CreateDependenciesFile();
        Debug.Log("生成依赖关系文件完毕");
        //6.生成版本文件完毕
        CreateVersionFile();
        Debug.Log("生成版本文件完毕");
    }
}


使用到的插件


OdinInspector Odin编辑器插件:https://odininspector.com/

相关文章
|
4天前
|
设计模式 前端开发 JavaScript
自动化测试框架设计原则与最佳实践####
本文深入探讨了构建高效、可维护的自动化测试框架的核心原则与策略,旨在为软件测试工程师提供一套系统性的方法指南。通过分析常见误区,结合行业案例,阐述了如何根据项目特性定制自动化策略,优化测试流程,提升测试覆盖率与执行效率。 ####
23 6
|
4天前
|
人工智能 前端开发 测试技术
探索软件测试中的自动化框架选择与优化策略####
本文深入剖析了当前主流的自动化测试框架,通过对比分析各自的优势、局限性及适用场景,为读者提供了一套系统性的选择与优化指南。文章首先概述了自动化测试的重要性及其在软件开发生命周期中的位置,接着逐一探讨了Selenium、Appium、Cypress等热门框架的特点,并通过实际案例展示了如何根据项目需求灵活选用与配置框架,以提升测试效率和质量。最后,文章还分享了若干最佳实践和未来趋势预测,旨在帮助测试工程师更好地应对复杂多变的测试环境。 ####
19 4
|
9天前
|
机器学习/深度学习 前端开发 测试技术
探索软件测试中的自动化测试框架选择与优化策略####
本文深入探讨了在当前软件开发生命周期中,自动化测试框架的选择对于提升测试效率、保障产品质量的重要性。通过分析市场上主流的自动化测试工具,如Selenium、Appium、Jest等,结合具体项目需求,提出了一套系统化的选型与优化策略。文章首先概述了自动化测试的基本原理及其在现代软件开发中的角色变迁,随后详细对比了各主流框架的功能特点、适用场景及优缺点,最后基于实际案例,阐述了如何根据项目特性量身定制自动化测试解决方案,并给出了持续集成/持续部署(CI/CD)环境下的最佳实践建议。 --- ####
|
10天前
|
Java 测试技术 持续交付
【入门思路】基于Python+Unittest+Appium+Excel+BeautifulReport的App/移动端UI自动化测试框架搭建思路
本文重点讲解如何搭建App自动化测试框架的思路,而非完整源码。主要内容包括实现目的、框架设计、环境依赖和框架的主要组成部分。适用于初学者,旨在帮助其快速掌握App自动化测试的基本技能。文中详细介绍了从需求分析到技术栈选择,再到具体模块的封装与实现,包括登录、截图、日志、测试报告和邮件服务等。同时提供了运行效果的展示,便于理解和实践。
46 4
【入门思路】基于Python+Unittest+Appium+Excel+BeautifulReport的App/移动端UI自动化测试框架搭建思路
|
9天前
|
测试技术 API Android开发
探索软件测试中的自动化框架选择与实践####
本文深入探讨了软件测试领域内,面对众多自动化测试框架时,如何依据项目特性和团队需求做出明智选择,并分享了实践中的有效策略与技巧。不同于传统摘要的概述方式,本文将直接以一段实践指南的形式,简述在选择自动化测试框架时应考虑的核心要素及推荐路径,旨在为读者提供即时可用的参考。 ####
|
13天前
|
测试技术 Android开发 UED
探索软件测试中的自动化框架选择
【10月更文挑战第29天】 在软件开发的复杂过程中,测试环节扮演着至关重要的角色。本文将深入探讨自动化测试框架的选择,分析不同框架的特点和适用场景,旨在为软件开发团队提供决策支持。通过对比主流自动化测试工具的优势与局限,我们将揭示如何根据项目需求和团队技能来选择最合适的自动化测试解决方案。此外,文章还将讨论自动化测试实施过程中的关键考虑因素,包括成本效益分析、维护难度和扩展性等,确保读者能够全面理解自动化测试框架选择的重要性。
31 1
|
19天前
|
监控 安全 jenkins
探索软件测试的奥秘:自动化测试框架的搭建与实践
【10月更文挑战第24天】在软件开发的海洋里,测试是确保航行安全的灯塔。本文将带领读者揭开软件测试的神秘面纱,深入探讨如何从零开始搭建一个自动化测试框架,并配以代码示例。我们将一起航行在自动化测试的浪潮之上,体验从理论到实践的转变,最终达到提高测试效率和质量的彼岸。
|
23天前
|
Web App开发 敏捷开发 存储
自动化测试框架的设计与实现
【10月更文挑战第20天】在软件开发的快节奏时代,自动化测试成为确保产品质量和提升开发效率的关键工具。本文将介绍如何设计并实现一个高效的自动化测试框架,涵盖从需求分析到框架搭建、脚本编写直至维护优化的全过程。通过实例演示,我们将探索如何利用该框架简化测试流程,提高测试覆盖率和准确性。无论你是测试新手还是资深开发者,这篇文章都将为你提供宝贵的洞见和实用的技巧。
|
11天前
|
机器学习/深度学习 自然语言处理 物联网
探索自动化测试框架的演变与未来趋势
随着软件开发行业的蓬勃发展,软件测试作为保障软件质量的重要环节,其方法和工具也在不断进化。本文将深入探讨自动化测试框架从诞生至今的发展历程,分析当前主流框架的特点和应用场景,并预测未来的发展趋势,为软件开发团队选择合适的自动化测试解决方案提供参考。
|
14天前
|
测试技术 持续交付
探索软件测试中的自动化框架:优势与挑战
【10月更文挑战第28天】 随着软件开发的快速进步,自动化测试已成为确保软件质量的关键步骤。本文将探讨自动化测试框架的优势和面临的挑战,以及如何有效地克服这些挑战。
29 0