做Unity开发,尤其是UI、本地化和微信小游戏这些场景,TextMeshPro(TMP)基本上是绕不开的组件。但很多新手第一次在Inspector里把中文字符串拖进TMP组件,一运行,满屏整整齐齐的小方块,心态直接崩。这个问题的本质其实不复杂:TMP和传统UGUI的Text不一样,它不是运行时拿系统字体去渲染的,而是把外部ttf/otf字体文件里的一部分字形烘焙成一张图集(Texture Atlas),字体资源里没有这个字,它就显示方块。所以解决思路就是两步:准备好含有中文的字体资源,并让它覆盖你要显示的字符。我下面会把从原理到实操完整讲一遍,按这套流程走,5分钟能让中文字正常显示,并且附上我自己整理的7000+字库制作思路和一堆踩坑记录,Unity新手、做聊天弹幕输入框的、以及要在数字孪生和微信小游戏里用TMP的,都应该能在这篇文章里找到答案。
1. 先搞清楚:TMP为什么显示小方块
1.1 小方块就是你字体资源里不存在的字符
我刚开始接触TMP的时候也犯过糊涂,以为是Unity安装出问题了,后来查了源码和文档才明白,TMP的渲染机制和UGUI的老Text完全不同。传统UGUI Text在做中文字符渲染时,走的是系统字体接口,Windows和macOS系统层面自带中文字体,所以哪怕你代码里没做任何设置,中文也能显示出来。但TMP为了跨平台一致性、为了性能,把字体相关的数据全部做成了自包含的资源,也就是说,它把ttf/otf里的字形提前烘焙到Asset里面,运行的时候不再依赖系统字体。
这个烘焙过程就是Font Asset Creator:你指定一个外部字体文件,再把需要打包的字形列表喂给它,它把每一个字符的轮廓、间距、图集坐标全部存进TMP_FontAsset文件里。如果你的列表里没有某个字,或者列表里有这个字但字体文件本身没有这个字形,那渲染的时候TMP就没有任何数据可用,于是它就用字体文件里的.notdef(未定义字符)占位符来渲染,也就是我们看到的方框。
用一个好理解的类比:这就像去打字机店盖章,TMP这套机制相当于先做了一批印章,你做印章的时候只刻了7000个字,你拿去盖一个“龘”字,印章盘里当然没有,盖出来的自然就是一个残缺的方块。这不代表你的字符串错了,而是你的印章盘里压根没准备这个字。
1.2 乱码的三张面孔:方块、问号、一片空白
很多人以为中文乱码只有“方块”这一种表现,实际上我排查过的项目里,至少有三类现象,对应的原因完全不同,如果不区分就会一直修错方向。
第一类是方块,也就是最常见的.notdef占位,原因基本就是Font Asset里没有这个字,或者字体文件本身缺字形。第二类是问号,这种情况通常是文本内容在导入时编码就坏了,比如从Excel导出CSV的时候用了错误的编码格式,TMP拿到手已经是乱码字符串,和字体资源没关系。第三类是一片空白,这个最容易迷惑人,有时候不是字体资源的问题,而是TMP的材质球被替换成了Unlit/Shader丢失导致的显示异常,也有人是设置了Font Asset但没赋值给TMP组件的fontAsset属性,结果还是走默认字体。
我的排查习惯是:先看代码或Prefab里的字符串本身正不正确,如果字符串在Inspector里显示正常,那就是Font Asset或Shader的问题;如果字符串在Inspector里就已经是乱码,那就去查编码链路,不要动字体资源。把这三种情况分清楚,能省掉大量无用功。
2. 5分钟实操:从字体文件到中文正常显示
2.1 选字体:开源中文字体怎么选
做TMP中文字体,第一步是准备一个ttf或otf文件。很多新手直接拿Windows自带的微软雅黑或宋体来用,这里有两个问题:一是版权,微软雅黑在商业游戏里是有授权限制的,正规发行项目会有风险;二是字体文件内部字形质量参差,生成出来的图集边缘容易发虚。我更推荐用开源字体,比如思源黑体(Source Han Sans)、阿里巴巴普惠体、HarmonyOS Sans、文泉驿微米黑。这几款都是开源可免费商用的,思源黑体有多个字重,标题正文都能覆盖,阿里巴巴普惠体也承诺了免费商用授权。
选择的时候注意字体文件格式,TMP官方支持ttf和otf,但按我实际测试的经验,ttf(TrueType)的兼容性和生成速度更稳定,有些otf字体在Font Asset Creator里生成时会出现字形偏移或图集空白。如果项目设计稿要求某一款字体只有otf格式,也不是不能用,但生成完一定要拖到场景里逐字检查一遍,尤其是中文标点和数字。
另外不建议下载那种动辄几十MB的“全字库”字体文件,尤其是从一些字体站打包下载的,里面经常混杂着来源不明的东西。规范的做法是找一个开源字体厂商的官方仓库,直接下载单独的ttf文件,几MB到十几MB完全够用。
2.2 制作字库txt:5分钟方案的核心
5分钟方案里最容易被忽略的一步,是准备一个包含了常用汉字的txt文本文件。TMP生成Font Asset的时候,Character Set选项里有一个“Characters from File”,就是让你指定一个文本文件,它会把这个文件里出现过的所有字符都收集起来,烘焙进图集。这个txt的格式很简单:不需要任何分隔符,也不需要转义,直接写汉字就行,TMP会自动去重,顺序也不影响。
那7000+常用字库怎么来?网上可以搜到很多别人整理好的“3500常用汉字表”和“通用规范汉字表”,直接复制进txt就能用。不过我更喜欢自己用脚本生成一份GB2312全集,因为GB2312一共收录了6763个汉字,加上常用符号正好能凑到7000+,这覆盖了现代汉语日常使用99.99%的场景。下面这个Python脚本就是按GB2312的区位码范围生成的:
# 生成GB2312全部汉字字库(6763字) def build_gb2312_charset(path): with open(path, "w", encoding="utf-8") as f: for hi in range(0xB0, 0xF8): # 汉字区码范围 B0-F7 for lo in range(0xA1, 0xFF): # 位码范围 A1-FE try: char = bytes([hi, lo]).decode("gb2312") f.write(char) except UnicodeDecodeError: pass if __name__ == "__main__": build_gb2312_charset("charset_gb2312.txt")这个脚本的逻辑很简单:GB2312编码中,汉字分布在区码B0到F7、位码A1到FE的范围里,两两组合,如果能正常解码成字符就写入txt,解码失败就跳过。跑完之后,你再在txt末尾手动补上数字、字母、全角半角标点、常用符号,一个7000+字库就齐了。把这份txt存在项目外的一个固定位置,比如D盘或文档目录,以后每个新项目都能用,这也是我个人的习惯。
2.3 打开Font Asset Creator生成字体资源
字体文件和字库txt都准备好了,接下来打开Unity编辑器。先确认TMP组件已经可用,Unity 2018.4以后TMP已经作为Package默认集成,如果编辑器里找不到Window菜单下的TextMeshPro项,到Package Manager里搜索TextMeshPro,安装即可。然后点击菜单:Window -> TextMeshPro -> Font Asset Creator,进入生成界面。
这里需要设置几个关键参数:
Source Font File选择你下载好的ttf字体文件,Character Set选择Characters from File,Character File选择刚才做好的字库txt,Font Size建议设置在90到120之间。Font Size直接影响图集里每个字形渲染清晰度,太低的字号在UI被放大时会出现模糊,太高又占图集空间,我一般用100。Atlas Resolution建议直接从2048开始,如果生成的字体页数很多,再考虑升到4096。
下面的Render Bold、Render Italic,如果项目里标题需要真正的Bold字形,就勾上,否则不勾,能省不少空间。Padding建议保持默认5左右,Padding的作用是给字形周围预留边距,避免图集压缩时字形贴边出现边缘噪点。设置完之后点击Generate Font Atlas,等待生成完成即可。
生成时间取决于机器性能、字数和图集大小,我的经验是7000字、2048图集、Font Size 100的情况下,大约需要1到3分钟。生成完成后先别急着关窗口,看右上角的Grid Size和页数,如果页数多到几十页,说明字号或字符量太大,需要适当缩小Font Size,或者把字库拆分成两个字体资源。确认无误后点击Save,保存成.asset文件,注意目录建议放在Assets下的Fonts文件夹里,并固定命名规则。
2.4 替换给TextMeshPro组件
字体资源生成好了,剩下的替换就简单了。场景里选中一个TextMeshPro文本组件(不管是3D的TextMeshPro还是UI的TextMeshPro - Text),在Inspector的Font Asset属性里,把之前保存的.asset文件拖进去,运行就能看到中文正常显示了。如果项目里大量文本都要用这个字体,还可以在Project Settings或TMP Settings里修改默认字体资源,这样新建的TMP组件自动用中文字体,不需要每次手动拖。
这里有个细节值得注意:TMP组件上的Font Asset属性是单个引用,不是数组,你替换成中文字体后,英文和数字也会用这个字体的字形。如果中文字体里的英文数字不好看,不要试图在同一个Font Asset里混入两套字体,正确做法是保留一个英文字体作为主字体,然后在中文字体的Fallback Font Assets列表里挂上中文资源,让英文走主字体、中文自动fallback到中文字体。这个机制我后面会详细展开。
3. 7000+字库与Dynamic/Static:内存与覆盖率的平衡
3.1 Dynamic模式:什么时候可以偷懒
TMP生成字体的时候有一个“Include Font Asset in Dynamic Mode”选项,很多人忽略了这个开关,但它决定了你的字体方案是懒人版还是精装版。
Dynamic模式的意思是,字体资源生成时不在图集里预烘焙任何字形,而是等运行时遇到具体字符,再从外部字体文件里动态取字形塞进图集。这样做最大的好处是,你不需要维护字库txt,也不怕玩家输入生僻字,任何字符都能动态显示。新版本TMP里Dynamic模式基本是默认推荐的,因为对于原型开发和工具类项目来说非常省事。
但Dynamic模式也有明显短板。第一是首次显示新字时会卡一下,这个卡顿是运行时同步操作,如果游戏在战斗过程或转场时突然要渲染一个新字,会有肉眼可见的掉帧。第二是内存不可控,动态模式会用多少字就塞多少字进图集,如果这是一款聊天软件或玩家自由输入昵称的游戏,跑久了图集会越来越大,最终内存占用可能比Static模式还夸张。第三是在WebGL、微信小游戏这种受限平台上,Dynamic模式有时会失效,表现为白屏或字体渲染失败,所以我做微信小游戏项目时,基本直接放弃Dynamic方案。
我的建议是:开发期原型、内部工具、输入框、聊天系统这种用户输入不可预期的场景,用Dynamic保底;但如果目标是正式上线、尤其是微信小游戏和WebGL平台,还是老老实实走Static静态字库更稳妥。
3.2 Static模式:为什么还是推荐维护7000字
Static模式就是我们前面讲的预烘焙方案,字体资源里把所有需要的字形一次性烘焙好,运行时零额外IO,加载稳定,不依赖系统字体,也不依赖运行时解析字体文件。这类方案最大的价值是可预测:编译进包里是什么样的,玩家机器上就是什么样的,不会出现“我这好好的,玩家那边全方块”的悲剧。
那为什么偏偏是7000字?因为GB2312一共6763个汉字,加上符号,就是7000+。这个字符量对于绝大多数国产游戏、应用、数字孪生项目已经非常够用。现代汉语常用字也就3500个左右,7000字已经覆盖了人名地名里的大部分生僻字,日常对话、剧情文本、系统提示基本不会超出这个范围。
当然我实际做项目时,并不会每次都硬扛7000字。很多游戏的核心对话其实就几百到两千个字,我会写一个编辑器脚本,把策划配置表里所有文本收集起来,扫出实际用到的字符,生成一个精简字库。7000字更像一个通用模板,新项目刚起步不知道要什么字时,先用7000字顶着,等文本稳定了再精简。
3.3 Fallback:一个字体不够怎么办
7000字再全,也总有覆盖不到的情况。比如游戏里要用特殊符号字体、Emoji、或者图形字,汉字字体本身没有这些字形,这时候不要尝试把所有字符塞进同一个字体,而是要利用TMP的Fallback机制。
Fallback机制通俗地说就是备胎列表:当TMP遇到主字体里没有的字形时,它会沿着Fallback Font Assets列表依次查找,哪个字体有就渲染哪个。你可以在字体资源的Inspector窗口底部看到Fallback Font Assets列表,也可以在TMP Settings里配置全局Fallback列表,让项目所有TMP组件都生效。
我自己的规则是:主字体永远是覆盖最全的正文中文字体,Fallback第一项挂Emoji/Sprite字体,第二项挂一个Dynamic模式的系统字体作为终极保险。这样既保证了绝大多数文本走静态渲染,又给极端情况留了一条活路。要注意的是,Fallback字体里如果也没有对应字形,渲染结果依然是方块,所以Fallback不是万灵丹,它只是把几个字体资源的字形覆盖范围拼在一起。
4. 那些最容易踩的坑:图集、编码与细节
4.1 图集分辨率到底开多大
Atlas Resolution是Font Asset Creator里最容易让新手纠结的参数,它决定每一张图集的像素尺寸,可选512、1024、2048、4096。这里有一个很粗略的估算公式:一行大概能放Atlas宽除以Font Size个字符,比如Atlas Resolution是2048、Font Size是100,那理论上一行最多放20个字符,一页大概能放400个字符。7000字就需要大约18页图集,每张2048x2048的纹理在移动端内存里至少要占16MB,算下来光字体图集就能吃掉近300MB内存,这显然不现实。
所以实际项目里需要做一个折中。我的常用组合是Atlas Resolution 4096、Font Size 80,这样一行大约能放51个字符,一页约2600个字符,7000字大约需要3页图集,每张4096纹理约64MB,总内存约192MB。这个数字依然偏高,但好在Unity渲染时只会加载用到的页,而且减少图集页数本身也能减少Draw Call和显存切换。
如果内存还是吃紧,那就得降低Font Size或者精简字符量。字体图集上的字形不需要像美术切图那么精细,Font Size 60到90之间,在多数UI尺寸下都够清晰,除非你把文字放得特别大。我见过一些项目为了“一劳永逸”,把字体页数压到1页,但Font Size只有30,结果UI一放大字就糊成一团,反而得不偿失。正确做法是先定字号上下限,再反推字符量和Atlas Resolution。
4.2 中英文混排、标点与全角半角
中文字体里通常也包含英文字母和数字的字形,但它们的设计风格往往跟正文汉字不完全统一,英文间距、数字宽度在中文UI里时常看着别扭。正规一点的游戏项目会专门配一个英文字体,主字体用英文的数字和字母,遇到中文就Fallback到中文字体,这样英文排版精致,中文又不缺字形。
还有一个容易忽略的问题是标点符号。中文字库里全角标点(,。!?)一般都齐全,但半角标点(, . ! ?)在很多中文字体里其实也有,只是位置和宽度可能不理想。如果你的字库txt里只放了汉字和全角标点,没有放半角标点,那代码里如果出现了英文逗号、英文句号,TMP同样会报方块。所以我建议字库txt里除了6763个汉字,一定要把数字、大小写字母、常用全半角标点都补齐。具体我用的最小集是:
- 数字:0-9
- 字母:a-z、A-Z
- 全角标点:,。!?;:、“”‘’()【】《》〈〉…—・
- 半角标点:, . ! ? ; : ( ) 空格 等
中英文混排还有一个隐蔽问题:行高不一致。中文和英文字体的Line Height来自字体度量信息,混排时如果两者差异大,中文行和英文行会上下错位。这种情况基本只能靠手动调整TMP字体资源的Line Height偏移,或者干脆让中英文都走同一个主字体,避免字体切换带来的行高跳动。
4.3 粗体斜体、阴影和描边的额外开支
TMP组件默认有Bold和Italic开关,很多新手以为这就是字体自带的粗体和斜体,其实TMP在运行时默认用的是“伪造粗体”和“伪造斜体”,也就是在渲染层面拉伸描边来模拟,效果一般,而且如果字体图集本身没有额外空间,伪粗体有时还会导致字形边缘发虚。如果要项目里的标题、战斗飘字等有正儿八经的粗体效果,我建议直接下载一个粗体字重的ttf,比如思源黑体的Bold字重,单独再生成一套Font Asset,然后通过Style或字体切换来使用。
Shadow和Outline也就是阴影和描边,属于Shader层面的效果,会带来额外的Overdraw,但不增加字体图集内存,这个可以放心用。真正的坑在于名字很相似但用途不同的SpriteAsset:TMP里做Emoji表情用的,不是字体资源,如果你把Emoji元素直接编进字符串,而项目里没配SpriteAsset,那也会显示为方块或直接消失,别和字体乱码搞混。
5. 常见问题与排查技巧实录
5.1 问题速查表
我整理了一份自己排查时常用的速查表,按现象分列,基本覆盖了TMP中文显示的大多数问题。
| 现象 | 可能原因 | 解法 |
|---|---|---|
| 运行后中文字全是方块 | Font Asset里没有这些字形 | 用字库txt重新生成,确保字符覆盖 |
| 某些固定文字还是方块 | 该字符超出了字库范围 | 把缺失字符加入txt重新生成或加Fallback |
| 字符串在Inspector里就乱码 | 数据源编码错误,字符串本身坏了 | 修Excel/CSV导入编码,和字体无关 |
| 字体生成页数特别多 | Font Size太大或字符量太多 | 降低Font Size、精简字库、加大Atlas Resolution |
| 打包后字体全失效 | 字体Asset引用丢失或没进Bundle | 检查依赖,把.asset拖进对应AssetBundle |
| 微信小游戏/WebGL白屏 | Dynamic模式不兼容 | 改用Static模式重新生成 |
| 运行时报Font Asset not found | 字体文件被移动或改名 | 重新拖入Font Asset引用 |
| 文字模糊有锯齿 | Font Size太小或Atlas Resolution太低 | 提高Font Size并重新生成 |
这表格看着简单,但我实际排查过的问题里,最多的是第一种和第五种。第一种是因为团队里有人直接拿默认字体模板生成,字库txt为空,结果所有中文字全缺;第五种是版本管理问题,字体asset没有正确引入AB包,导致打包后引用断裂。
5.2 几个独家经验
除了速查表,我再分享几个常规文档里不会写的教训。
第一个是关于字体文件位置的。很多新手喜欢把ttf字体文件放在StreamingAssets或Resources外部的自定义目录,以为这样能减小包体或者方便热更,但TMP生成的Font Asset在Inspector里引用的是ttf的GUID,如果ttf文件在打包时没有进入依赖链,字体资源虽然还在,但运行时动态取字形会失败。最简单的做法是把ttf放在Assets/Fonts下,一起参与打包,别搞什么花活儿。
第二个是版本管理方面的。中文字体生成的.asset文件通常动辄几十MB,如果你用Git管理项目,一定要配置好LFS(Large File Storage),否则协作者拉下来后,.asset可能变成一串指针或损坏文件,表现为字体资源打不开、字形缺失。SVN团队也要注意提交完整文件,我遇到过同事提交时漏传.asset对应的.meta文件,导致资源GUID混乱,一个Prefab里的字体引用全部错乱。
第三个是生成后不要轻易改Font Size和Atlas Resolution。Font Asset生成后会内嵌一个图集,如果你在Inspector里改了字体资源的Font Size参数,而不重新打开Font Asset Creator点击Generate Font Atlas,改动不会真正生效。我见过有人调了参数发现没变化,以为是Bug,其实是漏了Generate这一步。
第四个经验是关于“全字库”的悲观结论。网上有一些教程会教你用Unicode全部汉字甚至全部CJK字符生成字体,动辄几万字,我试过一次,生成的.asset文件几百MB,编辑器直接卡死,项目彻底跑不动。TMP生成汉字是按图集页数走的,超过一定量级,资源占用和加载时间都会指数级上升,这套路千万别学。7000字是一个比较甜的点,再多就得按项目实际用字裁剪。
6. 往远了说:多语言与按需加载
6.1 多语言如何安排字库
如果项目要做多语言,中文字符集只是第一关,后面还有繁体、日文、韩文等着你。我的经验是不要所有语言共用一个Font Asset,而是每种语言单独生成一份,在语言包切换时同步切换字体资源。具体做法可以给TMP组件写一个脚本,监听语言变更事件,读取对应语言的TMP_FontAsset并赋值,或者直接用Addressables按语言包加载字体,避免初始包过大。
繁体字库和简体字库的差异比很多人想象的大,台湾地区的用语和用字习惯也不完全同于香港,简体7000字里很多字在繁体中字形完全不同,直接拿简体字体渲染繁体文本会大量缺字。日文更是除了汉字还要有平假名、片假名,字符集范围和简体中文差异很大,别想着一套字库走天下。
6.2 扫描、裁剪字符集:从7000字到项目实际字库
前面说的7000字是模板,不是终点。我在项目后期一定会做一次字符集瘦身,方案很朴素:写一个编辑器脚本,遍历场景里所有TMP文本组件,再扫描所有Prefab和资源配置文件里的字符串字段,把这些字符串拆成字符集合,输出去重后的txt,然后拿这个txt重新生成Font Asset。这样字体资源基本能减到只有项目实际用到的字符,包体和内存都能明显下降。
这里要提醒一句:如果游戏里有玩家输入、聊天系统、网络返回数据,这类动态文本是扫描脚本扫不到的,所以不建议直接砍掉动态兜底方案。我一般的做法是:静态文本用扫描出来的精简字体,输入框和网络文本用Dynamic模式或者一个带Fallback的通用字体,保证不会走到“缺字方块”这一步。
扫描字符集脚本的核心逻辑大概是这样:
using System.Collections.Generic; using System.IO; using System.Text; using UnityEditor; using UnityEngine; public static class CharsetScanner { [MenuItem("Tools/扫描TMP字符集")] public static void ScanAllTmp() { HashSet<char> chars = new HashSet<char>(); // 假设你有方法获取场景中所有TMP组件,这里省略 // string text = tmp.text; // foreach (char c in text) chars.Add(c); // 遍历Prefab、ScriptableObject也可类似处理 StringBuilder sb = new StringBuilder(); foreach (char c in chars) sb.Append(c); File.WriteAllText("Assets/charset_project.txt", sb.ToString(), Encoding.UTF8); AssetDatabase.Refresh(); } }这段代码只是脚手架,实际使用要自己补齐如何收集所有TMP文本的方法。如果你项目里文本都集中在某些ScriptableObject或Excel导出的Json里,也可以让脚本去解析那些文件,思路是一样的。把字符集扫描做成CI流程的一部分,策划每次改文本后都自动跑一遍,字体资源永远是跟着内容走的,这才是最理想的维护状态。
最后再分享一个小技巧:把打磨好的7000字模板和一份项目精简字库都保存好,新项目启动时先用7000字模板生成字体,保证开发期什么字都不缺,等文本稳定再切换精简字库。很多团队忽视了这个过渡方案,开发到一半突然发现策划加了一段生僻字剧情,整个UI全变方块,临时去补字库、重生成、重新打包,浪费一整天。提前把模板字库准备好,这种幺蛾子基本就杜绝了。