1. 为什么TextMeshPro显示中文会成为一个“坑”
如果你刚开始用Unity做项目,大概率会经历这样一个场景:UI界面用Text组件显示英文一切正常,换成中文就变成了一堆方块或者问号。这时候你上网一搜,发现大家都在推荐TextMeshPro,说它渲染清晰、性能好、支持富文本。于是你兴冲冲地导入TMP,创建了一个TextMeshPro - Text组件,输入中文,结果——还是方块。
这不是你操作有问题,而是TextMeshPro的默认字体资源根本不包含中文字形。TMP和Unity内置的Text组件在字体处理机制上走的是完全不同的路线。内置Text依赖的是Unity的Font资源,系统层面会自动回退到可用的中文字体;而TMP使用的是预烘焙的Font Asset,字体图集里没有的字形就是没有,它不会自动帮你去找系统字体。
这个差异带来的直接后果就是:你必须自己制作一个包含中文字形的TMP Font Asset。而中文字符集动辄两三万字,全量烘焙出来的图集大小和内存占用都相当可观。所以这件事的核心矛盾在于——既要让中文正常显示,又要控制图集体积和运行时开销。后面我会围绕这个矛盾,把整个流程拆开讲清楚。
这篇文章适合两类人:一是刚接触TMP、被中文显示问题卡住的新手;二是已经能用TMP显示中文,但想进一步优化图集大小和加载策略的开发者。我会从字体资源准备、Font Asset生成、动态图集配置、常见异常排查几个维度展开,尽量把每个步骤背后的原因也讲明白。
2. 中文字体资源的准备与授权注意事项
2.1 选什么字体文件
TMP支持的字体格式是TrueType(.ttf)和OpenType(.otf)。理论上任何包含中文字形的字体文件都可以用,但实际选择时需要关注几个点。
第一是字符覆盖范围。有些字体标称支持中文,但实际只覆盖了GB2312的6763个常用字,遇到生僻字或者繁体字就会缺字形。如果你的项目面向的是通用场景,建议选择覆盖GB18030或者Unicode CJK统一表意文字基本区的字体。第二是字重和风格。TMP的Font Asset是基于某个具体字重生成的,Regular和Bold是两个独立的Font Asset,不能指望一个Regular通过加粗标签就变成真正的粗体——TMP的<b>标签只是模拟加粗,效果远不如直接用Bold字重生成一个Font Asset。
第三是授权问题。这是很多人容易忽略的。商业字体用于商业项目需要购买授权,开源字体如思源黑体、思源宋体、文泉驿系列、站酷系列等可以免费商用,但也要看清楚具体的许可证条款。我个人的建议是,项目初期就用开源字体把流程跑通,等美术风格确定后再考虑是否替换为商业字体。
2.2 字体文件的导入设置
把.ttf或.otf文件拖进Unity的Assets目录后,选中它,在Inspector面板里需要注意几个设置。
Font Size这个参数对TMP来说其实不太重要,因为TMP生成Font Asset时会用自己的采样尺寸。但Character选项建议设为Dynamic,这样Unity会保留完整的字符信息。Include Font Data必须勾选,否则打包后字体数据不会被打进包体,运行时就会出问题。
还有一个容易踩的坑:如果你的字体文件很大(比如某些全字符集字体有十几MB),Unity导入时可能会卡一下,这是正常的。导入完成后,在Project窗口里展开这个字体资源,应该能看到它下面自动生成了一个Font Asset和一个Material。但注意,这个自动生成的Font Asset是Unity内置Text系统用的,不是TMP用的。TMP需要你自己通过Font Asset Creator来生成。
3. 用Font Asset Creator生成中文Font Asset的完整流程
3.1 打开Font Asset Creator的正确方式
在Unity菜单栏选择Window > TextMeshPro > Font Asset Creator。如果你没看到这个菜单,说明TMP Essential Resources还没导入。在Window > TextMeshPro > Import TMP Essential Resources导入一次即可。
打开后的面板里,Source Font File选择你刚才导入的字体文件。接下来几个参数是决定成败的关键。
3.2 采样尺寸与图集分辨率的选择逻辑
Sampling Point Size决定了每个字形被采样的精度。这个值越大,字形边缘越清晰,但占用的图集空间也越大。对于中文来说,常用字号在24到48之间。我的经验是:如果UI上中文最大显示到72px,采样尺寸设48就够了;如果要做大标题,可以设到64或72。不建议无脑设很大,因为图集空间是有限的。
Atlas Resolution是图集的分辨率,常见选项有512、1024、2048、4096。这里有个计算公式可以帮你估算:假设采样尺寸为S,每个字符平均占用的像素面积大约是(S+2)^2(加2是padding),那么4096×4096的图集理论上能容纳约(4096×4096)/(S×S)个字符。以S=48为例,大约是7281个字符。但实际因为排列损耗和padding,能放下的会更少。
所以如果你要烘焙全量中文字符(约两万多字),4096的图集在48采样尺寸下是放不下的。这时候就需要用到Character Set的筛选功能。
3.3 Character Set的三种策略与适用场景
Font Asset Creator提供了几种Character Set选项,对应不同的使用策略。
ASCII只包含英文字符,显然不够用。Custom Range可以手动指定Unicode区间,比如4E00-9FFF就是CJK统一表意文字的基本区,覆盖了绝大多数常用汉字。Custom Characters可以粘贴一段具体的文本,只烘焙这段文本里出现的字符。Characters from File可以指定一个文本文件,把文件里所有出现过的字符都烘焙进去。
这几种策略怎么选?我的建议是分场景:
- 如果项目是固定文案的UI,比如设置界面、菜单、按钮文字,用Characters from File最划算。把所有UI文案整理成一个txt文件,烘焙出来的图集可能只有几百KB。
- 如果项目有玩家输入或动态文本,比如聊天、命名、随机生成的内容,那就需要覆盖常用字范围。可以用Custom Range指定
4E00-9FFF,但要注意图集大小。 - 如果项目是多语言的,建议按语言拆分Font Asset,中文一个、日文一个、韩文一个,运行时根据语言切换,而不是把所有字符塞进一个图集。
3.4 渲染模式与图集模式的选择
Render Mode有SDF、SDFAA、Raster、Smooth等选项。做UI文字用SDFAA(Signed Distance Field Anti-Aliasing)是标准选择,它在各种缩放比例下都能保持清晰。Raster是点阵模式,放大后会模糊,一般不推荐。
Atlas Population Mode有Static和Dynamic两个选项。Static就是烘焙时确定所有字形,运行时不能添加新字符。Dynamic允许运行时动态添加字形到图集,但需要字体文件本身包含完整的字符数据,且图集需要有剩余空间。
这里有个重要的取舍:Static模式性能最好,但字符集固定;Dynamic模式灵活,但运行时会有图集扩容和重排的开销。对于中文项目,我通常建议用Static模式配合合理的字符集筛选,因为中文动态添加字形的开销比英文大得多,而且图集很容易被撑满。
3.5 生成与验证
点击Generate Font Atlas按钮后,面板下方会显示生成的图集预览。如果看到密密麻麻的汉字排列在图集里,说明生成成功。点击Save as...保存为一个新的Font Asset文件。
生成完成后,把TMP组件上的Font Asset替换成你刚生成的这个。输入中文测试,应该就能正常显示了。如果还是方块,检查一下是不是Font Asset没有正确赋值,或者字符确实不在烘焙范围内。
4. 动态图集模式下的中文显示配置与性能权衡
4.1 Dynamic模式到底解决了什么问题
Static模式最大的限制是字符集固定。如果你的项目允许玩家输入中文名字,或者有从服务器下发的动态文案,Static模式就无能为力了。Dynamic模式允许TMP在运行时把缺失的字形实时添加到图集中,相当于把烘焙过程从编辑器搬到了运行时。
但Dynamic模式对中文字体来说有个前提:字体文件必须包含完整的字符数据,且Unity导入时Character选项要设为Dynamic。如果字体文件本身是子集化的(只包含部分字符),Dynamic模式也变不出缺失的字形。
4.2 动态图集的扩容机制与内存代价
Dynamic模式下,TMP的Font Asset会维护一个图集纹理。当新字形加入时,如果当前图集还有空位,就直接写入;如果满了,TMP会尝试扩容图集。扩容的方式取决于Font Asset的Atlas Width和Atlas Height设置,以及Multi Atlas Textures选项。
如果开启了Multi Atlas Textures,TMP会创建额外的图集纹理来容纳新字形。每个额外的图集都会占用显存,而且图集切换会带来额外的Draw Call。如果不开启,图集满了之后就无法再添加新字形,缺失的字符会显示为方块。
这里有个实际测试的数据可以参考:一个4096×4096的SDF图集,在采样尺寸48、padding为5的情况下,大约能容纳3000到4000个中文字形。如果玩家输入的名字涉及生僻字,很容易触发扩容。所以Dynamic模式虽然灵活,但内存占用和性能表现是不确定的,取决于运行时实际用到了多少字符。
4.3 混合策略:Static打底 + Dynamic补充
我在实际项目中用得最多的是一种混合策略:用Static模式烘焙一份常用字(比如GB2312的6763个字),保证绝大多数UI文案和常见输入都能覆盖;同时把Font Asset的Atlas Population Mode设为Dynamic,并开启Multi Atlas Textures,让生僻字有地方可去。
这样做的理由是:常用字的字形已经在主图集里了,不需要运行时动态添加,性能稳定;生僻字虽然会触发动态添加,但频率极低,对整体性能影响可以忽略。而且主图集的大小是可控的,不会因为动态添加而无限膨胀。
配置时需要注意,Static烘焙的字符集和Dynamic的补充是共存在同一个Font Asset里的。你可以在Font Asset Creator里先用Custom Range烘焙常用字,保存后再把Atlas Population Mode改为Dynamic。这样生成的Font Asset既有预烘焙的字形,又支持运行时扩展。
5. 中文显示异常时的排查链路与修复方案
5.1 方块、问号、乱码的区分与对应原因
中文显示异常的表现形式不同,原因也不同。方块通常意味着字形缺失,TMP找不到对应的字符,用缺省方块代替。问号可能是字体文件本身的问题,或者字符编码不对。乱码则可能是文本编码问题,比如UTF-8和GBK混用。
排查时第一步是确认Font Asset里到底有没有这个字符。在TMP组件的Inspector面板底部,有一个Font Asset的预览区域,点击后可以查看当前Font Asset包含的所有字符。如果目标字符不在列表里,那就是烘焙范围没覆盖到。
5.2 字体图集溢出与字符丢失
有时候Font Asset里明明有某个字符,但显示出来还是方块。这种情况可能是图集溢出了。在Font Asset Creator生成时,如果字符太多放不下,TMP会提示Atlas is full,但生成的图集里可能只包含了一部分字符。这时候需要减小采样尺寸、增大图集分辨率,或者缩小字符集范围。
另一个可能的原因是Material Preset的问题。TMP的Font Asset关联着一个Material,如果Material的Shader参数被改错了,比如Face Color设成了透明,文字就会看不见。检查一下Material的Face和Outline设置是否正常。
5.3 动态模式下字符添加失败的常见原因
Dynamic模式下,如果新字符添加失败,首先检查字体文件的Character设置是不是Dynamic。如果字体文件导入时设成了Static,Unity只会保留字体文件里实际用到的字符数据,Dynamic模式就找不到其他字形了。
其次检查图集是否已满且没有开启Multi Atlas Textures。如果图集满了又没有额外图集,新字符就加不进去。可以在运行时通过TMP_FontAsset.TryAddCharacters方法手动添加字符并检查返回值,确认添加是否成功。
还有一个容易被忽略的点:Dynamic模式下的字体文件必须保留在包体里。如果字体文件被标记为Editor Only或者在打包时被剥离了,运行时就没有字形数据可用了。确保字体文件的Include Font Data是勾选的,并且在打包后的资源里确实存在。
6. 中文Font Asset的体积优化与打包策略
6.1 图集分辨率与采样尺寸的平衡
中文Font Asset的体积主要取决于图集的分辨率和数量。一个4096×4096的SDF图集,如果格式是Alpha8,大约占16MB;如果是RGBA32,就是64MB。这个体积在移动端是不可接受的。
优化的第一个方向是降低图集分辨率。如果UI上中文最大只显示到48px,采样尺寸设32甚至24就够用了。采样尺寸降低后,同样分辨率的图集能容纳更多字符,或者可以用更小的图集容纳同样多的字符。
第二个方向是压缩图集格式。TMP的Font Asset在导入设置里可以调整纹理压缩格式。对于SDF图集,Alpha8格式通常就够了,因为SDF只需要一个通道。如果平台支持,可以用ASTC或ETC2压缩,进一步减小体积。但要注意压缩可能会引入伪影,需要在真机上验证效果。
6.2 按需拆分Font Asset
如果项目的中文文案量很大,一个Font Asset装不下,可以考虑按模块拆分。比如UI用一个Font Asset,剧情文本用另一个,战斗飘字用第三个。每个Font Asset只包含该模块实际用到的字符,总体积反而比一个大而全的图集小。
拆分的另一个好处是加载策略更灵活。UI的Font Asset可以常驻内存,剧情文本的Font Asset可以在进入剧情时加载、退出时卸载。这样峰值内存占用会低很多。
6.3 打包时的资源依赖检查
最后一步是打包后的验证。在Editor里显示正常不代表打包后也正常。常见的问题包括:字体文件没有被打进包体、Font Asset的Material丢失、图集纹理被压缩得太过导致模糊。
建议在打包后跑一次完整的中文显示测试,覆盖所有使用中文的界面和动态文本场景。如果发现某个界面的中文变成了方块,优先检查该界面使用的Font Asset是否被正确加载,以及字符是否在烘焙范围内。
另外,如果项目使用了AssetBundle,Font Asset和它依赖的字体文件、Material、Texture需要放在同一个Bundle里,或者确保依赖关系正确,否则运行时会出现资源丢失。
7. 我在实际项目中积累的几个实用技巧
第一个技巧是关于字符集文件的生成。如果你不确定项目到底用了哪些中文字符,可以写一个编辑器脚本,遍历所有Prefab和场景里的TMP组件,把它们的text属性收集起来,去重后输出成一个txt文件。用这个文件作为Characters from File的输入,烘焙出来的图集就是精确匹配项目实际需求的,体积最小。
第二个技巧是关于Font Asset的命名规范。建议把字体名称、字重、采样尺寸、字符集范围都体现在文件名里,比如SourceHanSans-Regular_S48_GB2312。这样当项目里有多个Font Asset时,一眼就能看出区别,不会搞混。
第三个技巧是关于动态字形的预热。如果项目知道玩家可能会输入哪些字符(比如常见的姓氏),可以在加载界面提前调用TryAddCharacters把这些字符预热到图集里,避免游戏过程中突然卡顿。预热列表可以从常用姓氏表或项目历史数据里提取。
第四个技巧是关于SDF Scale的调整。TMP的Material里有一个Scale参数,影响SDF的渲染效果。如果发现中文边缘有硬边或者模糊,可以微调这个值。一般来说,采样尺寸越大,Scale可以设得越小;采样尺寸小的时候,Scale要适当增大来补偿。
第五个技巧是关于多语言切换时的Font Asset管理。如果项目支持中英日韩多语言,建议用一个Font Asset Manager来统一管理不同语言的Font Asset加载和切换。切换语言时,遍历所有TMP组件,把Font Asset替换成对应语言的,同时注意Material Preset也要跟着换,否则描边、阴影等效果可能会丢失。