[Unity editor extension] font cropping tool, optimize font file size, modify text component fonts in batches

principle:

1. Scan the character set used in the project;

2. Cut out the unused characters from the font file, and only keep the characters used in the project;

3. Generate the cropped font file;

Tool function design:

1. Support batch selection of font files to be cropped by dragging font files or folders.

2. Scan the character set used in the project: mainly to obtain the Text and TextMeshPro text in the prefab, the text in the configuration table and data table, the text in the multi-language table, and the strings in the code.

3. Support setting the basic character set file: put the commonly used character set that needs to be reserved into the text file as the basic character set, and the user can select a custom basic character set file in the editor interface.

4. Merge the scanned character set with the basic character set to generate a trimmed font file.

Function realization:

1. The font selection function refers to the main interface logic of the tool set: [Unity editor extension] package body optimization artifact, image compression, batch generation of atlas/atlas variants, animation compression_unity image compression_TopGames' Blog-CSDN Blog

2. Scan the character set used in the project and save it to a file:

 Scan the character sets used in prefab, data table, configuration table, multilingual table, and code respectively.

private void ScanProjectCharSets()
        {
            if (string.IsNullOrWhiteSpace(EditorToolSettings.Instance.FontCroppingCharSetsOutput) || !Directory.Exists(EditorToolSettings.Instance.FontCroppingCharSetsOutput))
            {
                GF.LogWarning("跳过扫描字符集: 字符输出目录为空或目录不存在");
                return;
            }
            StringBuilder strBuilder = new StringBuilder();
            //扫描prefab中文本组件用到的字符
            var prefabGuids = AssetDatabase.FindAssets("t:prefab", new string[] { ConstEditor.PrefabsPath });
            foreach (var guid in prefabGuids)
            {
                var assetPath = AssetDatabase.GUIDToAssetPath(guid);
                var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
                var allTexts = prefab.GetComponentsInChildren<UnityEngine.UI.Text>(true);
                var allTmpTexts = prefab.GetComponentsInChildren<TMPro.TMP_Text>(true);
                foreach (var item in allTexts)
                {
                    if (string.IsNullOrEmpty(item.text)) continue;

                    strBuilder.Append(item.text);
                }
                foreach (var item in allTmpTexts)
                {
                    if (string.IsNullOrEmpty(item.text)) continue;

                    strBuilder.Append(item.text);
                }
            }
            //扫描配置表,数据表,多语言文件中的字符
            var txtFiles = new List<string>();
            var configs = Directory.GetFiles(ConstEditor.GameConfigPath, "*.txt");
            if (configs.Length > 0) txtFiles.AddRange(configs);

            var dataTables = Directory.GetFiles(ConstEditor.DataTablePath, "*.txt");
            if (dataTables.Length > 0) txtFiles.AddRange(dataTables);

            var languages = Directory.GetFiles(ConstEditor.LanguagePath, "*.json");
            if (languages.Length > 0) txtFiles.AddRange(languages);

            foreach (var item in txtFiles)
            {
                var text = File.ReadAllText(item, Encoding.UTF8);
                if (string.IsNullOrEmpty(text)) continue;
                strBuilder.Append(text);
            }

            //扫描代码中使用的字符
            var scriptGuids = AssetDatabase.FindAssets("t:script", new string[] { Path.GetDirectoryName(ConstEditor.HotfixAssembly), Path.GetDirectoryName(ConstEditor.BuiltinAssembly) });
            string charsetsPattern = "\"(.*?)\"";

            foreach (var item in scriptGuids)
            {
                var assetPath = AssetDatabase.GUIDToAssetPath(item);
                if (Path.GetExtension(assetPath).ToLower() != ".cs") continue;
                var codeTxt = File.ReadAllText(assetPath);
                MatchCollection matches = Regex.Matches(codeTxt, charsetsPattern);
                foreach (Match match in matches)
                {
                    string text = match.Groups[1].Value;
                    if (string.IsNullOrEmpty(text)) continue;
                    strBuilder.Append(text);
                }
            }
            var resultFile = UtilityBuiltin.ResPath.GetCombinePath(EditorToolSettings.Instance.FontCroppingCharSetsOutput, CharSetsFile);
            var result = strBuilder.ToString();
            var unicodeCharSets = String2UnicodeCharSets(result);
            unicodeCharSets = unicodeCharSets.Distinct().ToArray();

            result = UnicodeCharSets2String(unicodeCharSets);
            File.WriteAllText(resultFile, result, Encoding.UTF8);
            GF.LogInfo($"扫描字符集完成,共[{unicodeCharSets.Length}]个字符. 已保存到字符集文件:{resultFile}");
        }

It should be noted that ttf is a Unicode encoding method, and the string needs to be converted into Unicode encoding, and a Chinese character occupies 2 bytes. Convert the string to Unicode and store it in the uint[] array, and deduplication is also required.

String conversion to Unicode encoding:

/// <summary>
        /// 把字符串转换为
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        private static uint[] String2UnicodeCharSets(string str)
        {
            var bytesDt = System.Text.Encoding.Unicode.GetBytes(str);
            uint[] charSets = new uint[bytesDt.Length / System.Text.UnicodeEncoding.CharSize];
            for (int idx = 0, i = 0; i < bytesDt.Length; i += System.Text.UnicodeEncoding.CharSize)
            {
                charSets[idx++] = BitConverter.ToUInt16(bytesDt, i);
            }
            return charSets;
        }

Convert Unicode encoding to string:

/// <summary>
        /// 把Unicode数值转换为字符串
        /// </summary>
        /// <param name="charsets"></param>
        /// <returns></returns>
        private static string UnicodeCharSets2String(uint[] charsets)
        {
            StringBuilder strBuilder = new StringBuilder();
            for (int i = 0; i < charsets.Length; i++)
            {
                var unicodeChar = char.ConvertFromUtf32((int)charsets[i]);
                strBuilder.Append(unicodeChar);
            }
            return strBuilder.ToString();
        }

3. Use the Aspose.Font library to crop the font file:

Aspose.Font is a .net library that supports operations such as reading, creating, merging, and format conversion of font files. After downloading, import the dll into Unity to call it.

/// <summary>
        /// 裁剪字体
        /// </summary>
        /// <param name="ttf"></param>
        /// <param name="unicodeCharSets"></param>
        /// <returns></returns>
        public static bool CroppingFont(string ttf, uint[] unicodeCharSets)
        {
            if (Path.GetExtension(ttf).ToLower() != ".ttf")
            {
                Debug.LogWarning($"生成裁剪字体[{ttf}]失败:只支持裁剪ttf格式字体");
                return false;
            }
            try
            {
                var font = Aspose.Font.Font.Open(Aspose.Font.FontType.TTF, ttf) as TtfFont;
                var merger = HelpersFactory.GetFontCharactersMerger(font, font);
                var charsets = unicodeCharSets.Distinct().ToArray();
                var newFont = merger.MergeFonts(charsets, new uint[0], font.FontName);

                var newTtf = GenerateNewFontFileName(ttf);
                newFont.Save(newTtf);
                AssetDatabase.Refresh();
                return true;
            }
            catch (Exception e)
            {
                Debug.LogWarning($"生成裁剪字体[{ttf}]失败:{e.Message}");
                return false;
            }
        }

Comparison of font files before and after cropping:

The number of characters used in the project is 485, 76KB after cropping, and the complete font is 9525KB:

 Tool code:

using Aspose.Font.Ttf;
using Aspose.Font.TtfHelpers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

namespace UGF.EditorTools
{
    [EditorToolMenu("字体裁剪", typeof(CompressToolEditor), 6)]
    public class FontMinifyPanel : CompressToolSubPanel
    {
        /// <summary>
        /// 扫描出的字符保存到文件
        /// </summary>
        const string CharSetsFile = "CharSets_ScanFromProject.txt";
        public override string AssetSelectorTypeFilter => "t:font t:folder";

        public override string DragAreaTips => "拖拽添加字体文件/文件夹";
        public override string ReadmeText => "自动扫描项目中使用的字符,裁剪掉字体资源中未使用的字符";

        protected override Type[] SupportAssetTypes => new Type[] { typeof(UnityEngine.Font) };

        public override void DrawBottomButtonsPanel()
        {
            EditorGUILayout.BeginHorizontal("box");
            {
                var layoutHeight = GUILayout.Height(30);
                if (GUILayout.Button("扫描字符", layoutHeight))
                {
                    ScanProjectCharSets();
                }
                if (GUILayout.Button("裁剪字体", layoutHeight))
                {
                    GenerateMinifyFont();
                }
                if (GUILayout.Button("扫描并裁剪", layoutHeight))
                {
                    ScanAndGenerateMinifyFont();
                }
                EditorGUILayout.EndHorizontal();
            }
        }


        public override void DrawSettingsPanel()
        {
            EditorGUILayout.BeginVertical("box");
            {
                EditorGUILayout.BeginHorizontal();
                {
                    EditorGUILayout.LabelField("扫描字符集输出:", GUILayout.Width(100));
                    EditorGUILayout.LabelField(EditorToolSettings.Instance.FontCroppingCharSetsOutput, EditorStyles.selectionRect);
                    if (GUILayout.Button("选择路径", GUILayout.Width(100)))
                    {
                        EditorToolSettings.Instance.FontCroppingCharSetsOutput = EditorUtilityExtension.OpenRelativeFolderPanel("选择字符集保存目录", EditorToolSettings.Instance.FontCroppingCharSetsOutput);
                    }
                    EditorGUILayout.EndHorizontal();
                }
                EditorGUILayout.BeginHorizontal();
                {
                    EditorGUILayout.LabelField("基础字符集文件:", GUILayout.Width(100));
                    EditorGUILayout.LabelField(EditorToolSettings.Instance.FontCroppingCharSetsFile, EditorStyles.selectionRect);
                    if (GUILayout.Button("选择文件", GUILayout.Width(100)))
                    {
                        EditorToolSettings.Instance.FontCroppingCharSetsFile = EditorUtilityExtension.OpenRelativeFilePanel("选择字符集文件", EditorToolSettings.Instance.FontCroppingCharSetsFile, "txt");
                    }
                    EditorGUILayout.EndHorizontal();
                }
                EditorGUILayout.EndVertical();
            }
        }

        private void ScanAndGenerateMinifyFont()
        {
            ScanProjectCharSets();
            GenerateMinifyFont();
        }

        private void ScanProjectCharSets()
        {
            if (string.IsNullOrWhiteSpace(EditorToolSettings.Instance.FontCroppingCharSetsOutput) || !Directory.Exists(EditorToolSettings.Instance.FontCroppingCharSetsOutput))
            {
                GF.LogWarning("跳过扫描字符集: 字符输出目录为空或目录不存在");
                return;
            }
            StringBuilder strBuilder = new StringBuilder();
            //扫描prefab中文本组件用到的字符
            var prefabGuids = AssetDatabase.FindAssets("t:prefab", new string[] { ConstEditor.PrefabsPath });
            foreach (var guid in prefabGuids)
            {
                var assetPath = AssetDatabase.GUIDToAssetPath(guid);
                var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
                var allTexts = prefab.GetComponentsInChildren<UnityEngine.UI.Text>(true);
                var allTmpTexts = prefab.GetComponentsInChildren<TMPro.TMP_Text>(true);
                foreach (var item in allTexts)
                {
                    if (string.IsNullOrEmpty(item.text)) continue;

                    strBuilder.Append(item.text);
                }
                foreach (var item in allTmpTexts)
                {
                    if (string.IsNullOrEmpty(item.text)) continue;

                    strBuilder.Append(item.text);
                }
            }
            //扫描配置表,数据表,多语言文件中的字符
            var txtFiles = new List<string>();
            var configs = Directory.GetFiles(ConstEditor.GameConfigPath, "*.txt");
            if (configs.Length > 0) txtFiles.AddRange(configs);

            var dataTables = Directory.GetFiles(ConstEditor.DataTablePath, "*.txt");
            if (dataTables.Length > 0) txtFiles.AddRange(dataTables);

            var languages = Directory.GetFiles(ConstEditor.LanguagePath, "*.json");
            if (languages.Length > 0) txtFiles.AddRange(languages);

            foreach (var item in txtFiles)
            {
                var text = File.ReadAllText(item, Encoding.UTF8);
                if (string.IsNullOrEmpty(text)) continue;
                strBuilder.Append(text);
            }

            //扫描代码中使用的字符
            var scriptGuids = AssetDatabase.FindAssets("t:script", new string[] { Path.GetDirectoryName(ConstEditor.HotfixAssembly), Path.GetDirectoryName(ConstEditor.BuiltinAssembly) });
            string charsetsPattern = "\"(.*?)\"";

            foreach (var item in scriptGuids)
            {
                var assetPath = AssetDatabase.GUIDToAssetPath(item);
                if (Path.GetExtension(assetPath).ToLower() != ".cs") continue;
                var codeTxt = File.ReadAllText(assetPath);
                MatchCollection matches = Regex.Matches(codeTxt, charsetsPattern);
                foreach (Match match in matches)
                {
                    string text = match.Groups[1].Value;
                    if (string.IsNullOrEmpty(text)) continue;
                    strBuilder.Append(text);
                }
            }
            var resultFile = UtilityBuiltin.ResPath.GetCombinePath(EditorToolSettings.Instance.FontCroppingCharSetsOutput, CharSetsFile);
            var result = strBuilder.ToString();
            var unicodeCharSets = String2UnicodeCharSets(result);
            unicodeCharSets = unicodeCharSets.Distinct().ToArray();

            result = UnicodeCharSets2String(unicodeCharSets);
            File.WriteAllText(resultFile, result, Encoding.UTF8);
            GF.LogInfo($"扫描字符集完成,共[{unicodeCharSets.Length}]个字符. 已保存到字符集文件:{resultFile}");
        }
        private void GenerateMinifyFont()
        {
            var fontAsssts = this.GetSelectedAssets();
            if (fontAsssts.Count < 1)
            {
                GF.LogWarning($"请先把需要裁剪的字体资源添加到列表");
                return;
            }
            var projRoot = Directory.GetParent(Application.dataPath).FullName;
            var charSetString = GetCharSetStringFromFiles();
            if (string.IsNullOrWhiteSpace(charSetString))
            {
                GF.LogWarning($"要裁剪的字符集为空, 请设置字符集文件或检查字符集内容");
                return;
            }
            var unicodeCharSets = String2UnicodeCharSets(charSetString);
            GF.LogInfo($"字符集包含字符个数:{unicodeCharSets.Length}");
            foreach (var asset in fontAsssts)
            {
                var fontFile = Path.GetFullPath(asset, projRoot);
                if (CroppingFont(fontFile, unicodeCharSets))
                {
                    GF.LogInfo($"生成裁剪字体成功:{fontFile}");
                }
            }

        }

        private string GetCharSetStringFromFiles()
        {
            StringBuilder strBuilder = new StringBuilder();
            if (!string.IsNullOrWhiteSpace(EditorToolSettings.Instance.FontCroppingCharSetsOutput))
            {
                var projCharsFile = UtilityBuiltin.ResPath.GetCombinePath(EditorToolSettings.Instance.FontCroppingCharSetsOutput, CharSetsFile);
                if (File.Exists(projCharsFile))
                {
                    var str = File.ReadAllText(projCharsFile);
                    strBuilder.Append(str);
                }
            }
            if (!string.IsNullOrWhiteSpace(EditorToolSettings.Instance.FontCroppingCharSetsFile) && File.Exists(EditorToolSettings.Instance.FontCroppingCharSetsFile))
            {
                var str = File.ReadAllText(EditorToolSettings.Instance.FontCroppingCharSetsFile);
                strBuilder.Append(str);
            }
            return strBuilder.ToString();
        }
        /// <summary>
        /// 裁剪字体
        /// </summary>
        /// <param name="ttf"></param>
        /// <param name="unicodeCharSets"></param>
        /// <returns></returns>
        public static bool CroppingFont(string ttf, uint[] unicodeCharSets)
        {
            if (Path.GetExtension(ttf).ToLower() != ".ttf")
            {
                Debug.LogWarning($"生成裁剪字体[{ttf}]失败:只支持裁剪ttf格式字体");
                return false;
            }
            try
            {
                var font = Aspose.Font.Font.Open(Aspose.Font.FontType.TTF, ttf) as TtfFont;
                var merger = HelpersFactory.GetFontCharactersMerger(font, font);
                var charsets = unicodeCharSets.Distinct().ToArray();
                var newFont = merger.MergeFonts(charsets, new uint[0], font.FontName);

                var newTtf = GenerateNewFontFileName(ttf);
                newFont.Save(newTtf);
                AssetDatabase.Refresh();
                return true;
            }
            catch (Exception e)
            {
                Debug.LogWarning($"生成裁剪字体[{ttf}]失败:{e.Message}");
                return false;
            }
        }
        /// <summary>
        /// 根据字符集裁剪字体
        /// </summary>
        /// <param name="ttf"></param>
        /// <param name="charSets"></param>
        public static bool CroppingFont(string ttf, string charSets)
        {
            var unicodeChars = String2UnicodeCharSets(charSets);
            return CroppingFont(ttf, unicodeChars);
        }
        private static string GenerateNewFontFileName(string ttf)
        {
            var newFontSavePath = Path.GetFullPath(Path.GetDirectoryName(ttf), Directory.GetParent(Application.dataPath).FullName);
            var newFontFileName = Path.GetFileNameWithoutExtension(ttf) + "_mini";
            var newFontExt = Path.GetExtension(ttf);
            var newTtf = UtilityBuiltin.ResPath.GetCombinePath(newFontSavePath, newFontFileName + newFontExt);
            return newTtf;
        }
        /// <summary>
        /// 把字符串转换为
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        private static uint[] String2UnicodeCharSets(string str)
        {
            var bytesDt = System.Text.Encoding.Unicode.GetBytes(str);
            uint[] charSets = new uint[bytesDt.Length / System.Text.UnicodeEncoding.CharSize];
            for (int idx = 0, i = 0; i < bytesDt.Length; i += System.Text.UnicodeEncoding.CharSize)
            {
                charSets[idx++] = BitConverter.ToUInt16(bytesDt, i);
            }
            return charSets;
        }
        /// <summary>
        /// 把Unicode数值转换为字符串
        /// </summary>
        /// <param name="charsets"></param>
        /// <returns></returns>
        private static string UnicodeCharSets2String(uint[] charsets)
        {
            StringBuilder strBuilder = new StringBuilder();
            for (int i = 0; i < charsets.Length; i++)
            {
                var unicodeChar = char.ConvertFromUtf32((int)charsets[i]);
                strBuilder.Append(unicodeChar);
            }
            return strBuilder.ToString();
        }
    }
}

4. Batch font replacement tool

A batch processing tool that can apply the cropped font to Text and TextMeshPro components with one click.

The function is very simple, first set the Prefab that needs to search for the text component, and then configure the font file to be replaced.

The code base class of the main editor of the batch tool set, mainly used to automatically display the sub-tool panel on the toolbar of the main editor:

using GameFramework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Unity.VisualScripting;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;

namespace UGF.EditorTools
{
    /// <summary>
    /// 批处理操作工具
    /// </summary>
    public abstract class UtilityToolEditorBase : EditorToolBase
    {
        //public override string ToolName => "批处理工具集";
        public override Vector2Int WinSize => new Vector2Int(600, 800);

        GUIStyle centerLabelStyle;
        ReorderableList srcScrollList;
        Vector2 srcScrollPos;

        private int SelectOjbWinId => this.GetType().GetHashCode();
        private bool settingFoldout = true;

        List<Type> subPanelsClass;
        string[] subPanelTitles;
        UtilitySubToolBase[] subPanels;
        UtilitySubToolBase curPanel;
        private int mCompressMode;
        private List<UnityEngine.Object> selectList;

        private void OnEnable()
        {
            selectList = new List<UnityEngine.Object>();
            subPanelsClass = new List<Type>();
            centerLabelStyle = new GUIStyle();
            centerLabelStyle.alignment = TextAnchor.MiddleCenter;
            centerLabelStyle.fontSize = 25;
            centerLabelStyle.normal.textColor = Color.gray;

            srcScrollList = new ReorderableList(selectList, typeof(UnityEngine.Object), true, true, true, true);
            srcScrollList.drawHeaderCallback = DrawScrollListHeader;
            srcScrollList.onAddCallback = AddItem;
            srcScrollList.drawElementCallback = DrawItems;
            srcScrollList.multiSelect = true;
            ScanSubPanelClass();

            SwitchSubPanel(0);
        }


        private void ScanSubPanelClass()
        {
            subPanelsClass.Clear();
            var editorDll = Utility.Assembly.GetAssemblies().First(dll => dll.GetName().Name.CompareTo("Assembly-CSharp-Editor") == 0);
            var allEditorTool = editorDll.GetTypes().Where(tp => (tp.IsSubclassOf(typeof(UtilitySubToolBase)) && tp.HasAttribute<EditorToolMenuAttribute>() && tp.GetCustomAttribute<EditorToolMenuAttribute>().OwnerType == this.GetType()));

            subPanelsClass.AddRange(allEditorTool);
            subPanelsClass.Sort((x, y) =>
            {
                int xOrder = x.GetCustomAttribute<EditorToolMenuAttribute>().MenuOrder;
                int yOrder = y.GetCustomAttribute<EditorToolMenuAttribute>().MenuOrder;
                return xOrder.CompareTo(yOrder);
            });

            subPanels = new UtilitySubToolBase[subPanelsClass.Count];
            subPanelTitles = new string[subPanelsClass.Count];
            for (int i = 0; i < subPanelsClass.Count; i++)
            {
                var toolAttr = subPanelsClass[i].GetCustomAttribute<EditorToolMenuAttribute>();
                subPanelTitles[i] = toolAttr.ToolMenuPath;
            }
        }
        private void OnDisable()
        {
            foreach (var panel in subPanels)
            {
                panel?.OnExit();
            }
        }

        private void OnGUI()
        {
            if (curPanel == null) return;
            EditorGUILayout.BeginVertical();
            EditorGUILayout.BeginHorizontal("box");
            {
                EditorGUI.BeginChangeCheck();
                mCompressMode = GUILayout.Toolbar(mCompressMode, subPanelTitles, GUILayout.Height(30));
                if (EditorGUI.EndChangeCheck())
                {
                    SwitchSubPanel(mCompressMode);
                }
                EditorGUILayout.EndHorizontal();
            }
            srcScrollPos = EditorGUILayout.BeginScrollView(srcScrollPos);
            srcScrollList.DoLayoutList();
            EditorGUILayout.EndScrollView();
            DrawDropArea();
            EditorGUILayout.Space(10);
            if (settingFoldout = EditorGUILayout.Foldout(settingFoldout, "展开设置项:"))
            {
                curPanel.DrawSettingsPanel();
            }
            curPanel.DrawBottomButtonsPanel();
            EditorGUILayout.EndVertical();
        }


        /// <summary>
        /// 绘制拖拽添加文件区域
        /// </summary>
        private void DrawDropArea()
        {
            var dragRect = EditorGUILayout.BeginVertical("box");
            {
                GUILayout.FlexibleSpace();
                EditorGUILayout.LabelField(curPanel.DragAreaTips, centerLabelStyle, GUILayout.MinHeight(200));
                if (dragRect.Contains(UnityEngine.Event.current.mousePosition))
                {
                    if (UnityEngine.Event.current.type == EventType.DragUpdated)
                    {
                        DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
                    }
                    else if (UnityEngine.Event.current.type == EventType.DragExited)
                    {
                        if (DragAndDrop.objectReferences != null && DragAndDrop.objectReferences.Length > 0)
                        {
                            OnItemsDrop(DragAndDrop.objectReferences);
                        }

                    }
                }
                GUILayout.FlexibleSpace();
                EditorGUILayout.EndVertical();
            }
        }

        /// <summary>
        /// 拖拽松手
        /// </summary>
        /// <param name="objectReferences"></param>
        /// <exception cref="NotImplementedException"></exception>
        private void OnItemsDrop(UnityEngine.Object[] objectReferences)
        {
            foreach (var item in objectReferences)
            {
                var itemPath = AssetDatabase.GetAssetPath(item);
                if (curPanel.GetSelectedItemType(itemPath) == ItemType.NoSupport)
                {
                    Debug.LogWarningFormat("添加失败! 不支持的文件格式:{0}", itemPath);
                    continue;
                }
                AddItem(item);
            }
        }
        private void AddItem(UnityEngine.Object obj)
        {
            if (obj == null || selectList.Contains(obj)) return;

            selectList.Add(obj);
        }

        private void DrawItems(Rect rect, int index, bool isActive, bool isFocused)
        {
            var item = selectList[index];
            EditorGUI.ObjectField(rect, item, typeof(UnityEngine.Object), false);
        }

        private void DrawScrollListHeader(Rect rect)
        {
            if (GUI.Button(rect, "清除列表"))
            {
                selectList?.Clear();
            }
        }
        private void OnSelectAsset(UnityEngine.Object obj)
        {
            AddItem(obj);
        }

        private void AddItem(ReorderableList list)
        {
            if (!EditorUtilityExtension.OpenAssetSelector(typeof(UnityEngine.Object), curPanel.AssetSelectorTypeFilter, OnSelectAsset, SelectOjbWinId))
            {
                Debug.LogWarning("打开资源选择界面失败!");
            }
        }

        private void SwitchSubPanel(int panelIdx)
        {
            if (subPanelsClass.Count <= 0) return;
            mCompressMode = Mathf.Clamp(panelIdx, 0, subPanelsClass.Count);
            this.titleContent.text = subPanelTitles[mCompressMode];
            if (curPanel != null)
            {
                curPanel.OnExit();
            }

            if (subPanels[mCompressMode] != null)
            {
                curPanel = subPanels[mCompressMode];
            }
            else
            {
                curPanel = subPanels[mCompressMode] = Activator.CreateInstance(subPanelsClass[mCompressMode], new object[] { this }) as UtilitySubToolBase;
            }

            curPanel.OnEnter();
        }

        /// <summary>
        /// 获取当前选择的资源文件列表
        /// </summary>
        /// <returns></returns>
        public List<string> GetSelectedAssets()
        {
            return curPanel.FilterSelectedAssets(selectList);
        }
    }
}

Batch main editor:


namespace UGF.EditorTools
{
    [EditorToolMenu("资源/批处理工具集", null, 4)]
    public class BatchOperateToolEditor : UtilityToolEditorBase
    {
        public override string ToolName => "批处理工具集";
    }

}

Sub tool panel base class:

using GameFramework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;

namespace UGF.EditorTools
{
    public abstract class UtilitySubToolBase
    {
        protected UtilityToolEditorBase OwnerEditor { get; private set; }
        public abstract string AssetSelectorTypeFilter { get; }//"t:sprite t:texture2d t:folder"
        public abstract string DragAreaTips { get; }
        protected abstract Type[] SupportAssetTypes { get; }

        public UtilitySubToolBase(UtilityToolEditorBase ownerEditor)
        {
            OwnerEditor = ownerEditor;
        }

        public virtual void OnEnter() { }
        public virtual void OnExit() { SaveSettings(); }
        public abstract void DrawSettingsPanel();
        public abstract void DrawBottomButtonsPanel();
        /// <summary>
        /// 通过AssetDatabase判断是否支持, 注意如果是Assets之外的文件判断需要重写此方法
        /// </summary>
        /// <param name="assetPath"></param>
        /// <returns></returns>
        public virtual bool IsSupportAsset(string assetPath)
        {
            var assetType = AssetDatabase.GetMainAssetTypeAtPath(assetPath);
            return SupportAssetTypes.Contains(assetType);
        }

        /// <summary>
        /// 获取当前选择的资源文件列表
        /// </summary>
        /// <returns></returns>
        public virtual List<string> FilterSelectedAssets(List<UnityEngine.Object> selectedObjs)
        {
            List<string> images = new List<string>();
            foreach (var item in selectedObjs)
            {
                if (item == null) continue;

                var assetPath = AssetDatabase.GetAssetPath(item);
                var itmTp = GetSelectedItemType(assetPath);
                if (itmTp == ItemType.File)
                {
                    string imgFileName = Utility.Path.GetRegularPath(assetPath);
                    if (IsSupportAsset(imgFileName) && !images.Contains(imgFileName))
                    {
                        images.Add(imgFileName);
                    }
                }
                else if (itmTp == ItemType.Folder)
                {
                    string imgFolder = AssetDatabase.GetAssetPath(item);
                    var assets = AssetDatabase.FindAssets(GetFindAssetsFilter(), new string[] { imgFolder });
                    for (int i = assets.Length - 1; i >= 0; i--)
                    {
                        assets[i] = AssetDatabase.GUIDToAssetPath(assets[i]);
                    }
                    images.AddRange(assets);
                }
            }

            return images.Distinct().ToList();//把结果去重处理
        }
        protected string GetFindAssetsFilter()
        {
            string filter = "";
            foreach (var item in SupportAssetTypes)
            {
                filter += $"t:{item.Name} ";
            }
            filter.Trim(' ');
            return filter;
        }
        public virtual void SaveSettings()
        {
            if (EditorToolSettings.Instance)
            {
                EditorToolSettings.Save();
            }
        }

        internal ItemType GetSelectedItemType(string assetPath)
        {
            if (string.IsNullOrEmpty(assetPath)) return ItemType.NoSupport;

            if ((File.GetAttributes(assetPath) & FileAttributes.Directory) == FileAttributes.Directory) return ItemType.Folder;

            if (IsSupportAsset(assetPath)) return ItemType.File;

            return ItemType.NoSupport;
        }
    }
}

Font batch replacement sub-tool:

using System;
using TMPro;
using UnityEditor;
using UnityEngine;

namespace UGF.EditorTools
{
    [EditorToolMenu("替换字体", typeof(BatchOperateToolEditor), 0)]
    public class FontReplaceTool : UtilitySubToolBase
    {
        public override string AssetSelectorTypeFilter => "t:prefab t:folder";

        public override string DragAreaTips => "拖拽添加Prefab文件或文件夹";

        protected override Type[] SupportAssetTypes => new Type[] { typeof(GameObject) };


        UnityEngine.Font textFont;
        TMP_FontAsset tmpFont;
        TMP_SpriteAsset tmpFontSpriteAsset;
        TMP_StyleSheet tmpFontStyleSheet;

        public FontReplaceTool(BatchOperateToolEditor ownerEditor) : base(ownerEditor)
        {
        }

        public override void DrawBottomButtonsPanel()
        {
            if (GUILayout.Button("一键替换", GUILayout.Height(30)))
            {
                ReplaceFont();
            }
        }


        public override void DrawSettingsPanel()
        {
            EditorGUILayout.BeginHorizontal("box");
            {
                textFont = EditorGUILayout.ObjectField("Text字体替换:", textFont, typeof(UnityEngine.Font), false) as UnityEngine.Font;
                EditorGUILayout.EndHorizontal();
            }
            EditorGUILayout.BeginVertical("box");
            {
                tmpFont = EditorGUILayout.ObjectField("TextMeshPro字体替换:", tmpFont, typeof(TMP_FontAsset), false) as TMP_FontAsset;
                tmpFontSpriteAsset = EditorGUILayout.ObjectField("Sprite Asset替换:", tmpFontSpriteAsset, typeof(TMP_SpriteAsset), false) as TMP_SpriteAsset;
                tmpFontStyleSheet = EditorGUILayout.ObjectField("Style Sheet替换:", tmpFontStyleSheet, typeof(TMP_StyleSheet), false) as TMP_StyleSheet;

                EditorGUILayout.EndVertical();
            }
        }


        private void ReplaceFont()
        {
            var prefabs = OwnerEditor.GetSelectedAssets();
            if (prefabs == null || prefabs.Count < 1) return;

            int taskIdx = 0;
            int totalTaskCount = prefabs.Count;
            bool batTmpfont = tmpFont != null || tmpFontSpriteAsset != null || tmpFontStyleSheet != null;
            foreach (var item in prefabs)
            {
                var pfb = AssetDatabase.LoadAssetAtPath<GameObject>(item); //PrefabUtility.LoadPrefabContents(item);
                if (pfb == null) continue;
                EditorUtility.DisplayProgressBar($"进度({taskIdx++}/{totalTaskCount})", item, taskIdx / (float)totalTaskCount);
                bool hasChanged = false;
                if (textFont != null)
                {
                    foreach (var textCom in pfb.GetComponentsInChildren<UnityEngine.UI.Text>(true))
                    {
                        textCom.font = textFont;
                        hasChanged = true;
                    }
                }
                if (batTmpfont)
                {
                    foreach (var tmpTextCom in pfb.GetComponentsInChildren<TMPro.TMP_Text>(true))
                    {
                        if (tmpFont != null) tmpTextCom.font = tmpFont;
                        if (tmpFontSpriteAsset != null) tmpTextCom.spriteAsset = tmpFontSpriteAsset;
                        if (tmpFontStyleSheet != null) tmpTextCom.styleSheet = tmpFontStyleSheet;
                        hasChanged = true;
                    }
                }
                if (hasChanged)
                {
                    PrefabUtility.SavePrefabAsset(pfb);
                }
            }
            EditorUtility.ClearProgressBar();
        }
    }
}

Guess you like

Origin blog.csdn.net/final5788/article/details/131685612