Unity编辑器开发(四):实战、开发一个AB包编辑器工具

前言

在我们上一篇(Unity编辑器开发(三):实战、开发一个AB包编辑器工具)的结尾,我们拥有了如下图中那样的一个编辑器窗口:

这里写图片描述

接下来我们要继续干点有意义的事情了,毕竟这样一个空框框的东西确实不怎么美观。

Assets资源检索:资源对象类

今天的任务不是很重,我们只完成编辑器窗口右边的资源检索界面就可以了,让我们可以检索整个项目中的所有资源,这听起来很简单,当然做起来也很简单(可能,稍微有那么一点点的难度)。

首先,分析一下我们的需求,Assets目录下说白了就是一层一层的文件夹和文件而已,随便一个递归就能全部遍历出来,然后我们要将他们以同样的层级关系绘制到面板上,考虑性能方面的问题,我们不可能在代码中每帧都去遍历Assets以绘制界面,所以必须得遍历一次之后就将结果保存下来,那么,我们得将所有资源都统一抽象为一种类型的对象,以便于我们存储,到这里,资源对象类(AssetInfo)诞生了。

1、让我们新建这个类,AssetInfo将被用来代表Assets路径下的任何资源文件(包括文件夹)等。

//【AssetInfo.cs】
public class AssetInfo
{
}

2、然后AssetInfo中我们将要设置哪些属性呢?让我们想想对于一个资源文件来说,我们所关心的是哪些属性,我们想要保留的是哪些属性。首先,资源文件在硬盘中的完整路径我们需要保留,加入属性:

        /// <summary>
        /// 资源全路径
        /// </summary>
        public string AssetFullPath
        {
            get;
            private set;
        }

3、因为资源最终的目的是要打入AB包中,所以我们还要保留资源文件的Assets路径(也即是从当前项目的Assets目录开始的路径,别问我为什么,这么坑爹的设定我也不知道是谁想出来的),加入属性:

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

4、资源文件的名称要不要?我们要用来显示在窗口中的,当然要保留,加入属性:

        /// <summary>
        /// 资源名称
        /// </summary>
        public string AssetName
        {
            get;
            private set;
        }

5、到这里你就以为完了?不,远远不够,仔细想想,Unity是怎么在编辑器中标记每个资源文件的(以保证他们是独一无二的,就算另一个文件夹中有一模一样的文件,这两个文件也能被区分),对,GUID,这是每个资源文件的唯一标识,也就是身份证,让我们继续加入属性:

        /// <summary>
        /// 资源的GUID(文件夹无效)
        /// </summary>
        public string GUID
        {
            get;
            private set;
        }

6、想想资源的类型我们关不关心?当然也要,加入属性:

        /// <summary>
        /// 资源类型(文件夹无效)
        /// </summary>
        public Type AssetType
        {
            get;
            private set;
        }

7、我们以AssetInfo对象来同时表示资源文件对象和文件夹对象(当然也可以设计继承两个子类来分别抽象),那么在其中就必须得有另一个身份标记来表明当前的对象是资源文件还是文件夹,加入属性:

        /// <summary>
        /// 资源文件类型
        /// </summary>
        public FileType AssetFileType
        {
            get;
            private set;
        }

        /// <summary>
        /// 资源文件类型
        /// </summary>
        public enum FileType
        {
            /// <summary>
            /// 有效的文件资源
            /// </summary>
            ValidFile,
            /// <summary>
            /// 文件夹
            /// </summary>
            Folder,
            /// <summary>
            /// 无效的文件资源
            /// </summary>
            InValidFile
        }

8、这下你以为终于完了?不不,回想一下我们的资源检索窗口是怎样的,让我们在大脑中来一下记忆回放:
这里写图片描述

参考记忆图,我们发现还需要设计一些东西才能达到图中的效果,首先,每个对象的前面有一个勾选框,用来表示这个对象是否被勾选,这个好办,一个bool变量搞定,加入属性:

        /// <summary>
        /// 资源是否勾选
        /// </summary>
        public bool IsChecked
        {
            get;
            set;
        }

9、然后这个勾选框的前面还有一个上下文菜单按钮,也就是那个三角形,当然只有文件夹对象有,用来表示当前文件夹是否展开,这样也好办了,还是一个bool搞定,加入属性:

        /// <summary>
        /// 文件夹是否展开(资源无效)
        /// </summary>
        public bool IsExpanding
        {
            get;
            set;
        }

10、注意到没,有些对象是灰色的,一是无效的对象(.cs等这些无法被打进AB包的对象),这些对象我们可以用AssetFileType区分,回看上面的AssetFileType属性;二是已经打进某个AB包中的资源,毕竟我们不可能已经打了一次包的资源,又给装到另一个包重新打吧,所以又需要加入一个属性,用来表示当前资源所属的AB包,这里用string就行,加入属性:

        /// <summary>
        /// 所属AB包(文件夹无效)
        /// </summary>
        public string Bundled
        {
            get;
            set;
        }

11、这下真的完了?再看一看界面,有没有发现文件夹的下面可以保存其他的资源文件,这个功能我们的设计中好像没有,对,真的没有,想想要怎么做,让每一个资源都存储一个父对象的ID?好是好但我们从父节点开始的正向检索可能会出问题,除非父对象中又存储子对象的ID!好吧,这样设计感觉完全重复累赘了,所以还是直接在每一个对象中加入他子对象的索引就可以了,没有子对象的就为空,加入属性:

        /// <summary>
        /// 文件夹的子资源(资源无效)
        /// </summary>
        public List<AssetInfo> ChildAssetInfo
        {
            get;
            set;
        }

12、我们开始为AssetInfo设计构造方法了,要求是尽可能的让我们传入最少的参数就能构造一个正常的对象,而AssetInfo可以分别表示文件夹和资源文件两种对象,所以我们需要为两种不同的对象分别设计构造方法。

构造一个资源文件对象:

        /// <summary>
        /// 文件类型资源
        /// </summary>
        public AssetInfo(string fullPath, string name, string extension)
        {
            //我们需要这个资源文件的全路径
            AssetFullPath = fullPath;
            //以及经过一些计算之后得到Assets路径
            AssetPath = "Assets" + fullPath.Replace(Application.dataPath.Replace("/", "\\"), "");
            //我们需要这个资源文件的名称
            AssetName = name;
            //我们需要这个资源文件的GUID
            GUID = AssetDatabase.AssetPathToGUID(AssetPath);
            //我们需要这个资源对象的类型,很简单,通过后缀名就可以自己去判断一下,比如xxx后缀名的资源无效
            AssetFileType = AssetBundleTool.GetFileTypeByExtension(extension);
            //我们需要这个资源对象的文件类型
            AssetType = AssetDatabase.GetMainAssetTypeAtPath(AssetPath);
            //默认对象未被勾选
            IsChecked = false;
            //展开的功能对于资源文件是无效的
            IsExpanding = false;
            //默认未绑定任何一个AB包
            Bundled = "";
            //资源文件不存在子资源
            ChildAssetInfo = null;
        }

构造一个文件夹对象:

        /// <summary>
        /// 文件夹类型资源
        /// </summary>
        public AssetInfo(string fullPath, string name, bool isExpanding)
        {
            //我们需要这个文件夹的全路径
            AssetFullPath = fullPath;
            //我们需要这个文件夹的Assets路径
            AssetPath = "Assets" + fullPath.Replace(Application.dataPath.Replace("/", "\\"), "");
            //我们需要这个文件夹的名称
            AssetName = name;
            //文件夹对象不需要GUID了
            GUID = "";
            //设置这是一个文件夹对象
            AssetFileType = FileType.Folder;
            //文件夹对象没有具体的文件类型
            AssetType = null;
            //默认对象未被勾选
            IsChecked = false;
            //构造时设定是否展开文件夹
            IsExpanding = isExpanding;
            //文件夹不需要绑定AB包
            Bundled = "";
            //初始化文件夹的子资源集合
            ChildAssetInfo = new List<AssetInfo>();
        }

好了,AssetInfo类的设计基本完工了,最后我们的AssetInfo类大概就是如下这个样子:

    //【AssetInfo.cs】
    public class AssetInfo
    {
        /// <summary>
        /// 资源全路径
        /// </summary>
        public string AssetFullPath
        {
            get;
            private set;
        }
        /// <summary>
        /// 资源路径
        /// </summary>
        public string AssetPath
        {
            get;
            private set;
        }
        /// <summary>
        /// 资源名称
        /// </summary>
        public string AssetName
        {
            get;
            private set;
        }
        /// <summary>
        /// 资源的GUID(文件夹无效)
        /// </summary>
        public string GUID
        {
            get;
            private set;
        }
        /// <summary>
        /// 资源文件类型
        /// </summary>
        public FileType AssetFileType
        {
            get;
            private set;
        }
        /// <summary>
        /// 资源类型(文件夹无效)
        /// </summary>
        public Type AssetType
        {
            get;
            private set;
        }
        /// <summary>
        /// 资源是否勾选
        /// </summary>
        public bool IsChecked
        {
            get;
            set;
        }
        /// <summary>
        /// 文件夹是否展开(资源无效)
        /// </summary>
        public bool IsExpanding
        {
            get;
            set;
        }
        /// <summary>
        /// 所属AB包(文件夹无效)
        /// </summary>
        public string Bundled
        {
            get;
            set;
        }
        /// <summary>
        /// 文件夹的子资源(资源无效)
        /// </summary>
        public List<AssetInfo> ChildAssetInfo
        {
            get;
            set;
        }

        /// <summary>
        /// 文件夹类型资源
        /// </summary>
        public AssetInfo(string fullPath, string name, bool isExpanding)
        {
            AssetFullPath = fullPath;
            AssetPath = "Assets" + fullPath.Replace(Application.dataPath.Replace("/", "\\"), "");
            AssetName = name;
            GUID = "";
            AssetFileType = FileType.Folder;
            AssetType = null;
            IsChecked = false;
            IsExpanding = isExpanding;
            Bundled = "";
            ChildAssetInfo = new List<AssetInfo>();
        }

        /// <summary>
        /// 文件类型资源
        /// </summary>
        public AssetInfo(string fullPath, string name, string extension)
        {
            AssetFullPath = fullPath;
            AssetPath = "Assets" + fullPath.Replace(Application.dataPath.Replace("/", "\\"), "");
            AssetName = name;
            GUID = AssetDatabase.AssetPathToGUID(AssetPath);
            AssetFileType = AssetBundleTool.GetFileTypeByExtension(extension);
            AssetType = AssetDatabase.GetMainAssetTypeAtPath(AssetPath);
            IsChecked = false;
            IsExpanding = false;
            Bundled = "";
            ChildAssetInfo = null;
        }
    }
}

Assets资源检索:检索资源

开始我们下一个有意义的工作了:AssetInfo类设计好了,接着就是遍历整个Assets路径找到所有资源并创建为AssetInfo对象。

想一想就知道我们必须得设计一个递归函数才能完成这个工作:

        //链接至静态工具类【AssetBundleTool.cs】
        /// <summary>
        /// 读取资源文件夹下的所有子资源
        /// </summary>
        public static void ReadAssetsInChildren(AssetInfo asset)
        {
        }

好了,我优雅的关闭了XX翻译,函数名给整好了。

递归嘛,说白了就是自己调用自己,不懂得先给整出来再说:

        //链接至静态工具类【AssetBundleTool.cs】
        /// <summary>
        /// 读取资源文件夹下的所有子资源
        /// </summary>
        public static void ReadAssetsInChildren(AssetInfo asset)
        {
            //读取子资源
            ReadAssetsInChildren(asset);
        }

我们的ReadAssetsInChildren方法接收一个参数AssetInfo 对象,按照我们上面的设计来说,我们的整个资源检索列表就只是一个AssetInfo 对象,他含有诸多子对象,然后子对象又有子对象,模拟一个类似文件架构的层层结构,如果这个对象不是一个文件夹对象,那他肯定是不存在子对象的,所以就不能继续遍历他的子对象,加一个判断:

        //链接至静态工具类【AssetBundleTool.cs】
        /// <summary>
        /// 读取资源文件夹下的所有子资源
        /// </summary>
        public static void ReadAssetsInChildren(AssetInfo asset)
        {
            //不是文件夹对象,不存在子对象
            if (asset.AssetFileType != FileType.Folder)
            {
                return;
            }

            //读取子资源
            ReadAssetsInChildren(asset);
        }

然后我们可以判断一个对象是否是文件夹了,是文件夹的话,直接遍历文件夹中的内容作为其子对象就可以了。

        //链接至静态工具类【AssetBundleTool.cs】
        /// <summary>
        /// 读取资源文件夹下的所有子资源
        /// </summary>
        public static void ReadAssetsInChildren(AssetInfo asset)
        {
            //不是文件夹对象,不存在子对象
            if (asset.AssetFileType != FileType.Folder)
            {
                return;
            }

            //打开这个文件夹
            DirectoryInfo di = new DirectoryInfo(asset.AssetFullPath);
            //获取其中所有内容,包括文件或子文件夹
            FileSystemInfo[] fileinfo = di.GetFileSystemInfos();
            //遍历这些内容
            foreach (FileSystemInfo fi in fileinfo)
            {
                //如果该内容是文件夹
                if (fi is DirectoryInfo)
                {
                    //判断是否是无效的文件夹,比如是否是Editor,StreamingAssets等,这些文件夹中的东西是无法打进AB包的
                    if (IsValidFolder(fi.Name))
                    {
                        //是合格的文件夹,就创建为文件夹对象,并加入到当前对象的子对象集合
                        AssetInfo ai = new AssetInfo(fi.FullName, fi.Name, false);
                        asset.ChildAssetInfo.Add(ai);

                        //然后继续深层遍历这个文件夹
                        ReadAssetsInChildren(ai);
                    }

                }
                //否则该内容是文件
                else
                {
                    //确保不是.meta文件
                    if (fi.Extension != ".meta")
                    {
                        //是合格的文件,就创建为资源文件对象,并加入到当前对象的子对象集合
                        AssetInfo ai = new AssetInfo(fi.FullName, fi.Name, fi.Extension);
                        asset.ChildAssetInfo.Add(ai);
                    }
                }
            }
        }

好了,在初始化的时候调用一下ReadAssetsInChildren方法,整个Assets路径下的资源就都全数记录下来了,然后我们拥有了一个AssetInfo对象!

Assets资源检索:显示资源列表

资源对象已经拿到手了,下一步就是重点了,将他们显示在窗口中,并且要有这种类似文件夹的层级结构。
好吧,可能你还不知道怎么下手,我们还是先回到上一篇博客中的代码,AssetsGUI,这是我们用来展现整个资源检索列表UI的方法,当时我们写成了这样:

        //【AssetBundleEditor.cs】
        private void AssetsGUI()
        {
        //区域的视图范围:左上角位置固定,宽度为窗口宽度减去左边的区域宽度以及一些空隙(255),高度为窗口高度减去上方两层标题栏以及一些空隙(50)
            _assetViewRect = new Rect(250, 45, (int)position.width - 255, (int)position.height - 50);
            _assetScrollRect = new Rect(250, 45, (int)position.width - 255, _assetViewHeight);


            _assetScroll = GUI.BeginScrollView(_assetViewRect, _assetScroll, _assetScrollRect);
            GUI.BeginGroup(_assetScrollRect, _box);

            //AssetGUI(_asset,0);

            if (_assetViewHeight < _assetViewRect.height)
            {
                _assetViewHeight = (int)_assetViewRect.height;
            }

            GUI.EndGroup();
            GUI.EndScrollView();
        }

很明显我所注释的AssetGUI方法就是所有控件的进一步绘制方法(注意不是AssetsGUI,少一个s),由于我们的资源对象AssetInfo是一个深度未知的复杂结构对象,那么遍历他也得用递归才能搞定了,唰唰唰几下先把函数写出来再说:

        //【AssetBundleEditor.cs】
        /// <summary>
        /// 展示一个资源对象的GUI,indentation为缩进等级,子对象总比父对象大
        /// </summary>
        private void AssetGUI(AssetInfo asset, int indentation)
        {
        }

然后回想我们的界面设计图,每一个对象独自一行:

        //【AssetBundleEditor.cs】
        /// <summary>
        /// 展示一个资源对象的GUI,indentation为缩进等级,子对象总比父对象大
        /// </summary>
        private void AssetGUI(AssetInfo asset, int indentation)
        {
            //开启一行
            GUILayout.BeginHorizontal();
            //以空格缩进
            GUILayout.Space(indentation * 20 + 5);
            //结束一行
            GUILayout.EndHorizontal();
        }

判断是文件夹的话,要创建一个上下文菜单按钮,并且继续深层遍历,是文件的话,根据他是否是无效类型或者已经绑定的资源来决定他是否被禁用:

        //【AssetBundleEditor.cs】
        /// <summary>
        /// 展示一个资源对象的GUI,indentation为缩进等级,子对象总比父对象大
        /// </summary>
        private void AssetGUI(AssetInfo asset, int indentation)
        {
            //开启一行
            GUILayout.BeginHorizontal();
            //以空格缩进
            GUILayout.Space(indentation * 20 + 5);
            //这个资源是文件夹
            if (asset.AssetFileType == FileType.Folder)
            {
                //画一个勾选框
                if (GUILayout.Toggle(asset.IsChecked, "", GUILayout.Width(20)) != asset.IsChecked)
                {
                }
                //获取系统中的文件夹图标
                GUIContent content = EditorGUIUtility.IconContent("Folder Icon");
                content.text = asset.AssetName;
                //创建一个上下文菜单按钮
                asset.IsExpanding = EditorGUILayout.Foldout(asset.IsExpanding, content);
            }
            //否则是文件
            else
            {
                //判断是否禁用
                GUI.enabled = !(asset.AssetFileType == FileType.InValidFile || asset.Bundled != "");
                //画一个勾选框
                if (GUILayout.Toggle(asset.IsChecked, "", GUILayout.Width(20)) != asset.IsChecked)
                {
                }
                //缩进单位10的长度,为了抵消文件夹前面的上下文菜单按钮
                GUILayout.Space(10);
                //根据对象的类型获取他的图标样式
                GUIContent content = EditorGUIUtility.ObjectContent(null, asset.AssetType);
                content.text = asset.AssetName;
                //展示这个对象,以Label控件
                GUILayout.Label(content, GUILayout.Height(20));
                GUI.enabled = true;
                //如果此对象绑定有AB包,就显示这个AB包的名称
                if (asset.Bundled != "")
                {
                    GUILayout.Label("[" + asset.Bundled + "]", _prefabLabel);
                }
            }
            //每一行的高度20,让高度累加
            _assetViewHeight += 20;
            GUILayout.FlexibleSpace();
            //结束一行
            GUILayout.EndHorizontal();

            //如果当前文件夹是展开的,换一行进行深层遍历其子对象,且缩进等级加1
            if (asset.IsExpanding)
            {
                for (int i = 0; i < asset.ChildAssetInfo.Count; i++)
                {
                    AssetGUI(asset.ChildAssetInfo[i], indentation + 1);
                }
            }
        }

这里跳的有点快,但正如代码中注释的那样,其实思路很简单,然后我们将上面的检索资源的方法用在打开窗口初始化的时候调用一次,并将Assets目录作为整个资源结构的根目录,开始扫描文件和创建界面了。

        //【AssetBundleEditor.cs】

        private AssetInfo _asset;

        [MenuItem("Window/AssetBundle Editor %#O")]
        private static void OpenAssetBundleWindow()
        {
            AssetBundleEditor ABEditor = GetWindow<AssetBundleEditor>("AssetBundles");
            ABEditor.Init();
            ABEditor.Show();
        }
        private void Init()
        {
            //以Assets目录创建根对象
            _asset = new AssetInfo(Application.dataPath, "Assets", true);
            //从根对象开始,读取所有文件创建子对象
            AssetBundleTool.ReadAssetsInChildren(_asset);

            Resources.UnloadUnusedAssets();
        }

然后我们在编辑器打开窗口:

这里写图片描述

OK,效果达到了,今天的工作完工,该撤了,下一篇在继续完善!

猜你喜欢

转载自blog.csdn.net/qq992817263/article/details/79928370