Unity editor development (4): actual combat, develop an AB package editor tool

foreword

At the end of our last article ( Unity editor development (3): actual combat, developing an AB package editor tool ), we have an editor window as shown below:

write picture description here

Next, we have to continue to do something meaningful. After all, such an empty frame is really not very beautiful.

Assets resource retrieval: resource object class

Today's task is not very heavy, we only need to complete the resource retrieval interface on the right side of the editor window, so that we can retrieve all the resources in the whole project. then a little more difficult).

First of all, let’s analyze our needs. The Assets directory is simply a layer of folders and files. Any recursion can be traversed. Then we need to draw them on the panel with the same hierarchical relationship. Consider In terms of performance, it is impossible for us to traverse Assets every frame in the code to draw the interface, so we have to traverse once and then save the result. Then, we have to abstract all resources into one type of object, In order to facilitate our storage, here, the resource object class (AssetInfo) was born.

1. Let's create a new class, AssetInfo will be used to represent any resource files (including folders) under the Assets path, etc.

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

2. Then what properties will we set in AssetInfo? Let's think about what properties we care about and what properties we want to keep for a resource file. First of all, we need to keep the full path of the resource file on the hard disk and add the attribute:

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

3. Because the ultimate purpose of the resource is to enter the AB package, we also need to keep the Assets path of the resource file (that is, the path starting from the Assets directory of the current project, don't ask me why, it's such a foolish setting for me I don't know who came up with it), add the attribute:

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

4. Do you need the name of the resource file? What we want to use to display in the window, of course, to keep, add attributes:

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

5. Do you think it's over here? No, it's not enough, think about how Unity tags each resource file in the editor (to ensure that they are unique, even if there is an identical file in another folder, the two files can be distinguished ), yes, GUID, this is the unique identification of each resource file, that is, the ID card, let's continue to add attributes:

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

6. Do we care about the type of resources? Of course, add attributes:

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

7. We use the AssetInfo object to represent the resource file object and the folder object at the same time (of course, two subclasses can also be designed to be abstracted separately), then there must be another identity tag in it to indicate whether the current object is a resource file or Folder, add properties:

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

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

8. Do you think it's over now? No no, recall what our resource retrieval window looks like, let's play a memory replay in our brains:
write picture description here

Referring to the memory map, we found that we still need to design some things to achieve the effect in the figure. First, there is a check box in front of each object to indicate whether the object is checked or not. This is easy to handle, and a bool variable is used. Add property:

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

9. Then there is a context menu button in front of this check box, which is the triangle. Of course, only the folder object has it, which is used to indicate whether the current folder is expanded. :

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

10. Did you notice that some objects are grayed out, one is invalid objects (.cs and other objects that cannot be entered into the AB package), these objects can be distinguished by AssetFileType, look back at the AssetFileType property above; Enter the resources in a certain AB package. After all, we can't have typed the resources of the package once, and then loaded them into another package and typed them again, so we need to add an attribute to indicate the AB package to which the current resource belongs. Just use string here, add attributes:

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

11. Is this really over? Take a look at the interface again, have you found that other resource files can be saved under the folder? We don't seem to have this function in our design, yes, really not, think about how to do it, let each resource store a parent object id? Good is good but our forward search from the parent node can be problematic unless the parent object stores the ID of the child object! Well, this design feels completely repetitive and cumbersome, so you can directly add the index of its sub-object to each object. If there is no sub-object, it will be empty and add attributes:

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

12. We have begun to design a construction method for AssetInfo. The requirement is that we can construct a normal object by passing in the fewest parameters as much as possible, and AssetInfo can represent two kinds of objects: folder and resource file respectively, so we need to construct two kinds of objects. Design and construct methods for different objects respectively.

Construct a resource file object:

        /// <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;
        }

Construct a folder object:

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

Well, the design of the AssetInfo class is basically completed, and finally our AssetInfo class looks like this:

    //【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 resource retrieval: retrieve resources

Let's start our next meaningful work: the AssetInfo class is designed, and then it is to traverse the entire Assets path to find all the resources and create them as AssetInfo objects.

Thinking about it, we have to design a recursive function to do the job:

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

Well, I turned off the XX translation gracefully, and the function name was rectified.

Recursion, to put it bluntly, is to call yourself by yourself. I don’t know how to make it out first and then say:

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

Our ReadAssetsInChildren method receives a parameter AssetInfo object. According to our design above, our entire resource retrieval list is just an AssetInfo object, which contains many sub-objects, and then sub-objects have sub-objects, simulating a file-like structure. Layer-by-layer structure, if this object is not a folder object, then he must have no sub-objects, so he cannot continue to traverse his sub-objects and add a judgment:

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

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

Then we can determine whether an object is a folder. If it is a folder, we can directly traverse the contents of the folder as its child objects.

        //链接至静态工具类【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);
                    }
                }
            }
        }

Well, call the ReadAssetsInChildren method during initialization, all the resources in the entire Assets path are recorded, and then we have an AssetInfo object!

Assets resource retrieval: display resource list

The resource objects are already in hand, the next step is to focus on displaying them in the window, and have this kind of folder-like hierarchy.
Well, maybe you don't know how to start, let's go back to the code in the previous blog, AssetsGUI, which is the method we use to display the entire resource retrieval list UI. At that time, we wrote it like this:

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

Obviously, the AssetGUI method I annotated is the further drawing method of all controls (note that it is not AssetsGUI, one less s), since our resource object AssetInfo is a complex structure object with unknown depth, then traversing it has to be done with recursion. , Swish a few times, write the function first and then say:

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

Then recall our interface design diagram, each object is on its own line:

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

If it is judged to be a folder, create a context menu button and continue to traverse deeply. If it is a file, decide whether it is disabled according to whether it is an invalid type or a bound resource:

        //【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);
                }
            }
        }

The jump here is a bit fast, but as noted in the code, the idea is actually very simple, and then we use the above method of retrieving resources to be called once when opening the window initialization, and use the Assets directory as the root directory of the entire resource structure. It's time to scan files and create screens.

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

Then we open the window in the editor:

write picture description here

OK, the effect has been achieved, today's work is completed, it is time to withdraw, and the next one will continue to improve!

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324522149&siteId=291194637