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

news2024/11/24 9:20:07

原理:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/747271.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络安全设备Bypass功能介绍及分析

网络安全平台厂商往往需要用到一项比较特殊的技术&#xff0c;那就是Bypass&#xff0c;那么到底什么是Bypass呢&#xff0c;Bypass设备又是如何来实现的&#xff1f;下面我就对Bypass技术做一下简单的介绍和说明。 一、 什么是Bypass。 大家知道&#xff0c;网络安全设备一般…

mac安装Golang开发环境及入门

目录 一、Mac brew 安装go环境 1.1 安装步骤 1.2 设置GOPATH 及环境变量 1.3 编写第一个go程序 二、快速入门 1.1 快速入门需求 1.2 go学习&#xff08;自用&#xff09; 一、Mac brew 安装go环境 1.1 安装步骤 1&#xff09;终端输入&#xff0c;也可以指定下载go版本…

SPSSAU方差分析+python

准备数据 将数据格式调整为以下格式&#xff1a; jupyter处理过程 #读取数据 import numpy as np import pandas as pd# 创建一个空的DataFrame t1 pd.DataFrame() t2 pd.DataFrame() t3 pd.DataFrame() T1pd.read_excel(./数据/抑郁_T1.xlsx)T1.columnsT1.iloc[0] T1T1…

模板类与继承

模板类与继承 模板类继承普通类普通类继承模板类的实例化版本。普通类继承模板类模板类继承模板类模板类继承模板参数给出的基类 模板类继承普通类 基类 派生类 测试函数; 普通类继承模板类的实例化版本。 模板基类 普通类继承模板基类的实例化版本&#xff1a; 普通…

PROFINET转DeviceNet网关devicenet通讯模块

远创智控YC-DNT-PN这款神器&#xff0c;连接PROFINET和DeviceNet网络&#xff0c;让两边数据轻松传输。 这个网关不仅从ETHERNET/IP和DEVICENET一侧读写数据&#xff0c;还可以将缓冲区数据交换&#xff0c;这样就可以在两个网络之间愉快地传递数据了&#xff01;而且&#xff…

云计算的学习(三)

三、云计算中的网络基础知识 3.1虚拟化中网络的架构 a.虚拟化中网络的架构 二层交换机作为接入交换机使用&#xff0c;三层交换机可以作为汇聚交换机或核心交换机&#xff0c;在抛开网络安全设备时&#xff0c;路由器直接连接在互联网上。 b.广播和单播 物理服务器内部主要…

【单片机】MSP430单片机,msp430f5529,DHT11 温湿度检测仪,上限报警,单击双击判定,OLED

文章目录 功能接线示意图效果图原理 功能 硬件 IIC OLED 0.96寸 无源蜂鸣器 低电平触发 DHT11 温湿度传感器 板子上的2个按键 板子上的2个灯 功能&#xff1a; 1 显示温湿度 2 按键单击双击识别 3 按键修改温湿度&#xff0c;双击选择某一个设置项目&#xff0c;单击进行加或…

Python(五):print函数详解

❤️ 专栏简介&#xff1a;本专栏记录了我个人从零开始学习Python编程的过程。在这个专栏中&#xff0c;我将分享我在学习Python的过程中的学习笔记、学习路线以及各个知识点。 ☀️ 专栏适用人群 &#xff1a;本专栏适用于希望学习Python编程的初学者和有一定编程基础的人。无…

Redis 高频面试题 2023 最新版

Redis 高频面试题 2023 最新版 文章目录 Redis 高频面试题 2023 最新版一、Redis缓存相关1. 什么是缓存穿透&#xff1f;如何解决2. 什么是缓存击穿&#xff1f;如何解决 一、Redis缓存相关 1. 什么是缓存穿透&#xff1f;如何解决 是什么 缓存穿透就是根据某条件查询一个数据…

C++STL:顺序容器之forward_list

文章目录 1. 概述2. 成员函数和使用forward_list容器相关的函数 3. forward_list 容器的创建 1. 概述 forward_list 是 C 11 新添加的一类容器&#xff0c;其底层实现和 list 容器一样&#xff0c;采用的也是链表结构&#xff0c;只不过 forward_list 使用的是单链表&#xff…

解决ubuntu cuda版本nvcc -V和nvidia-smi不一致问题

在使用nvcc -V和nvidia-smi查看cuda版本时不一致&#xff1a; nvcc -V版本是10.1 nvidia-smi的版本是12.2 上面如果能显示版本&#xff0c;所以是已经有驱动&#xff0c;首先要删除之前的驱动&#xff1a; 1、执行以下命令&#xff0c;删除旧版本的驱动 sudo apt-get purge …

数据库用户管理

一 目录 一 新建用户 二 用户名的改变 三 删除用户 四 修改用户密码root用户 五 忘记mysql的密码时候怎么办 六 数据库的权限赋予 ​编辑 七 查询用户名的权限 八 权限的撤销 九总结 新建用户 CREATE USER 用户名来源地址 [IDENTIFIED BY [PASSWORD] 密码]; 只是一个用…

node中表单验证捕获第三方库

1.安装 npm install escook/express-joi npm install joi17.4.0 2.使用 2.1创建一个schema文件夹 说明&#xff1a;用于表单规则集&#xff0c;设置表单的规划的 // 导入定义验证的包 const joi require("joi");// 定义用户名和密码的验证规则 const username j…

数据结构(王卓版)——初识数据结构

一、数据结构讲什么&#xff1a; 程序数据结构算法 数据结构的基础以及数据结构的应用方向。 数据、数据元素、数据项、数据对象 数据结构 逻辑结构的种类 存储结构种类 主要学习顺序和链式存储结构。 有时间可以了解&#xff0c;不用重点学。 数据类型&#xff1a; 抽象数据类…

win11利用start11实现全屏菜单,磁贴配置

Win11磁贴配置 最近电脑还是升级到 win11 了。我之前采用的美化方案是桌面上的图标全部移到 win10 开始菜单里的全屏菜单上&#xff0c;用磁贴贴一排。每次要访问文件的时候都去开始菜单里找&#xff0c;而不是放在桌面上&#xff0c;这样桌面也可以空出来欣赏壁纸。参考配置链…

数据库压力测试方法概述

一、前言 在前面的压力测试过程中&#xff0c;主要关注的是对接口以及服务器硬件性能进行压力测试&#xff0c;评估请求接口和硬件性能对服务的影响。但是对于多数Web应用来说&#xff0c;整个系统的瓶颈在于数据库。 原因很简单&#xff1a;Web应用中的其他因素&#xff0c;…

MSP430F249 Proteus仿真智能刷卡计时计费水表系统 -0070

MSP430F249 Proteus仿真智能刷卡计时计费水表系统 -0070 Proteus仿真小实验&#xff1a; MSP430F249 Proteus仿真智能刷卡计时计费水表系统 -0070 功能&#xff1a; Protues版本&#xff1a;8. 9 程序编写&#xff1a;IAR 7.10 硬件组成&#xff1a;MSP430F249 单片机 …

auc的计算方法

方法一 用指示函数表示上式中正样本预测值大于负样本预测值的正负样本对&#xff0c;则得到 p表示预测得分。 在给出的例子中&#xff0c;包含有2个正样本(A, B)和3个负样本(C, D, E)&#xff0c;因此一共有6个(2*3)正负样本对&#xff0c;即公式中分母为6。 接下来计算公式…

Rogue DHCP Server攻击(仿冒DHCP Server攻击)

目录 事件原理 DHCP 仿冒攻击 防护方法 事件原理 攻击原理:攻击者仿冒DHCP Server向客户端分配错误的IP地址以及错误的网关等信息,导致用户无法正常的访问网络。漏洞分析:DHCP客户端收到DHCP Server的DHCP消息之后,无法区分这些DHCP消息是来自仿冒的DHCP Server还是…