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

这篇具有很好参考价值的文章主要介绍了U3D客户端框架(资源管理篇)之自动化打Assetbundle包管理器。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、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”,名称固定为小写,如果使用了大写字母之后,系统会自动转换为小写格式。
unity包管理器,轻量级客户端框架(Unity3D),3d,自动化,unity

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

我们使用的是自定义的Assetbundle资源打包器,没有使用unity自带的那个打标签的那种方式。因为使用自定义打包管理器方式有如下优点:1.对AssetBundle打包功能更方便拓展;2.可控性跟强一些;3.效率更高打包速度更快;
文件夹是否为一个资源包:如果勾选一个文件打包成一个ab包;否则每个文件打包成一个ab包
是否是初始资源:在文件
是否加密:
unity包管理器,轻量级客户端框架(Unity3D),3d,自动化,unity

三、代码实现

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

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/文章来源地址https://www.toymoban.com/news/detail-699011.html

到了这里,关于U3D客户端框架(资源管理篇)之自动化打Assetbundle包管理器的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • U3D热更新技术

    @作者 : SYFStrive @博客首页 : HomePage 📌: 个人社区(欢迎大佬们加入) 👉: 社区链接🔗 🤷‍♀️: 创作不易转发需经作者同意😈 💃: 程序员每天坚持锻炼💪 👉 U3D热更新技术 (🔥) 🕐:开发者将测试好的代码,发布到应用商店的审核平台,平台方会进行稳定性及性

    2024年02月02日
    浏览(51)
  • U3D通过按钮点击实现场景切换

    1.新建UI,选择button选项,新建button;   3.新建一个空对象,挂载一个scenechange c#脚本; 4.编写脚本,1头文件using UnityEngine.SceneMangement                    2public void change() {                     scenemanager.loadscene (1)  }//括号中的数字为第2步中场景后面的数字          

    2024年02月07日
    浏览(44)
  • 【U3D引擎】没有切换中文选项&切换中文模式?

    第一步,查看是否有勾选简体中文模块 第二步,拉到底部,勾选简体中文,点击继续 第三步,勾选已阅读同意后点击安装 第四步,等待下载安装完成 过程中会自动安装VS软件 如有下载失败可重新下载 注:如果重复提示错误也没有没有关系,直接略过就好, 第五步,随便打

    2024年02月06日
    浏览(68)
  • 【Unity入门】1.创建第一个u3d项目

            大家好,我是Lampard~~      欢迎来到Unity入门系列博客     Halo大家好久不见,最近半年比较懒惰,一直都比较少更新(不过摆烂确实挺开心哈哈哈哈哈)。最近项目要转3D,引擎要从以前的cocos转向unity,关注我的小伙伴可能知道,我以前开发的一直是cocos2d的内容,

    2024年03月15日
    浏览(49)
  • AVProVideo☀️一、一款U3D视频播放插件介绍

    🎊 商务合作:https://skode.cn/file/businesscard/wechat.jpg 🎥 本文由 星河造梦坊公司官方 原创! 🏅 如果你有技术问题或项目开发,都可以加上方的联系方式,和我聊一聊你的故事🧡 前段时间看到有人问: 橙哥,AVProVideo支持8K全景视频嘛? 看来,好多人对这款插件的支持的功能

    2024年02月07日
    浏览(46)
  • 云备份客户端——客户端整体设计框架以及实用类工具实现

    客户端要实现的功能和服务端相比相对简单,客户端要实现的功能是 自动对指定文件中的文件进行备份,也就是定时对指定文件进行扫描,根据文件信息判断文件,符合要求(新文件或者被修改过的文件)进行上传 因此我们客户端大概需要实现下面三个模块 数据管理模块:

    2024年02月09日
    浏览(52)
  • 使用U3D、pico开发VR(二)——添加手柄摇杆控制移动

    1System: 2Move Speed:注意速度过小会导致看起来没有移动 3RightHandMoveAction(right loco move):个人采用右手柄实现移动 1System: 2Turn Speed:注意旋转速度过小会导致看起来没有移动 3RightHandMoveAction:个人采用右手柄实现旋转 至此,初步的移动功能就实现了

    2024年02月08日
    浏览(61)
  • unity客户端开源框架

    链接:https://github.com/yomunsam/TinaX/tree/master TinaX 主要实现了以下功能: Lua 语言支持 出于普遍的热更新需求,TinaX原生提供了基于 Tencent/xlua 的Lua语言运行环境,并为主要功能提供了Lua层面的API支持。 如果不需要Lua环境的话,也可以在项目中将Lua相关功能完全关闭,不会影响包

    2024年01月16日
    浏览(68)
  • U3D游戏角色血条制作并显示血量变化

    关键:利用Slider来制作血条 大概效果: 数字会随着血量变化而变化。 步骤 1、在层级面板中右击,选择UI中的Slider. 2、创建好后,将Slider命名为HealthBar,可以看到层级面板中Slider的结构为,删掉其中的“Handle Slide Area”. 3、在层级面板选中HealthBar,在右侧的检查器窗口可以看到

    2024年02月09日
    浏览(42)
  • AVPro Movie Capture☀️一、一款U3D录屏插件介绍

    🎊 商务合作:https://skode.cn/file/businesscard/wechat.jpg 🎥 本文由 星河造梦坊公司官方 原创! 🏅 如果你有技术问题或项目开发,都可以加上方的联系方式,和我聊一聊你的故事🧡 AVPro Movie Capture,是一款249美元的Unity的游戏录制插件。 它既支持游戏内录制,也支持编辑器内录制

    2024年02月07日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包