精简中文字符集,解决Unity TMP缺字与生成慢 做 UI 的应该都有过这种经历界面写好了文案也填进去了一进 Play ModeTextMeshPro 文本里密密麻麻全是方块和空白。问题不出在文案而是字体资源里根本没有这些字形。Unity TextMeshPro 的 SDF 渲染方案必须先把字符放进字形图集Atlas运行时才能正常取用缺字就只能显示成“豆腐块”。中文又不像英文只有 26 个字母全量塞进去几万个汉字生成一次能让你等得怀疑人生。所以我在搭建模拟项目 X 的 UI 阶段直接整理了一套简体中文字符集专门给开发期用。这套东西说穿了就是一份精心挑选的字符表配合 TMP 的 Font Asset Creator 生成一个精简字体资源覆盖率足够日常 UI 文案生成速度又能控制在可接受范围。这篇就把怎么选字符、怎么生成、怎么补坑完整讲清楚。1. 先把需求捋清楚开发期字体为什么这么麻烦1.1 TMP 缺字是怎么发生的开发期为什么尤其明显TextMeshPro 的渲染核心是 SDF简单理解就是把每个字符的轮廓转成一张带距离场信息的图运行时在着色器里重建边缘。为了让任意字符都能被显示字符必须先出现在一张或多张 Atlas 图集里。字体资源 File 决定了图集里预烧录了哪些字符。TextMeshPro 组件在绘制文本时如果遇到图集中没有的字符就会显示为空缺字形常见表现就是 UI 上出现一个个空心方块。开发期这个问题的频率会成倍放大。原因很简单开发中的文案是不断变化的。策划今天改活动名运营明天加版本公告测试同学随手往表格里填个繁体字都会把字体资源的缺口暴露出来。我在模拟项目 X 里就遇到过这种情况某个界面标题正常副标题出现三个方块排查半天发现是输入法把简体词转成了繁简混排字符集里没有对应字形。这种问题如果每次都靠“换一张全量字体图集”来解决资源体积和生成时间都会失控尤其是几千上万字的字体资源每次改动都要重新生成一次开发节奏完全被打断。1.2 用表和逻辑告诉你开发期不该直接上全量字库很多新手的第一反应是“那就把所有汉字都放进字体资源里”。理论上可行实际开发中却很不划算。Unicode 的 CJK 统一汉字区段 U4E00 到 U9FFF 就有两万多个字符再加上扩展区更是海量。把这些全部生成到 TMP 图集里生成时间以小时计都不奇怪图集页数会膨胀到几十页编辑器内存和显存占用都跟着暴涨。开发期根本不需要这些生僻字它们只会拖慢每次迭代。有这么几种方案可以选择我常把它们放在一张表里对比方案生成时间运行性能缺字风险资源体积适用阶段全量字符集静态字体极长一般极低很大发布尾期可选精选常用字符集静态字体适中优按覆盖范围而定小开发期主推动态字体Dynamic无需生成有卡顿风险低但运行时不可控取决于源字体原型验证可用“仅用于开发”这个定位意味着这套字符集不需要追求理论上的无懈可击它的核心目标是覆盖绝大多数日常文案生成够快图集体积可控开发流程顺畅。那些极生僻的姓氏、古汉字、异体字留到正式版本配合最终文案再处理。开发阶段最重要的是速度和稳定不是“全”。2. 字符集内容怎么定覆盖范围与生成思路2.1 字符范围怎么划常用字、次常用字与扩展区做字符集之前要定范围否则很容易陷入“我到底放多少字”的选择困难。我自己的做法是分三层第一层是中文常用字第二层是通用符号和拉丁字符第三层是项目文案采集的补充字。常用汉字部分可以直接使用一套常用字表大体上就是 3500 个常用字加 1000 个次常用字。这个规模对日常 UI 文案的覆盖率已经很高游戏里的按钮、弹窗、活动标题基本不会跳出这个范围。如果你在做社交类或内容型产品可能会有一些动态内容比如玩家昵称、聊天文本这就要在字符集之外再做动态字体的兜底方案而不是一味把字符集加大。从 Unicode 区间的角度来划分我建议这样选基础拉丁字符U0020 到 U007E包含半角数字、大小写字母和常见半角标点。CJK 标点U3000 到 U303F包含中文全角空格“ ”、顿号、句号、书名号等。CJK 统一汉字区U4E00 到 U9FFF只选取常用字和次常用字不要全量。全角形式UFF00 到 UFFEF包含全角数字、全角拉丁字母和全角标点按需加入。很多人容易忽略一个原则不要为了让字符集“看起来完整”就把所有 Unicode 区间一股脑勾上。扩展区 B 还有几万个字符字体源文件也不一定包含它们的字形勾上之后 Missing Glyphs 的数量会非常难看生成的图集里还会出现一堆空白字形纯粹浪费资源。2.2 谁都容易漏的空白和标点汉字选好了符号却往往被忽略而符号恰恰是 UI 文案里出现频率最高的部分。我之前整理字符集时在空格上吃过亏。TMP 文本里的空格 U0020 必须存在于字体资源中否则所有单词或中文词组的间距都会异常。还有换行符 U000A、回车符 U000D、Tab 制表符 U0009虽然正常 UI 里不常用但某些多行文本组件中会出现。不换行空格 U00A0 也建议补上很多编辑器和翻译软件会自动生成这种字符。标点符号的中英文区别也很容易出问题。中文文案里的句号、逗号、括号是全角形式英文是半角形式两套都要放进去。我列一下开发字符集中比较推荐保留的符号集合ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz 0123456789 .,;:!?-()[]{}/|\~#$%^*_ 。、“”‘’【】《》〈〉——……·…·×÷≈≠≤≥℃㎡→←↑↓这里有三点需要特别提醒。第一省略号建议放两个 U2026 字符而不是几个英文句点代替否则字体渲染出来只是三个点不是标准的居中省略号。第二破折号在中文排版中一般是两字位宽度字符集里存在 U2014 EM DASH 即可文案输入时连写两个。第三中文单双引号“和”、‘和’左右方向不同不要只放一边否则文案一侧是直引号一侧是弯引号看起来非常别扭。2.3 从项目文案里自动提取字符集的脚本直接靠常用字表虽然稳妥但可能会包含大量实际用不到的汉字让图集多出不少页数。更聪明的做法是把当前项目的有效文案扫描一遍提取去重后得到一份“最小字符集”再用常用字表做兜底。这样生成的字体资源最贴合实际生成时间也最短。提供一个参考脚本语言用 Python跑一遍就能把多个文本文件里的字符收集起来from pathlib import Path paths [ UI/Strings/zh-CN.txt, UI/Strings/item_names.txt, UI/Strings/npc_names.txt, ] chars set() for p in paths: path Path(p) if not path.exists(): continue text path.read_text(encodingutf-8) for ch in text: if ch not in \r\n\t: chars.add(ch) # 空白字符不能省 chars.update([ , \n, \t]) output .join(sorted(chars)) with open(font_charset_dev.txt, w, encodingutf-8, newline\n) as f: f.write(output) print(charset chars:, len(chars))这段脚本会把所有文本中出现过的字符做成合集再去重排序最后生成一个精简的 TXT 字符集文件。跑完后会把文件放在项目外导入 Unity 项目目录里使用。脚本的编码输出是 UTF-8 无 BOM这一点对后面的 TMP 生成很重要下面会解释原因。3. 动手制作从字符集文件到 TMP Font Asset3.1 准备两样东西字体源文件和 UTF-8 字符集文件开始制作之前先要准备好两个文件。第一个是字体源文件我用的是一份开源的黑体类 TTF/OTF支持简体中文。选择源文件时注意两点一是尽量用 TTF 或 OTF不要用 TTC 合并字体包TMP 对 TTC 的支持不稳定容易出现导入后识别不到字形的情况二是确认字体文件的使用授权开发期团队内使用通常问题不大但如果项目要对外发布字体版权必须提前落实。第二个就是上一节生成的字符集文件。这个文件看起来就是个纯文本内容是一行行字符没有特殊格式。唯一要注意的是编码必须是 UTF-8最好是无 BOM。TMP 读取字符文件时如果碰到带 BOM 的文件可能把开头的不可见字符也当成一个字形塞进去导致生成的字体资源里莫名其妙多出一个空白字符或者第一个字符显示异常。还需要把字符集文件放进 Unity 项目内的某个目录比如放在Assets/Fonts/CharacterSets/下。TMP 的 Font Asset Creator 并不要求文件在 Assets 内但放在 Assets 里可以纳入版本管理团队成员拉下来直接能用不用各自再找文件。3.2 Font Asset Creator 参数填法一套开发期配置单Unity 窗口菜单里找到Window TextMeshPro Font Asset Creator打开后就是生成字体资源的界面。核心操作是把 Source Font File 指向字体文件把 Character Set 单选区切到Characters from File再指定字符集文件然后点 Generate Font Atlas。真正容易踩坑的是下面这一组参数我用开发期的默认推荐值说明参数开发建议值理由Sampling Point Size80 到 96点阵采样大小值越大字形细节越丰富但图集占用越大Padding8 到 12SDF 边缘重建需要梯度空间太小会锯齿发虚太大浪费图集Packing MethodFast开发期优先速度Optimum 虽然排列紧凑但生成更慢Atlas Resolution2048 x 2048单页容纳字数和显存占用之间较平衡Character SetCharacters from File精确控制字库内容Render ModeSDFAA观感清晰抗锯齿正式包再按平台换 SDF8 或 MSDFInclude Font Data建议开启避免其他机器或打包后源字体路径失效导致样式错乱填好参数后直接点 Generate Font Atlas。生成过程中窗口下方会显示进度条和统计信息重点看 Missing Glyphs 的数量。这个数值表示“字符集里有但字体源文件里没有对应字形”的字符数。如果 Missing Glyphs 为 0说明源字体完整覆盖了你的字符集可以放心使用。如果大于 0需要把缺的字从字符集里去掉或者换一个字形覆盖更全的字体源文件。生成完成后点Save as按钮把资源保存成 TMP Font Asset。保存完在任意 TextMeshProUGUI 组件的 Font Asset 字段上指向刚生成的文件界面里的中文就能正常显示了。3.3 一页图集能塞多少字为什么说字不能贪多为了方便你预估资源规模和生成时间这里给一个简单的容量估算。假如 Sampling Point Size 是 90Padding 是 9每个汉字在 Atlas 中大约占一个 (909×2) 也就是 108 像素的格子。2048×2048 分辨率的图集单页最多放进约 19 乘 19 个字也就是 361 个字形。这样算下来收集 5000 个字符就要十几页图集生成自然快不了。这解释了为什么我经常提醒开发同学不要在一个字符集里狂塞字符。表面上多放几百个字不是大事实际生成速度和图集页数都会线性上涨最后编辑器还会因为 Atlas 页数太多出现明显的加载延迟。精简字符集的收益立竿见影用最贴近项目的字符集配合常用字兜底生成时间通常可以从几分钟压到一分钟内。4. 把流程放到模拟项目 X 里走一遍4.1 从 6000 字砍到 2400 字生成时间的变化我在模拟项目 X 搭建的时候UI 规模大概几十个界面文案来自配置表和预制体直接扫出来的有效字符数并不大。第一次图省事我直接拿一份现成的“常用 3500 次常用 1000 符号 800”字符集生成字体资源总字符接近 6000。用 2048×2048、SDFAA、Padding 9 的配置跑一次生成要等大概五分钟实在熬人。后来改成先跑采集脚本从项目所有文案文件里提取去重再把常用字表合并进去最终字符集缩减到 2400 出头。同样配置再生成一次时间降到大概一分半图集页数也少了接近一半。这个对比给我留下的印象很深字符集的“精”比“全”更能提升开发体验。之后每次项目大版本更新我都会重新跑一遍采集脚本把新增角色名、活动名、公告文本里的新字符补进去缺失的字及时加进字符集再生成一次整套流程基本能在几分钟内完成。采集脚本之外我还写了一个简单的 C# 编辑器工具用来扫描场景和预制体里所有 TMP 组件的文本把每个组件当前显示的字符自动收集汇总。这个工具的开发思路很简单using System.Collections.Generic; using System.IO; using System.Text; using UnityEditor; using UnityEngine; using TMPro; public static class TmpCharSetTool { [MenuItem(Tools/TMP/Collect TMP Texts)] public static void Collect() { HashSetchar set new HashSetchar(); string[] guids AssetDatabase.FindAssets(t:Prefab); foreach (string guid in guids) { string path AssetDatabase.GUIDToAssetPath(guid); GameObject prefab AssetDatabase.LoadAssetAtPathGameObject(path); if (prefab null) continue; TMP_Text[] texts prefab.GetComponentsInChildrenTMP_Text(true); foreach (TMP_Text tmp in texts) { foreach (char c in tmp.text) { set.Add(c); } } } StringBuilder sb new StringBuilder(); foreach (char c in set) sb.Append(c); File.WriteAllText(Assets/Fonts/CharacterSets/tmp_charset.txt, sb.ToString(), new UTF8Encoding(false)); AssetDatabase.Refresh(); Debug.Log(charset count: set.Count); } }这个工具不会替换你的人工符号补充但它能快速汇总界面中出现过的所有字符配合常用字表兜底比纯手工维护字符集省力得多。4.2 录制现场踩到的三个坑第一个坑是 UTF-8 BOM。最初的字符集文件是从文本编辑器里直接保存的保存时默认带了 BOM。生成字体资源后发现每次使用都会在文本开头多出一个零宽字符查了很久才发现是文件编码的问题。解决办法很简单另存为 UTF-8 无 BOM或者在上面的 Python 脚本里用encodingutf-8而不是utf-8-sig。第二个坑是空格被脚本当作多余字符过滤掉。采集脚本如果有strip()或replace( , )之类的操作空格就直接没了。去掉后所有 TMP 文本的单词间距、中文文案的自然间隔全部挤在一起看起来像串行文本。排查半天才意识到字符集里没有空格。修正之后在字符集合里显式加上空格、Tab、换行三个字符问题立刻消失。第三个坑是遇到繁体字 Missing Glyphs。项目文本里混了几个繁体字字符集里有但字体源文件本身没有对应字形所以生成结果依然缺字。解决办法不是换一套更大的字符集而是给主字体资源挂一个 Fallback 后备字体资源或者在字体源文件层面换用字形覆盖更广的黑体。开发期直接用 Fallback 最省事主字体不用重新生成一个备用字体资源专门承载少量冷门字。4.3 开发/发布字体资源分开一键切换开发期的字体资源和正式发布字体资源我从来不会混用。开发字体用精简字符集正式字体用最终文案集的完整字体资源。为了切换方便我维护了两个 TMP Font Asset然后在项目里通过一个 Editor 菜单项批量替换所有预制体上的 TMP 组件字体资源。菜单按钮遍历项目里的 Prefab 和场景把所有 TMP 组件的 Font Asset 统一改成开发版或正式版。这样开发分支和发布分支的预制体无需修改资源引用只是打包前切一次字体即可。需要注意一点如果预制体是嵌套的 SubPrefab替换脚本要递归处理所有子物体。我一开始只替换了 Prefab 根节点的 TMP 组件导致部分界面的字体还是旧的看起来像是没切成功。后来统一改成GetComponentsInChildrenTMP_Text(true)才算彻底解决。5. 常见问题排查与避坑清单5.1 缺字的排查路径和 Fallback 方案UI 上看到方块时第一步不是改代码而是先确认这个字符到底来自哪里。把有问题的文本复制到文本编辑器里查看字符的 Unicode 码值再对照字符集文件确认它是否在集合内。如果在集合内但界面上仍然缺字那问题在字体源文件本身源文件就没有这个字形TMP 自然生成不出来。这种场景最有效的处理方式不是无限扩充主字体而是给字体资源挂 Fallback。在 TMP Font Asset 的 Inspector 底部找到 Fallback Font Assets 列表把另一个包含生僻字的字体资源加进去。渲染时 TMP 会在主字体图集里找不到字形后自动去后备字体里匹配。开发期我给主字体挂了两个后备字体资源一个负责生僻汉字一个负责常见文化符号生成主字体资源的时间几乎没有额外增加。5.2 生成慢、图集大的治理顺序生成慢和资源大往往一起出现治理顺序我建议遵循“剔除 → 降采样 → 降分辨率 → 换打包模式”的路线。先看字符集是否有大量用不到的字符把明显冗余的生僻姓氏、异体字、不全的繁简体混合内容剔除。再降低 Sampling Point Size从 96 降到 80 往往能明显减少图集占用。之后再看 Atlas Resolution如果图集页数很多说明单页容量不够可以适当提升分辨率或打开 Multi Atlas Textures。最后才考虑把 Render Mode 从 SDFAA 换成 SDF8因为这会牺牲一部分显示质量开发期不建议优先动这一项。这里有个容易误解的点Atlas Resolution 提高到 4096 并不一定让整体资源变小。单页能放更多字确实会减少页数但 4096 图集本身占用的显存远超 2048在开发机上可能看不出差别真机上却可能成为负担。开发期的取舍标准只有一个生成时间别拖太久界面显示别缺字也别发虚。5.3 动态字体和静态字体的正确打开方式TMP 支持把字体资源设置成动态模式运行时遇到字符集之外的字会自动计算 SDF 并加入图集看似很省事。但实践中我很少在开发期把它当主力方案原因有三个第一动态生成是运行时 CPU/GPU 成本界面第一次出现新字符时会明显卡一下复杂 UI 上这个问题会被放大许多倍第二动态字体依赖源字体文件一旦同事机器上没有对应字体或者项目被拷到别的电脑上运行表现可能就是大面积缺字第三动态字体的图集在运行时按需增长内存里可控性很差低端设备很容易出现纹理占用飙升。真正适合动态字体的是那些文本内容完全不可控的场景比如玩家输入框、聊天系统、网页内容显示。开发期的 UI 文案是可控的用静态字体配合精简字符集要稳定得多。5.4 打包前最后的检查清单从开发期切到正式发布前我会按这个清单快速过一遍检查项错误表现正确做法Include Font Data 是否开启打包后真机字体错乱或缺失字体资源勾选 Include Font Data是否误用开发版精简字体发布版缺少正式文案中的生僻字切换为正式字体资源或补全字符Fallback 字体是否随包切换界面后某些字符空白确认后备字体资源被主字体引用字体源文件授权是否落实法律和合规风险确认字体可商用或符合项目授权图集页数是否过多真机显存压力大精简字符或调整图集分辨率这套流程走完发布包的字体问题基本就能提前拦住。开发期那份简体中文字符集则安安静静待在项目里继续服务下一轮迭代。最后分享一个小习惯我用这个方案替换掉最初的动态字体方案后字体出问题的频率明显下降。现在我的做法是把font_charset_dev.txt文件放在字体资源同一级目录下每次遇到缺字先把字补进文件再重新生成一次字体资源最后连同字符集文件一起提交版本管理。这样整个团队共用同一份字符集缺字不再只出现在某一个人界面上而是变成一次性的、可追溯的修改。如果你也被 TMP 缺字和生成时间折磨不妨从整理一份精简字符集开始先把开发期体验提上来正式包的资源优化再单独处理这个投入回报很稳定。