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