U3D client framework (resource management) to automatically open the Assetbundle package manager

1. Introduction to AssetBundle

AssetBundle is a collection of resources packaged using a compressed format provided by Unity for storing resources. It can store any kind of resources that Unity can recognize, such as models, texture maps, audio, scenes and other resources. It is also possible to load developer-defined binaries. Their file type is .assetbundle/.unity3d, they are pre-designed and can be easily downloaded into our game or scene.

In general, the specific development process of AssetBundle is as follows:
(1) To create an Asset bundle, the developer packages the required resources into an AssetBundle file through a script in the unity editor.
(2) Upload server. The developer uploads the packaged AssetBundle file to the server. This enables the game client to obtain current resources and update the game.
(3) Download AssetBundle, first download it to the local device, and then add resources to the game through the loading module of AsstBudle.
(4) Loading, through the API provided by Unity, the model, texture map, audio, animation, scene, etc. contained in the resource can be loaded to update the game client.
(5) Uninstall AssetBundle, which can save memory resources after uninstallation, and ensure the normal update of resources.

2. AssetBundle multi-platform packaging

2.1 Use Unity's own editor to package AssetBundle

(1) Only the resources in the Asset window can be packaged. We click GameObject->Cube, then create a preset in the Asset window, name it cubeasset, and drag the Cube to the preset.

(2) Click the newly created prefab cubeasset, and there is a creation tool named "AssetBundle" at the bottom of the property window in the lower right corner of the editor interface. Then just create it. The empty one can be created by clicking the menu option "New..." and name it "cubebundle". The name is fixed in lowercase. If capital letters are used, the system will automatically convert them to lowercase.
insert image description here

2.2 Use a custom packager to package AssetBundle

We are using a custom Assetbundle resource packager, not using the labeling method that comes with unity. Because using the custom packaging manager method has the following advantages: 1. It is easier to expand the AssetBundle packaging function; 2. The controllability is stronger; 3. The efficiency is higher and the packaging speed is faster;
whether the folder is a resource bundle: if Check a file to be packaged into an ab package; otherwise, each file is packaged into an ab package
Whether it is an initial resource: in the file
Whether to encrypt:
insert image description here

3. Code implementation

The code implementation part is described according to the process. The functions that are very detailed will not be written here, and these referenced but not written functions can be found in the complete code section. It will mainly explain in detail what each step does. I will talk about this process/function in detail, what it does, what it does, and the function of the function.

1. Cache package resource information BuildAssetBundleForPath

The main function of the function: add packaging resource information (settings: package name, suffix, variant name. Cache the packaging information); according to the settings in the editor, decide whether a folder is packaged into a package, or each file is packaged into a package Package, filter out all Meta files when setting the package file; you can decide whether to set the variant of the package according to the needs of the project: the main purpose of the variant is to load different package to achieve the effect of optimizing resources; reset the path: let the file start from the Assets/ directory (required for unity API packaging), and finally add the filtered file information to the compilation list.

  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. Use the Unity API to package resources BuildAssetBundles

According to the user's settings in the editor, pass the path information, packaging information, AssetBundle packaging settings, and packaging platform to UnityAPI, and then call the BuildPipeline.BuildAssetBundles function to package.

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

3. Copy the temporary directory of the file to the official directory CopyFile

The user's input is always unsafe, and the user cannot specify the final saved directory and file name, so first find a place to save it, and then specify the target path and file name through the code code, so there must always be a move in the end operate.

    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. Encrypted resource bundle AssetBundleEncrypt

According to the user's settings in the editor, decide whether this resource bundle should be encrypted. It is divided into encrypted folders and files. When encrypting a folder, you will get all the functions in the directory, and finally call the encrypted single file function. Then read the file stream (bytes array), use the XOR factor to perform XOR encryption on the file stream, rewrite the encrypted file stream data into the file, and the encryption is completed.

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. Generate dependencies file CreateDependenciesFile

The main function of the function is to cache the dependencies of the resource package. When loading, the loading order can be determined according to the configuration in the file. In the end, two files will be generated: json file: will record {file information: dependency list {a,b,c}}; this is mainly used for web access, directly obtain file dependency information through http, and is also convenient for debugging Check the dependency information at any time; bytes binary file: the main function of this is to store it locally and load it when comparing resources.

   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. Generate version file CreateVersionFile

Collect resource package information, write the name of the resource package, resource name MD5, byte size, whether it is a parent package resource, and whether it is encrypted into a line of data and write it into the version file. When updating resources, it is necessary to use the MD5 of the resource package and the MD5 of the file on the CDN for consistent matching. If there is no match, the resources in the CDN will be downloaded to the local first, so the version file is a necessary file. will be generated and then written to the packaging directory.

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();
        }
    }

full code

[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("生成版本文件完毕");
    }

}


plugins used

OdinInspector Odin editor plugin: https://odininspector.com/

Guess you like

Origin blog.csdn.net/qq_33531923/article/details/128496384