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

简介: 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/

相关文章
|
19天前
|
敏捷开发 测试技术 持续交付
自动化测试框架的演进与实践
随着软件开发流程的加速,自动化测试成为确保软件质量的关键。本文将探索自动化测试框架的发展轨迹,从早期的线性脚本到现代的持续集成环境中的复杂框架,并讨论如何根据项目需求选择合适的自动化测试工具,以及实施自动化测试策略时面临的挑战和解决方案。
|
5天前
|
Java 测试技术 Python
《手把手教你》系列基础篇(八十)-java+ selenium自动化测试-框架设计基础-TestNG依赖测试-番外篇(详解教程)
【6月更文挑战第21天】本文介绍了TestNG中测试方法的依赖执行顺序。作者通过一个实际的自动化测试场景展示了如何设计测试用例:依次打开百度、搜索“selenium”、再搜索“selenium+java”。代码示例中,`@Test`注解的`dependsOnMethods`属性用于指定方法间的依赖,确保执行顺序。如果不设置依赖,TestNG会按方法名首字母排序执行。通过运行代码,验证了依赖关系的正确性。
26 4
|
10天前
|
测试技术 持续交付 UED
探索自动化测试框架:提高软件质量的利器
在软件开发周期中,自动化测试框架扮演着至关重要的角色。本文将深入探讨自动化测试框架如何提升测试效率、确保软件质量和促进持续集成/持续部署(CI/CD)的实施。文章将分析自动化测试的优势、挑战以及实施策略,为读者提供一份全面的自动化测试指南。
|
14天前
|
监控 测试技术
局域网监控软件的自动化测试框架搭建(使用JUnit)
本文介绍了如何使用JUnit搭建局域网监控软件的自动化测试框架。通过创建测试类和编写测试用例,如连接测试和警报功能测试,确保软件功能的正确性。测试完成后,利用HTTP POST请求将监控数据自动提交到指定网站,实现数据的实时更新和追踪。这种方法能提升测试效率,保证软件质量和稳定性。
64 3
|
2天前
|
机器学习/深度学习 人工智能 测试技术
自动化测试框架的演进与实践
【6月更文挑战第23天】在软件工程领域,自动化测试框架的发展不断推动着质量保证的效率和效果。本文将探讨自动化测试框架从简单脚本到复杂集成系统的演变过程,并分析当前流行的框架如Selenium、Appium以及新兴的AI驱动测试工具。我们将通过具体案例,展示如何在现代软件开发实践中有效应用这些框架以提升测试覆盖率和准确性。
|
2天前
|
Java 测试技术 Python
《手把手教你》系列基础篇(八十一)-java+ selenium自动化测试-框架设计基础-TestNG如何暂停执行一些case(详解教程)
【6月更文挑战第22天】本文介绍了如何在TestNG中不执行特定测试用例。当部分模块未准备好时,可以通过以下方式暂停测试:③使用`@Test(enabled=false)`注解来禁用测试用例。作者提供了一个Java Selenium自动化测试的示例,展示如何通过修改`enabled`参数控制测试方法的执行。代码中,`testSearch2()`方法被禁用,因此在测试运行时不执行。文章还包含了测试报告和执行过程的截图。
33 7
|
6天前
|
Java 测试技术 Python
《手把手教你》系列基础篇(七十九)-java+ selenium自动化测试-框架设计基础-TestNG依赖测试-下篇(详解教程)
【6月更文挑战第20天】TestNG是一个Java测试框架,提供两种测试方法依赖机制:强依赖(所有前置方法成功后才运行)和弱依赖(即使前置方法失败,后置方法仍运行)。文中通过代码示例展示了这两种依赖如何实现,并解释了当依赖方法失败时,如何影响后续方法的执行。文章还包含了TestNG Suite的运行结果截图来辅助说明。
28 8
|
1天前
|
XML Java 测试技术
《手把手教你》系列基础篇(八十二)-java+ selenium自动化测试-框架设计基础-TestNG测试报告-上篇(详解教程)
【6月更文挑战第23天】TestNG 是一个用于自动化测试的 Java 框架,它自动生成测试报告,包括 HTML 和 XML 格式。报告可在 `test-output` 文件夹中找到。要创建测试用例,可创建一个实现了 `@Test` 注解的方法。通过 `testng.xml` 配置文件来组织和执行测试。默认报告包括测试结果、失败点和原因。用户还能实现 `ITestListener` 和 `IReporter` 接口来自定义报告和记录器。
18 2
|
22天前
|
存储 数据管理 测试技术
构建Python构建自动化测试框架(原理与实践)
当谈到软件质量保证时,自动化测试是一个不可或缺的步骤。Python作为一种简单易学的编程语言,具有丰富的测试框架和库,使得构建自动化测试框架变得相对简单。本文将介绍如何使用Python构建自动化测试框架,包括选择合适的测试框架、编写测试用例、执行测试和生成报告等方面。
构建Python构建自动化测试框架(原理与实践)
|
7天前
|
Java 测试技术 Python
《手把手教你》系列基础篇(七十七)-java+ selenium自动化测试-框架设计基础-TestNG依赖测试- 上篇(详解教程)
【6月更文挑战第18天】TestNG是一个Java测试框架,它允许在测试方法间定义执行顺序和依赖关系。当不指定依赖时,TestNG默认按方法名首字母排序执行。`@Test`注解的`dependsOnMethods`属性用于指定方法依赖,如`test1`依赖`test4`,则实际执行顺序为`test4`、`test2`、`test3`、`test1`。如果依赖的方法失败,后续依赖的方法将被跳过。此外,`dependsOnGroups`属性通过组名指定依赖,方便管理多个相关测试方法。通过`groups`定义方法所属组,然后在其他方法中用`dependsOnGroups`引用这些组。
21 5

热门文章

最新文章