做游戏本地化这几年,我最怕的不是翻译,是中文。英文版本跑得好好的,中文一开,UI 上全是毛边、方块字、乱码,玩家截图发过来,评论区直接炸。查到最后,问题几乎都集中在同一个环节——TextMeshPro 的字体资源。Unity 默认的 TMP 字体对中文支持本来就是瘸腿的,如果你直接用完整中文字体硬怼,图集膨胀、运行时补字卡顿、边缘发虚,各种问题全来了。今天这篇就把中文本地化这块一次性讲清楚:怎么根据文案动态生成一套最小字体集,把边缘模糊和乱码这两个老大难彻底解决。
这篇文章适合正在做多语言版本、尤其是中文版本的同学,也适合那些已经踩了字体坑、想从根上理顺本地化流程的人。我会从原理讲到实操,再把自动化的思路和踩坑记录都放出来,尽量让你看完能直接照着做。
1. 中文本地化为什么总在字体上翻车
1.1 直接用完整中文字体,Unity 里会出什么问题
中文字体和英文拉丁字体最大的区别就是字符数量。英文 26 个字母,加上数字、标点,撑死几百个字符。中文呢?GB2312 常用字就有 6763 个,GBK 全量两万多个,你想覆盖得全一点,思源黑体一个 Regular 字重下来 8MB 到 16MB 很正常。这还只是一个字重,如果你要 Regular、Bold、Italic 三套,光字体文件就几十 MB 了。
有人说,那我把字体打进包里,用的时候 TMP 会自己烘焙图集,问题不大吧?实际一跑就露馅了。TMP 处理中文字符时,每个用到的字符都要光栅化到一张图集纹理(Atlas)里。几千个汉字塞进去,图集尺寸直接飙到 2048 甚至 4096,这还只是一个字重。内存里的 Texture 占用、加载时间、构建时间全上去了。
更麻烦的是 TMP 的动态字体模式。Unity 2019 之后 TMP 支持动态字体,遇到图集里没有的字符会现场补字。听起来很方便对吧?但中文场景下,动态补字意味着运行时要把字形从 TTF/OTF 里解出来、光栅化、重新排图集。第一次遇到生僻字的时候,那一帧直接卡一下,图集还会越来越大,搞到最后内存和性能双输。
1.2 TMP 的 SDF 渲染和“边缘模糊”到底是怎么回事
TMP 默认的渲染方式叫 SDF(Signed Distance Field,有向距离场)。简单说,它把每个字形烘焙成一张距离场纹理,存的是"当前像素离字形边缘有多远"这个数值,而不是直接存颜色。渲染时用 Shader 根据距离值还原边缘。这个方案的好处是放大缩小都不会太糊,但代价是边缘细节有损失,尤其是中文这种笔画密集、拐角多的文字。
边缘模糊的直接原因通常有三个:一是字形采样点太低。TMP 生成 Font Asset 时有 Sampling Point Size 这个参数,它决定了字形被光栅化时的像素尺寸。中文笔画多,采样点低了,SDF 距离场里存的信息不够,边缘自然发虚。二是图集纹理被压缩了。纹理格式压成 DXT 或 ASTC 后,距离场数据被有损压缩,边缘会出毛刺。三是老版本 TMP 默认的单通道 SDF 对复杂字形本来就不够用,中文笔画交叉的地方信息丢失严重。
后来 TMP 引入了 MSDF(Multi-channel Signed Distance Field)方案,用三个颜色通道分别保存不同方向的距离场,在拐角处能还原出更锐利的边缘。对中文这种复杂字形,MSDF 的提升是肉眼可见的。我用同样的字号、同样的采样点,SDF 还是有点毛,MSDF 基本干净了。
1.3 “乱码”分为三种,大部分不是字体本身的锅
一说中文字体乱码,很多人第一反应是"字体文件坏了"。实际做本地化时,我遇到的乱码大概率可以分成三类。
第一类是编码层乱码。最常见的是从 Excel 导出的 CSV 不是 UTF-8 编码,或者只有 UTF-8 没有 BOM,Unity 的 TextAsset 读出来全变成乱码。这种情况和字体一点关系都没有,纯粹是文件编码问题。
第二类是字形缺失。这个才是真正的"字体问题"。字符在 Uncode 标准里存在,但你用的字体根本没有这个 glyph,TMP 只能显示成一个方框或者问号。比如很多免费字体收录的汉字不全,你正好用了某个生僻字,它就给你来个方块。还有一种情况是字符集漏了,你只把 UI 文案里的字符放进了字体,但某个动态拼接的文本用了不在字符集里的字,结果运行时补不上,也变成方块。
第三类是字体回退的意外。TMP 有 Fallback Font Assets 机制,当前字体没有这个字形时会自动去 Fallback 字体链里找。但如果 Fallback 链配得不对,或者回退到了系统字体,显示效果就会和其他文本不一致,看起来就像"乱码"。
所以排查的时候,第一步先分清到底是编码问题、缺字形问题,还是回退问题,别一上来就重做字体。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动态生成最小字体集的整体思路
2.1 核心目标:只保留游戏真正用到的字符
做中文本地化时,最理想的状态是:游戏里每句中文用到的每个字符,字体资源里都有;不用的字符,一个都不放。这就是标题里说的"最小字体集"。你可以把它理解成给字体做一次"减脂",把几万汉字浓缩成实际用到的几百到两千个字符。
具体的做法是:从本地化文案、UI 文本、代码里拼接的字符串中收集所有字符,去重后得到一个字符集,然后用这个字符集去裁剪原始字体文件,生成一个子集字体。Unity 里最终用的 TMP Font Asset,只包含这个子集字体的字符。
这样做的好处非常直接:图集小、加载快、内存低、不会运行时补字。中文 UI 常见的文本量,字符集一般在 1500 到 3000 个左右,一张 1024 或 2048 的图集就放下了。相比原来大字体动不动要 4096 的 Atlas,体积和内存天差地别。
2.2 为什么优先选“预处理子集字体”而不是 TMP 动态字体
这里有个容易混淆的概念。标题说的"动态生成最小字体集",指的是在打包/编辑阶段,根据文案动态准备好一套最小字体资源,不是 TMP 运行时的 Dynamic Font Asset。那为什么不用 TMP 自带的动态字体?我前面提过,动态模式下,如果字符集覆盖不到位,运行时遇到新字符会现场补字,有卡顿、有内存增长,还会改图集。这在测试机上偶尔触发一次还好,线上玩家设备千奇百怪,万一某个字触发补字,掉帧就是实打实的体验问题。
而且动态字体和"最小字体集"在理念上是冲突的。最小字体集追求的是可控、可预测、固定开销,动态字体则是把不确定性留到了运行时。对于需要上线的产品,我强烈建议走"预处理 + 静态字体"这条路。
另外还有一个原因:子集字体文件本身有很多好处。裁剪后的 OTF/TTF 只有几百 KB 到一两 MB,如果想用 Addressables 按语言分包,每个语言包都很小,用户下载中文包的成本极低。字体文件小了,字体加载、序列化的速度也快。
2.3 需要准备哪些素材和工具
要跑通这套流程,需要准备三样东西。
第一是原始中文字体文件。建议选择开源、可免费商用的思源黑体、阿里巴巴普惠体,或者霞鹜文楷这类字形完整的字体。注意,子集化不等于拿到授权,商用前还是要看你用的字体的授权协议。
第二是本地化文案的汇总文件。不管你是用 CSV、JSON 还是 Excel,都要能一次性导出所有中文文本。如果项目里文本分散在各个场景的组件上,建议先做一轮"提取 UI 文本"的工作,把文案统一收口,这本身也是本地化工程化的基础。
第三是 Python 环境和 fontTools 库。fontTools 是处理字体文件最成熟的 Python 库,可以无损裁剪字体、读写 cmap,跨平台都能用。安装就一行 pip 命令,后面会细说。如果你不想用 Python,也可以用 Unity 编辑器脚本直接调用 FontEngine API 生成 TMP Font Asset,但字符集的计算、字体裁剪校验这些工作,用 Python 更顺手。
3. 实操:从本地化文案中提取字符集
3.1 先把本地化文本全部收集到一起
字符集提取的输入,必须是项目里所有可能出现的中文文本。我见过不少团队只扫了 UI 静态文本,结果动态拼接的玩家名、活动名、系统通知全漏了,上线才发现缺字。
最可靠的做法是,本地化文案统一维护在表格软件或专门的本地化平台里,导出格式固定为 UTF-8 的 CSV 或 JSON。排查时可以写一个脚本遍历整个导出文件目录,把所有单元格内容全部读进来。注意 CSV 的编码坑:Excel 导出的 CSV 经常是带 BOM 的 UTF-8 或者 GBK,Python 读取时要用 utf-8-sig 处理,不然第一个 key 会带一个看不见的 BOM 字符,后面比对字符集时老觉得不对劲。
文本里的占位符也要处理。比如 "欢迎回来,{playerName}!" 这种字符串,提取字符时 {playerName} 里的字母和花括号都会被算进去。花括号本身要保留,字母按需保留,但如果同一个文本里英文单词是给别的语言用的,中文文案里通常不会有完整单词,所以不用担心。真正要小心的是反斜杠、换行符 \n、制表符 \t 这些,处理时先去掉转义或者单独保留原字符,别把 n 和 t 误当成普通字符。
3.2 Python 脚本提取字符集(有代码)
下面这个脚本我一直在用,逻辑很简单:遍历所有 CSV/JSON 文件,把所有文本拼起来,去重,再补上必要的默认字符,最后输出一个纯文本文件。
python复制import json
import csv
import glob
import sys
def collect_text_from_csv(path):
texts = []
with open(path, "r", encoding="utf-8-sig") as f:
reader = csv.reader(f)
for row in reader:
for cell in row:
if cell:
texts.append(cell)
return texts
def collect_text_from_json(path):
texts = []
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
def walk(obj):
if isinstance(obj, str):
texts.append(obj)
elif isinstance(obj, dict):
for v in obj.values():
walk(v)
elif isinstance(obj, list):
for item in obj:
walk(item)
walk(data)
return texts
def main():
# 改成你的本地化文件目录
files = glob.glob("./localization/**/*.csv", recursive=True)
files += glob.glob("./localization/**/*.json", recursive=True)
chars = set()
for path in files:
if path.endswith(".csv"):
texts = collect_text_from_csv(path)
else:
texts = collect_text_from_json(path)
for text in texts:
chars.update(text)
# 必须保留的基础字符:空格、换行、制表符
extra_chars = " \n\t"
# 半角英文和数字,防止中文文案里夹带英文
extra_chars += "0123456789"
extra_chars += "abcdefghijklmnopqrstuvwxyz"
extra_chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
chars.update(extra_chars)
# 排序后输出,方便后面 git diff 看字符集变化
sorted_chars = sorted(chars)
output = "".join(sorted_chars)
with open("character_set_zh.txt", "w", encoding="utf-8") as f:
f.write(output)
print(f"total unique chars: {len(output)}")
print(output)
if __name__ == "__main__":
main()
运行完会生成一个 character_set_zh.txt,里面是去重后的所有字符。建议把这个文件提交到版本管理,文案一改动,diff 里能直接看出哪些字符新增了。检查字符数时心里有个数:常见的中文界面 1500 到 2500 个字符都算正常,如果突然冒出 5000 个,要么文案确实多,要么有非中文语言混进来了。
3.3 字符集范围一定要补全的几类字符
光靠脚本从文案里提取还不够,有几类字符必须手动补,不然后面有得哭。
中文标点必须全覆盖。全角逗号、句号、感叹号、问号,书名号《》、括号()、省略号……这些在中文文案里太常见,但有些免费字体的标点收录不全。我的习惯是把 CJK 标点范围 U+3000 到 U+303F 整个区间,以及全角形式符号 U+FF00 到 U+FFEF 里常用的那部分强制加进字符集。具体可以查一下 Unicode 表,别偷懒。
特殊符号也要留意。温度单位摄氏度 ℃、版权符号 ©、商标符号 ™、注册符号 ®,还有 emoji 以外的箭头、货币符号、序号 ①②③,都可能在活动文案里出现。这些字符往往不在你扫描的普通文本里,但一旦 UI 用了,没有就是方块。
最后一个建议是,如果游戏支持中文输入(比如聊天、玩家命名),最小字体集就兜不住用户输入了。这种情况下,要么给输入内容单独用系统字体回退,要么策划限制输入字符范围,要么把输入功能用的字体单独用一种支持全量 GB2312 的字体并接受图集开销。这块要提前和策划对齐,别等到测试反馈"玩家名字显示方块"才回头补。
4. 用 fontTools 生成最小字体文件
4.1 核心命令:pyftsubset 一步到位
拿到字符集之后,下一步就是裁剪字体文件。fontTools 自带的 pyftsubset 命令行工具可以一条命令做完,非常方便。先安装:
bash复制pip install fonttools
然后运行:
bash复制pyftsubset SourceHanSansSC-Regular.otf \
--text="$(cat character_set_zh.txt)" \
--output-file=Lang_zh_Regular.otf \
--layout-features='*' \
--glyph-names \
--symbol-cmap \
--legacy-cmap \
--notdef-glyph \
--notdef-outline \
--recommended-glyphs \
--name-IDs='*' \
--name-legacy \
--name-languages='*'
逐个解释一下关键参数。--text 是把字符集直接传给工具,这是最直观的方式。--layout-features='*' 保留 OpenType 排版特性,中文虽然没有西文连字,但数字的等比/等宽特性、某些字体的小型大写这类特性保留了没坏处。--glyph-names 和 --symbol-cmap、--legacy-cmap 是为了兼容不同平台的字体解析逻辑,有些老设备对 cmap 格式挑剔,这几个参数能减少目标平台认不出字体的概率。
--notdef-glyph 和 --notdef-outline 一定要加。它们保留字体默认的"缺字方块"轮廓,万一某个字符漏了,至少 UI 上显示的是一个整齐的方块而不是直接渲染崩溃。--recommended-glyphs 会把字体设计者建议的额外字形保留下来,比如某些字体在中英文混排时需要额外的空格比例字形,这个参数能减少排版异常。
如果你更习惯写 Python 而不是命令行,也可以用 fontTools 的库函数,效果一样:
python复制from fontTools import subset
options = subset.Options()
options.set(
layout_features='*',
name_IDs='*',
notdef_outline=True,
recommended_glyphs=True,
symbol_cmap=True,
legacy_cmap=True,
)
font = subset.load_font("SourceHanSansSC-Regular.otf", options)
subsetter = subset.Subsetter(options)
subsetter.populate(text=open("character_set_zh.txt", encoding="utf-8").read())
subsetter.subset(font)
subset.save_font(font, "Lang_zh_Regular.otf")
4.2 生成之后的检查:别急着进 Unity
裁剪出来的字体文件,先别直接丢进 Unity,花两分钟验证一下,能省不少排查时间。
最简单的验证方式是用 fontTools 的 ttx 工具把子集字体的 cmap 导出成 XML,然后数一下里面的字符数:
bash复制ttx -t cmap Lang_zh_Regular.otf -o cmap.xml
打开 cmap.xml,看 <cmap_format_4> 或 <cmap_format_12> 里有多少个 <map> 标签,数量应该和你字符集的去重字符数接近,略有差异是正常的(比如字体里有些 glyph 对应多个 Unicode 字符位)。如果数量差太多,说明字符集提取或者裁剪参数有问题,这时候回头查脚本哪一步漏了。
还有一个实操小技巧:在 Python 里直接把字符集和字体的 cmap 做一次差集,看有没有字符在字符集里但字体渲染不出来。
python复制from fontTools.ttLib import TTFont
font = TTFont("Lang_zh_Regular.otf")
cmap = font.getBestCmap()
chars = open("character_set_zh.txt", encoding="utf-8").read()
missing = [c for c in chars if ord(c) not in cmap]
print("missing:", "".join(missing))
这一步能快速定位"字体本身不支持"的字符。通常这类字符集中在冷僻异体字、扩展 A 区汉字,真遇到了就换一个字形更全的原始字体,或者让策划改文案。整个流程里,缺字形这个问题越早发现越省事。
4.3 字体选型与多字重的处理
最小字体集方案里,原始字体的选择比你想的更重要。我推荐直接选开源可商用的思源黑体或者阿里巴巴普惠体,因为它们的字形全,GB2312 甚至 GBK 都收得齐,裁剪后的子集在中文显示上不容易缺字形。用系统字体(比如微软雅黑)做游戏字体的问题是跨平台一致性差,而且授权上对商业游戏不友好,Android 上根本没有同样字形的字体,显示效果会漂移。
字重方面,UI 设计稿里中文通常至少要有 Regular 和 Bold 两个字重。TMP 自带的加粗是一种 fake bold,英文上还凑合,中文笔画本来就密,fake bold 一加,笔画直接糊成一团,边缘更虚。正确做法是原始字体文件里就准备两个字重,各自跑一遍 pyftsubset,生成两个子集字体文件,再在 Unity 里分别生成两个 TMP Font Asset。用的时候通过 Font Style 或者直接切换 Font Asset 来控制,别用 TMP 的 Bold 样式模拟。
5. Unity 侧的 TMP 字体资源创建与参数设置
5.1 Font Asset Creator 的正确姿势
Unity 里创建 TMP 字体资源的入口是 Window > TextMeshPro > Font Asset Creator。把裁剪好的 Lang_zh_Regular.otf 拖到 Source Font File,这时先别急着点 Generate,先把下面几个参数调对。
Sampling Point Size 是中文字体最关键的参数。英文 32 点可能就够了,中文建议 90 到 120。这个参数决定字形光栅化到图集时的像素尺寸,点太小,SDF 存的信息不够,边缘发虚;点太大,字符占的图集格子就大,一个字符集塞不进一张 Atlas,会报 Atlas capacity exceeded。常见的做法是先用 90 试,看生成的图集边缘质量和容量是否均衡,不够再往上加。如果你的字符集特别大(超过 3000 字符),可能要把 Sampling Point Size 降到 72 左右,配合 2048 图集才放得下。
Padding 建议设 9。Padding 是字形边缘到图集格子边界的距离,太小的话相邻字形的 SDF 信息会互相渗色,显示出发光或者黑影。这个参数用默认的 9 就行,不用刻意省空间。Packing Method 选 Optimum,它比 Fast 更费时,但图集利用率更高。Atlas Resolution 选 1024x1024 起步,字符集大的直接 2048。如果 2048 还放不下,优先减 Sampling Point Size,实在不行再考虑开 Multi Atlas Textures,把 Atlas 拆成多张,但那样会增加 Draw Call 和内存,能不开就不开。
5.2 关键选择:静态字体还是动态字体
Character Set 那里,选 Custom Characters,把 character_set_zh.txt 里的内容整个粘贴进去,然后点 Generate Font Atlas。这一个动作就把字体资源变成"静态"的了——图集里固定包含这些字符,运行时不补字。
很多人在这里会手滑选成 Dynamic。选了 Dynamic 之后,TMP 会允许运行时动态添加新字符,但代价就是回到我开头说的运行时补字问题。做中文本地化,我的经验是:字体资源一定要保持静态。动态字体留给英文那种字符集小的场景还行,中文一开动态,图集增长不可控,掉帧不可控,能不用就不用。
生成完的 Font Asset 建议命名成"Lang_zh_Regular_SDF"这种一眼能看出用途的名字,方便后面做语言分包和代码查找。源字体文件在生成 Font Asset 之后就不需要被运行时引用了,TMP 运行时用的是图集纹理和字形映射数据,不会去读原始 OTF,所以你不用担心把大字体文件打进包体。如果你用 AssetBundle 或 Addressables 打包,可以把 Font Asset 和它的 Atlas 纹理单独放一组,只给中文语言包用,其他语言包完全不加载这个资源,包体省得非常明显。
5.3 材质和纹理设置里的隐形坑
生成完 Font Asset,还有一个容易被忽略的地方:字体图集纹理的压缩格式。如果你在 Player Settings 里把纹理压缩成了 ASTC 或 DXT,中文字体的 SDF 纹理边缘会产生明显的压缩噪点。SDF 纹理本质是距离场数据,有损压缩等于直接往数据里掺噪声,边缘能干净才有鬼了。
我的处理方式是,在 Assets 下找到这个 Font Asset 对应的 Atlas 纹理,把它的 Advanced 纹理设置里 Format 改成最高质量,关闭压缩或者用 RGBA 32 Bit 无压缩格式。图集一般只有 1 到 2 张 2048,无压缩多占的内存也就 16MB 左右,换来的是边缘清晰,这笔账是划算的。Mipmap 建议关掉。UI 字体在 2D 界面里用不到 Mipmap,开着反而会在文字缩小到一定程度时出现模糊和闪烁。
关于材质参数,TMP 的默认材质里有 Face Dilate、Softness、Underlay 这些滑块。很多同学说边缘模糊,其实是 Face Dilate 没调到合适值。SDF 渲染时 Face Dilate 控制字形边缘的膨胀度,数值偏小边缘会瘦小发虚,偏大又会发胖变糊。最佳做法是生成一个字体后,在场景里放一排不同 Font Size 的文本,然后逐个调 Dilate 和 Softness,调到各字号都舒服为止。这个调好的材质参数会存在 TMP Settings 的默认材质里,后续新文本都继承,不用每个文本单独调。
5.4 启用 MSDF 后还需要注意什么
Unity 2021.2 之后,TMP 在 Font Asset Creator 里提供了 MSDF 渲染模式。创建字体资源时,Render Mode 选择 SDFAA 之外的 MSDF 相关选项,生成出来的图集是 RGBA 多通道的,三个通道分别编码不同方向的距离场,在中文这种笔画交叉、拐角密集的场景,边缘还原度比单通道 SDF 高一大截。同一个字体,同一个 Sampling Point Size,MSDF 和 SDF 放一起对比,MSDF 的中文明显干净锐利,这是目前解决边缘模糊最有效的一招。
代价是图集内存大概是原来的三倍。因为一个字形要写三个通道的数据,原来的单通道图集是灰度,MSDF 是 RGBA。不过 UI 字体图集本来就是小开销,一张 1024 的 MSDF 纹理也就 4MB 左右,大多数游戏完全能接受。换 MSDF 之后,Shader 要用 TMP 自带的 MSDF 版本,TMP 的默认 shader 已经支持了,通常不用手动处理。如果发现用了 MSDF 但显示依然是灰的,先查一下材质用的 Shader 是不是 .shader 文件里带 MSDF 的那套,UI 默认材质有时会被覆盖。
还有个小坑:MSDF 对 Tile 方式的 Atlas 支持不如 SDF 成熟,如果开着 Multi Atlas Textures,某些版本的 TMP 在 MSDF 模式下排图有 bug,所以我前面说尽量别开多 Atlas,就是这个原因。
6. 把字符集校验和生成流程自动塞进构建管线
6.1 用编辑器脚本拦截构建,缺字直接报错
手工跑 Python、手工粘贴字符集,这套流程做一两次还行,项目迭代起来文案每天都在变,漏掉一次就是线上事故。所以我把字符集校验做成了 Unity 编辑器脚本,在打包前自动跑一遍。
思路是这样:写一个 IPreprocessBuildWithReport 的实现,在 OnPreprocessBuild 里做两件事。第一,重新扫描本地化文件,生成当前最新的字符集字符串。第二,读取现有的 TMP Font Asset,遍历它的 character table,校验是否包含了最新字符集里的所有字符。如果有缺失,直接打印一个明显的 Build 错误,甚至可以根据配置决定是否中断构建。
csharp复制using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
public class FontCoverageCheck : IPreprocessBuildWithReport
{
public int callbackOrder => 0;
public void OnPreprocessBuild(BuildReport report)
{
string latestChars = FontBuildTools.CollectAllCharsFromLocalization();
string missing = FontBuildTools.GetMissingChars(
latestChars,
AssetDatabase.LoadAssetAtPath<TMP_FontAsset>("Assets/Fonts/Lang_zh_Regular_SDF.asset")
);
if (!string.IsNullOrEmpty(missing))
{
Debug.LogError($"[FontCheck] 缺少 {missing.Length} 个字符: {missing}");
}
}
}
这段代码是骨架,实际项目里 CollectAllCharsFromLocalization 和 GetMissingChars 要按你的本地化文件和字体资源路径去实现,但核心逻辑就是"比对字符集和字形表"。这么做的价值在于,把整个流程从"靠自觉"变成"构建时的硬性检查",漏字的问题在 CI 上就被卡住,不会再流到玩家手里。
6.2 常见问题排查速查表
我把这几年前前后后踩过的坑整理成一张表,按"现象-原因-解决方案"的顺序排列,遇到问题直接对着查。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 中文边缘发虚、有毛刺 | Sampling Point Size 太低 | 提高到 90-120,重新生成 Font Asset |
| 中文边缘发虚、有毛刺 | Atlas 纹理被压缩 | 纹理格式改为无压缩 RGBA32 |
| 中文边缘发虚、有毛刺 | 仍在使用单通道 SDF | 切换到 MSDF 渲染模式 |
| 个别字显示方块 | 字符不在子集内 | 更新 character_set_zh.txt,重新裁剪字体并生成 Font Asset |
| 个别字显示问号或乱码 | 字体本身没有这个字形 | 换原始字体,或让策划改文案 |
| 整个 TMPro 文本显示乱码 | 本地化文件编码不对 | 统一用 UTF-8 with BOM,读取时用 utf-8-sig |
| 运行时某帧明显卡顿 | TMP 字体是动态模式,正在补字 | 改用静态字体,构建时校验字符覆盖 |
| 生成图集报 Atlas capacity exceeded | 采样点太大或字符太多 | 降低 Sampling Point Size,或开 Multi Atlas(慎用) |
| 中文粗体笔画糊成一团 | 用了 TMP 的 fake bold | 用真正的 Bold 字重子集字体,别用样式模拟 |
| 中文显示发虚但英文正常 | 字体材质 Face Dilate 没调 | 调 Face Dilate 和 Softness,单独保存材质 |
| 某个 UI 字体用的还是默认字体 | 忘了给 TextMeshProUGUI 指定字体 | 全局检索 Font Asset YAML,统一替换 |
6.3 踩坑心得:材质参数和本地化编码的统一
最后说两个最容易反复踩的坑。第一个是 TMP 材质参数。很多时候"边缘模糊"不是字体资源的问题,而是材质 Face Dilate 和 Softness 组合不对。我见过一个项目,字体资源质量已经拉满了,结果默认材质里 Dilate 是负数,所有文本边缘都是虚的。排查时先看一眼材质参数,再动字体资源,顺序别搞反。
第二个是编码统一。项目里如果同时存在 UTF-8、UTF-8 with BOM、GBK 的本地化文件,Python 脚本读的时候就要做多编码兼容,非常容易出 bug。我的建议是:所有本地化文件一律强制 UTF-8 with BOM。Excel 导出时注意选 UTF-8 而不是默认的 ANSI,JSON 文件保存时在编辑器里把 Encoding 选成 UTF-8 with BOM。这一步统一了,整个管线的编码坑能少 80%。
还有 CI 环境要注意。如果你在 CI 上跑 Python 脚本,确认 CI 机器的 Python 环境里有 fonttools,不然构建会卡在导入阶段。我习惯在工程根目录放一个 requirements.txt,里面写 fonttools==4.x.x,CI 里先 pip install -r requirements.txt 再构建,避免环境不一致。
7. 最后再分享一点个人经验
中文本地化这件事,看起来只是"把文本翻译成中文",实际砸在上面的时间大部分都在字体上。前端显示效果、包体大小、运行时性能、平台适配,全都被一个字体资源卡着。我做过多语言项目之后最大的体会是:字体方案一定要在一开始就定好,不要在项目中期还让 UI 上每个文本组件各自用不同的字体资源,那只会让问题像滚雪球一样膨胀。
如果你是做小 Demo 或者原型验证,直接一个大字体丢进去,反而快;如果你面对的是一款要上线、要持续迭代的产品,我建议今天就按这个流程走:本地化文案统一收口,Python 脚本提取字符集,pyftsubset 裁剪字体,Unity 里用 MSDF 静态字体,最后把构建校验接上。这套流程跑顺之后,后续发布任何语言版本,字体问题基本不会再找上门。这也是我从"每次提测都被中文显示问题搞到头大"到"多语言版本一次过"之间最关键的转变。
