[Unity 杂货铺] TMP 字体在编辑器中的动态数据清理
1 导读
TextMeshPro 字体设置为 Dynamic 时,在每次编辑 UI 时会自动生成字形数据并保存到 .asset 文件,导致 Git 频繁记录无意义变动。
本文分析了问题根源,并给出了解决方案:在 asset 触发变动保存时,调用 TMP_Font ClearFontAssetData 清理数据。
你可以直接从该 Github 库中获取脚本参考:https://github.com/STARasGAMES/tmpro-dynamic-data-cleaner
2 前言
TextMeshPro 动态字体资产,会将动态生成的数据保存在与配置数据相同的 asset 文本文件中。基本上每次编辑 UI 都会导致 .asset 文件变动,这给版本控制带来了很大的麻烦。同时,对字体真正的设置修改也很可能被淹没在这些动态数据中。
TMP_Font 提供了一个 Clear Dynamic Data On Build 的配置选项,但那只是用于构建时。
原本觉得这是一个很小众的“问题”,甚至都算不上问题。顺手问了一下 AI,还真给出了相关讨论和其他人的解决方式:
TMP 字体 asset,每次拼UI都会生成纹理导致 Git 变化,有好办法处理吗?
DeepSeek 不仅提供了一份 Unity Issue Tracker 中很新的链接:DYNAMIC TMP_FONT ASSET SHOWS UP AS MODIFIED IN SOURCE CONTROL WHEN TEXTMESHPRO TEXT USING IT IS CHANGED
还提供了不少 Unity 论坛中相关的讨论,本文的解决方式则来自于其中一则:https://discussions.unity.com/t/solved-tmpro-dynamic-font-asset-constantly-changes-in-source-control/868941/
这点是非常棒的,因为之前很难想象该使用何种关键字,如何通过搜索引擎匹配找到这样的讨论。
3 问题描述
3.1 现象
只是修改了一个 UI 上的文字,提交时却发现 TMP 字体资源(如 NotoSansCJK-Regular SDF.asset)也在变动列表中。
打开版本控制工具查看差异,发现文件变化巨大,但实际只改了寥寥几个字符。
多人协作时,频繁出现字体资源的合并冲突,耗费大量精力。
3.2 根源
TMP 的动态字体(Dynamic)模式,当字体资源的 Atlas Population Mode 设置为 Dynamic 时,TMP 会在编辑器运行时自动将用到的字符字形(glyph)添加到字体图集纹理中。问题是,这些动态生成的二进制图集数据会被直接保存回 .asset 文件中。
具体流程:
你在场景中创建一个 TMP 文本,输入文字“你好”。
TMP 检查当前字体图集中是否包含“你”和“好”的字形。
如果没有,就从源字体文件中提取这两个字,生成 SDF 纹理,并写入图集。
最后,将更新后的图集数据序列化保存到 .asset 文件中。
因此,即便只是新增一个字符,字体文件也会被修改,并被 Git 捕获。
4 解决方案
可以在 Unity 保存任何资产之前,自动检测并清除动态字体资源中的“生成性”二进制数据,只保留配置信息。
直接提供代码,来自于:https://github.com/STARasGAMES/tmpro-dynamic-data-cleaner
字体 .asset 似乎不会在每次修改时都进行写入保存,可以尝试使用 Ctrl + S 来触发 OnWillSaveAssets。
using System;
using TMPro;
using UnityEditor;
using UnityEngine;
internal class DynamicFontAssetAutoCleaner : UnityEditor.AssetModificationProcessor
{
static string[] OnWillSaveAssets(string[] paths)
{
foreach (string path in paths)
{
try
{
var assetType = AssetDatabase.GetMainAssetTypeAtPath(path);
// GetMainAssetTypeAtPath() sometimes returns null, for example, when path leads to .meta file
if (assetType == null)
continue;
// TMP_FontAsset is not marked as sealed class, so also checking for subclasses just in case
if (assetType != typeof(TMP_FontAsset) && assetType.IsSubclassOf(typeof(TMP_FontAsset)) == false)
continue;
// Loading the asset only when we sure it is a font asset
var fontAsset = AssetDatabase.LoadMainAssetAtPath(path) as TMP_FontAsset;
// Theoretically this case is not possible due to asset type check above, but to be on the safe side check for null
if (fontAsset == null)
continue;
#if UNITY_2022_1_OR_NEWER
bool isDynamic = fontAsset.atlasPopulationMode is AtlasPopulationMode.Dynamic or AtlasPopulationMode.DynamicOS;
#else
bool isDynamic = fontAsset.atlasPopulationMode is AtlasPopulationMode.Dynamic;
#endif
if (!isDynamic)
continue;
// Debug.Log("Clearing font asset data at " + path);
fontAsset.ClearFontAssetData(setAtlasSizeToZero: true);
}
catch (Exception e)
{
Debug.LogException(e);
Debug.LogError($"Something went wrong while clearing dynamic font data. For more info look at previous log message. Font asset path: '{path}'");
}
}
return paths;
}
}
调用 ClearFontAssetData 之后,.asset 文本降到 100多行。查看 Inspector,Character Table 和 Gylph Table 也都已清空。

浙公网安备 33010602011771号