【Unity编辑器扩展】字库裁剪工具, 优化字体文件大小,批量修改文本组件字体

原理:

1. 扫描项目中用到的字符集;

2. 把字体文件裁剪掉没用到的字符,仅保留项目中用到的字符;

3. 生成裁剪后的字体文件;

工具功能设计:

1. 支持通过拖拽字体文件或文件夹批量选择需要裁剪的字体文件。

2. 扫描工程中使用到的字符集:主要是获取prefab中Text、TextMeshPro的文本,配置表和数据表中的文本,多语言表的文本以及代码中的字符串。

3. 支持设置基础字符集文件:把需要强制保留的常用的字符集放进文本文件作为基础字符集,可在编辑器界面由用户选择自定义基础字符集文件。

4. 把扫描出的字符集和基础字符集合并,生成裁剪后的字体文件。

功能实现:

1. 字体选择功能参考工具集主界面逻辑:【Unity编辑器扩展】包体优化神器,图片压缩,批量生成图集/图集变体,动画压缩_unity 图片压缩_TopGames的博客-CSDN博客

2. 扫描项目中使用过的字符集,并保存到文件:

 分别扫描prefab、数据表、配置表、多语言表、代码中使用的字符集。

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

需要注意的是,ttf是Unicode编码方式,需要把字符串转为Unicode编码,一个中文占2个字节。把字符串转换为Unicode编码保存在uint[]数组中,并且还需要进行去重。

字符串转换为Unicode编码:

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

把Unicode编码转换为字符串:

/// <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. 使用Aspose.Font库裁剪字体文件:

Aspose.Font是一个支持对字体文件读取、创建、合并、格式转换等操作的.net库,下载后把dll导入Unity即可调用。

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

字体文件裁剪前后对比:

工程中用到字符个数为485个,裁剪后76KB,完整字体9525KB:

 工具代码:

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. 字体批量替换工具

批处理工具,可以把裁剪过的字体一键应用到Text、TextMeshPro组件上。

功能很简单,先设置需要搜索文本组件的Prefab,然后配置要替换成的字体文件。

批处理工具集主编辑器代码基类,主要用于自动把子工具面板显示在主编辑器的工具栏:

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

批处理主编辑器:


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

}

子工具面板基类:

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

字体批量替换子工具:

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

猜你喜欢

转载自blog.csdn.net/final5788/article/details/131685612