ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity 中文不显示全解析:TextMeshPro 字体图集与编码乱码排查

Unity 中文不显示全解析:TextMeshPro 字体图集与编码乱码排查 Unity 里中文不显示这事看着是个小问题实际能把人卡一整天。我自己第一次遇到是在做数字孪生的一个工厂看板项目界面上所有中文标签全变成了□□□英文和数字一切正常查了半天以为是字体没打包最后发现是 TextMeshPro 的字体图集里压根就没烘中文字形。后来陆续在 Android 包、WebGL 包、微信小游戏里都撞过类似的坑每次的表现还都不一样有的是方块有的是纯空白有的是编辑器里好好的、真机上一片虚无。这篇就把 Unity 中文不显示这个问题从根上拆一遍从旧版 UI Text 到 TextMeshPro从字体图集怎么烘到打包后为什么掉再到输入法、串口、HTTP 数据里的中文乱码全都按实操顺序讲清楚。不管你是刚学 Unity 的新手还是已经在带项目的老手只要你的界面里有中文这篇基本能覆盖你 90% 的翻车场景。1. 先定位中文不显示到底卡在哪一层很多人一看到中文不显示就去换字体换完还是不行然后开始怀疑人生。问题在于 Unity 里中文不显示至少有四个完全不同的成因分别落在字体文件层、字体资产层、纹理图集层和字符编码层。你如果不先分清是哪一层换多少次字体都是白费功夫。我的习惯是先看症状再看组件最后才动资源顺序反了就是纯浪费生命。1.1 四种症状对应的其实是四种完全不同的根因方块或空心方框是最常见的。这代表字体资产里有这个字符的索引但拿不到字形数据系统就画了一个 missing glyph 占位符。TMP 默认的缺失字形就是一个小方框旧版 Text 也类似。这种情况 99% 是字体资产本身没包含中文字形。完全空白、什么都不画比方块更隐蔽。通常是渲染层面的问题字体资产引用丢了、材质球没了、图集纹理没加载成功或者 SDF 生成的图集里这个字符对应的矩形区域是空的。WebGL 上如果纹理图集被压缩成了不支持的格式也会表现为整体空白。显示成问号或乱码这就不是渲染问题了而是编码问题。典型场景是从串口读回来一串字节、从 HTTP 拿回来一段 JSON你用错误的编码方式把它转成 string中文自然就烂了。这种情况你换一百个字体也没用得去查 UTF-8 和 GBK 的转换。只有部分字缺失比如测试能显示曦鑫显示成方块。这是静态字体图集只烘了常用字集生僻字没进去。这个最好解决加动态补充或者扩字符集就行。1.2 为什么英文字体从来不出问题一到汉字就翻车核心差异在字形数量。ASCII 可打印字符一共 95 个加控制字符也就 128 个任何一个字体文件都能轻松塞进一张 1024×1024 的纹理里。而 CJK 基本区U4E00–U9FA5有 20902 个汉字再加上标点、全角符号、扩展区轻轻松松三四万字形。第二个差异是字形复杂度。拉丁字母的轮廓点通常几十个一个汉字几百个点是常态龘这种字能上千。这意味着同样的采样精度下单个汉字占用的图集面积更大SDF 生成也更慢。第三个差异是体积。一个只含拉丁字符的 TTF 可能 200KB一个完整的思源黑体 SC Regular 大概 10MB 到 16MB而烘成 SDF 图集之后4096×4096 的 RGBA 纹理一张就是 64MB 显存压缩后小一些但也不便宜。所以做中文字体方案本质上是在覆盖多少字图集多大显存多少这三个变量之间做取舍而不是简单换个字体文件。1.3 一条三分钟定位路径照着走基本不会跑偏我通常按这个顺序查效率最高先选中出问题的 Text 或 TextMeshProUGUI 组件看它引用的 Font Asset 到底是谁。双击那个 Font Asset打开 Inspector 里的 Character Table在搜索框里输入一个不显示的字看能不能搜到。搜不到说明字符集没包含问题在字体资产层去重新生成或加动态模式。搜得到但 Inspector 右上角预览图里对应位置是空的说明图集没烘上多半是图集尺寸不够或者生成时中断了。都正常去看材质球和 Shader 有没有丢尤其是改了渲染管线Built-in 换 URP之后。以上都没问题才去怀疑编码层抓原始数据看字节。提示TextMeshProUGUI 只能接受 TMP_FontAsset旧版 Text 只能接受 FontTTF 导入后的 Font 对象。往 TMP 组件里拖一个 TTFUnity 不会报错但也不会显示很多人就在这一步被坑过。2. 旧版 UI Text默认字体里根本没有汉字虽然现在新项目基本都用 TextMeshPro 了但大量存量的老项目、外包接手的项目、以及 Unity 官方一些示例里还是能看到旧版 Text 组件。它在中文显示上的坑跟 TMP 完全不同得单独说。2.1 LegacyRuntime.ttf 的真实身份以及为什么它一个汉字都没有Unity 在 2022.1 之前内置字体叫 Arial.ttf2022.1 之后改名叫 LegacyRuntime.ttf路径在Resources/unity_builtin_extra里。名字换了但有一点没变它就是个纯拉丁字体不含任何 CJK 字形。所以新建一个 Text 组件默认字体就是它。你输入中文它找不到字形就给你画方块。这不是 Bug是设计如此。很多人第一次遇到会以为是编码问题其实压根不是。要改成中文字体最直接的做法是把一个含中文的 TTF 拖进工程。但拖进去只是第一步导入设置里还有几个参数决定了它到底能不能正常显示中文。2.2 导入中文字体时这几个参数必须逐个确认选中 TTF 文件在 Inspector 里重点看这几项参数建议值说明Font Size16 或 32影响默认字形尺寸动态字体下影响不大Rendering ModeSmoothHinted Raster 在小字号下更清晰但仅限 Windows 渲染CharacterDynamic关键项见下文Ascent Calculation ModeFace Metrics用字体自身的度量位置更准Include Font Data勾选不勾选的话打包后可能找不到字体Font Names保留原样运行时通过名字查找时会用到其中Character这一项最要命。它有三个主要选项Dynamic运行时按需把用到的字符渲染进字体纹理。中文项目基本都选这个因为它不会一开始就吃掉几 MB 纹理而是用到哪个字加哪个字。Unicode导入时预烘字体包含的所有字符。一个完整中文字体会让 Unity 卡很久生成一张巨大的字体纹理通常不推荐。ASCII 起始 / ASCII 扩展只烘拉丁字符这时候中文必然不显示就是它造成的。Include Font Data也要勾上。它的作用是把这个字体文件本身打进包体里运行时动态取字形。如果不勾Unity 会去调用操作系统字体PC 上可能还能蒙对Android 和 WebGL 上基本就是空。注意微软雅黑、苹方这类系统字体是有版权风险的商业项目里别直接用。中文项目我一般推荐思源黑体Source Han Sans SC、阿里巴巴普惠体、HarmonyOS Sans、霞鹜文楷这类开源或明确可商用的字体。换字体不解决显示问题但用错字体可能让你的问题从技术问题变成法务问题。2.3 动态字体与预烘图集到底该怎么取舍旧版 Text 使用动态字体时Unity 会在运行时维护一张 font texture用到新字符就往里加。听起来很美但有两个副作用。第一个是卡顿。每加一批新字符纹理会重建Texture Rebuild在低端 Android 机上能明显感觉到界面一顿。数字孪生项目里那种地名、设备名随机出现的场景最容易触发。第二个是图集溢出。动态字体纹理有尺寸上限一般 4096×4096装满之后就不能再加新字符了新字会变成方块或者用其他字体回退。这个上限在运行时不会报错只是悄悄不显示非常难查。我的处理方式是常用字比如通用规范汉字表一级字表的 3500 字预先用动态模式跑一遍把图集固定下来然后转成非动态。项目里可以写个编辑器脚本遍历所有 Text 组件的内容把它们用到的字符都提前喂一遍。这样既能保证覆盖又不会在运行时突然卡一下。3. TextMeshPro 中文字体资产全流程实操现在项目里 90% 的中文显示问题都出在 TMP 这边。原因很简单TMP 用的是自建的 Font Asset 体系跟你直接拖一个 TTF 完全不是一回事。理解不了这套体系你永远只能靠试。3.1 为什么 TMP 的中文时好时坏看起来毫无规律TMP 的核心机制是SDFSigned Distance Field 纹理图集。它把字体的字形轮廓转换成一张带距离信息的灰度图渲染时通过 Shader 采样这张图来还原字形边缘。好处是同一个字体资产可以在任意字号下保持清晰不用为每个字号单独烘一张图。代价就是所有要用到的字符必须预先存在于图集里。图集里没有的字符TMP 不会去 TTF 里现取除非开了 Dynamic 模式直接给你画一个 missing glyph。而图集容量是有限的。一张 4096×4096 的图集在采样字号 60、padding 5 的设置下粗略估算能装三千多个字符具体数字取决于字形的最大包围盒。你项目里如果混了常用字加生僻字加专业术语很容易就跑出这个范围。表现出来就是大部分字正常个别字变方块看起来毫无规律其实规律很清楚——就是图集装不下了。3.2 采样字号与图集尺寸的换算动手前先算一遍生成 Font Asset 之前我建议先做一次粗算避免反复重来。算法很简单每个字符占用的图集面积约为(采样字号 2 × padding)²图集总面积是宽 × 高两者相除就是理论上限。采样字号Padding单字符估算面积2048×2048 可容纳4096×4096 可容纳909108² ≈ 11664约 359 字约 1438 字60570² 4900约 856 字约 3424 字48456² 3136约 1337 字约 5350 字36342² 1764约 2378 字约 9511 字注意这是理论上限实际因为字形包围盒大小不一、排布有空洞能装的数量会再打个七八折。所以想要一张图集放下 3500 常用字采样字号建议控制在 48 到 60 之间如果你做的是大字号标题比如 72px 以上采样字号就得往上提那就别指望一张图集搞定了得开 Multi Atlas Textures 或者分层。采样字号本身怎么选经验值是它应该接近你项目里最大的实际显示字号。如果你的正文是 32px标题是 64px那采样字号取 64 到 72 比较合适。取太小放大后会糊取太大图集浪费。3.3 Font Asset Creator 里每一项参数的实测建议打开Window TextMeshPro Font Asset Creator下面是我在一个中型项目里跑通的一套配置Source Font File选你的中文 TTF/OTF。注意必须是导入设置里 Include Font Data 勾选过的。Sampling Point Size选 Custom Size填 60。如果你的界面最大字号不超过 40填 48 更省图集。Padding填 5。Padding 的作用是给 SDF 留出描边、阴影、发光效果的空间。如果你要用 Outline 或 Underlay得加大到 9 甚至 12纯实心字 4 到 6 就够。Padding 每加 1图集消耗涨得很快别乱给。Packing MethodFast。Optimum 模式排得更紧但生成慢很多中文几千字的时候你可能要等十几分钟。Atlas Resolution4096×4096。2048 只在字符集很小的时候用。Character Set这是最关键的一项。下拉里选Custom Range或者Unicode Range (Hex)。常用的中文 Unicode 区间3000-303F CJK 符号和标点 FF00-FFEF 全角 ASCII、全角标点 4E00-9FA5 CJK 统一汉字基本区20902 字如果你不想一次烘两万字可以在Custom Characters里粘贴一份常用字表比如通用规范汉字表的一级字表 3500 字加上你项目里的专有名词。这样图集利用率最高。Render Mode选SDFAA。它生成最快质量对绝大多数 UI 场景够用。如果做超大号标题字可以试SDFAA_HINTED或者SDF32但生成时间会翻好几倍。Get Kerning Pairs勾上。中文虽然不太依赖字距调整但中英混排时有帮助。点Generate Font Atlas等进度条跑完检查预览图有没有大面积空白或者方块然后Save as...存成 Font Asset。一定要看预览图图集溢出的话预览里直接就缺字这时候回头调参数比打包后再查省事得多。3.4 静态图集加动态补充再加 Fallback三段式方案最省心单靠一张静态图集覆盖不了所有场景。我在项目里用的是一套三段式方案第一段静态主图集。用 3500 常用字加项目专有名词采样字号 604096 图集生成一个 Font Asset设为项目默认字体。第二段开动态补充。在 Font Asset 的 Inspector 里把Atlas Population Mode设为Dynamic并勾选Multi Atlas Textures。这样运行时遇到静态图集里没有的字TMP 会自动从源字体取字形加进去装满一张就再开一张。// 运行时主动把一批字符补给字体资产避免首次显示时卡顿 public TMP_FontAsset mainFont; void PreloadCharacters(string chars) { // TMP 3.0 及以上版本支持返回 false 表示有字符加不进去 bool ok mainFont.TryAddCharacters(chars, out string missing); if (!ok) { Debug.LogWarning($以下字符未能加入字体图集{missing}); } } // 单个字符查询排查缺字时很好用 void CheckGlyph(char c) { if (!mainFont.HasCharacter(c)) Debug.LogWarning($字体资产中缺少字符{c}); }第三段配 Fallback 链。在 TMP SettingsAssets/TextMesh Pro/Resources/TMP Settings.asset里把主字体资产的Fallback Font Assets列表配上。比如主字体是思源黑体回退可以配一个思源宋体再回退一个只含符号的字体。这样某个字体缺字时会自动往下找而不是直接画方块。提示动态模式在多张图集下会持续占用显存移动端要盯着点。可以在关键界面加载完后调一次fontAsset.ClearFontAssetData()里的相关清理接口或者干脆在关卡切换时做一次资源回收。3.5 避头尾和行距中文排版里最容易被忽略的隐藏坑中文显示出来了不代表排版就对了。我见过不少项目字是能显示的但标点符号跑到行首或者逗号句号孤零零挂在下一行开头看着特别业余。TMP 提供了两个设置来处理这个问题在TMP Settings里Line Breaking Following Characters不允许出现在行首的字符填中文的收尾标点比如)]、,.;:!?…—以及中文全角的。、》」』Line Breaking Leading Characters不允许出现在行尾的字符填中文的起始标点比如([《「『和全角【《「『如果这两个列表是空的TMP 就按西文规则断行中文标点自然乱跑。填完之后标点会自动被挤到正确的位置。行距是另一个问题。中文字体的 Line Height 通常比拉丁字体大直接沿用默认值会造成行与行之间要么挤在一起要么松得离谱。可以在 Font Asset 的Face Info里手动调Line Height、Ascent Line、Descent Line或者在建 Text 组件时调Line Spacing参数单位是百分比负值收紧正值放宽。我一般先调字体资产的 Face Info这样所有引用它的组件都是统一的。4. 编辑器正常、打包后就没了平台差异排查这是最让人抓狂的一类问题。编辑器里预览一切正常打出 Android 包或者 WebGL 包中文全变方块。这类问题基本都出在资源打包和平台差异上。4.1 Android 和 iOS 的字体裁剪与资源剔除Unity 的托管代码裁剪Managed Stripping Level在 IL2CPP 下默认是 Medium 或 High。它会把看起来没被引用的代码删掉。TMP 的动态字体逻辑有一大块是通过反射和接口调用的裁剪器有时候识别不到就会把关键方法删了。表现出来就是编辑器正常、真机缺字。处理办法是在Assets/link.xml里保留 TMP 相关程序集linker assembly fullnameUnity.TextMeshPro preserveall/ assembly fullnameUnityEngine.TextCoreFontEngineModule preserveall/ /linker另一个常见原因是Include Font Data 没勾。编辑器里 Unity 会用系统字体兜底你感知不到问题打包到手机上系统里没这个字体直接就空了。所以凡是用了自定义字体的项目这个勾必须确认。还有一种情况是字体资产被资源剔除机制误伤。如果你的 Font Asset 只在运行时通过代码或 Addressables 加载构建时可能被判定为未引用而剔除。把它放进Resources文件夹或者在 Addressables 里显式标记为可寻址资源。4.2 WebGL 和微信小游戏体积、图集加载和首帧卡顿WebGL 平台上中文问题的根源常常是体积和加载时机。一张 4096×4096 的 RGBA 图集未压缩就是 64MB。WebGL 里不能随便用 ASTC 或 ETC2只能用 DXT 或者不压缩加载时间长用户可能在你图集到位之前就看到界面了。我的做法是在 WebGL 上把图集压到 2048字符集收窄到项目实际用到的 2000 字以内然后用一份很小的加载场景先把字体资产预加载完再进入主界面。微信小游戏那边限制更明显。小游戏包体积有硬上限纹理内存也很紧张。我一般会把中文字体的字符集压到几百字只保留界面上的固定文案用 Custom Characters 精确列出来采样字号降到 40 到 48图集 2048。这样一张图集能控制在几百 KB。注意小游戏环境下动态字体图集的运行时写入不一定可靠特别是一边加载一边加字的时候。稳妥做法是全部静态烘好运行时只读。宁可多花点时间做字符集收敛也别指望运行时空降字形。顺便说一句WebGL 上用 IDBFS 做本地存储的时候如果你用中文做文件名或键名写不进去或者读不出来多半也是编码问题——这类问题跟字体无关但症状很像容易误判。4.3 运行时图集膨胀的监控与收敛开了动态模式之后图集是会持续增长的。我见过一个项目跑了两小时图集从一张变成四张显存多吃了两百多兆低端机直接崩。监控方式很简单写个小工具定时打印当前字体资产的图集数量和内存占用// 放在开发构建里定时输出图集状态 void DumpAtlasInfo(TMP_FontAsset font) { Debug.Log($图集数量{font.atlasTextures.Length}); long total 0; foreach (var tex in font.atlasTextures) { if (tex null) continue; total (long)tex.width * tex.height * 4; } Debug.Log($图集估算显存{total / 1024f / 1024f:F2} MB); }收敛手段有两个一是把界面文案全部静态化尽量不用运行时拼接随机字符串二是对玩家输入、服务器下发这类不可控文本单独用一个动态字体资产跟主 UI 字体隔离方便单独清理。数字孪生这类场景里设备名、报警信息经常是动态下发的特别要注意这一点。5. 输入与外部数据里的中文另一类不显示前面讲的都是渲染不出来还有一类是数据本身就是错的。这两类问题症状相似处理方式完全不同得分清楚。5.1 TMP_InputField 和中文输入法TMP_InputField 在 PC 上输入中文理论上没问题但有几个坑。一是输入法候选框位置不对。这个跟Character Limit、Content Type有关某些组合下 TMP 会拒绝多字节输入。把 Content Type 设成 Standard 或者 Custom允许所有字符一般能解决。二是输入中文后光标跳位。这是 TMP 的 caret 位置计算没有正确处理 CJK 全角字符导致的常见于使用了自定义字体资产、且字体资产的字符宽度信息不完整的情况。检查一下字体资产的 glyph 表里有没有全角字符的宽度数据。三是移动端的软键盘问题。Android 上TouchScreenKeyboard在某些输入法下会返回空字符串尤其是输入中文拼音还没上屏的时候。稳妥做法是在onValueChanged里过滤掉纯拼音中间态只在onEndEdit时取值。5.2 串口、HTTP、JSON、CSV 读进来变问号这是彻底的编码问题。典型场景下位机用 GBK 编码发中文你在 Unity 里直接Encoding.UTF8.GetString(bytes)结果就是一串问号或者乱码。正确的做法是明确指定编码// 串口读到的字节按约定的编码解析 byte[] buffer serialPort.ReadExistingBytes(); string text Encoding.GetEncoding(GBK).GetString(buffer); // 也别忘了注册编码提供程序否则 .NET Core/IL2CPP 下找不到 GBK // 在启动时调用一次 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);注意在 IL2CPP 后端下Encoding.GetEncoding(GBK)会因为缺少编码表而抛异常。必须先在初始化阶段调用Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)再使用。这个坑我在两个项目里都踩过报错信息还不明显容易误判成串口本身的问题。HTTP 那边相对简单只要服务端返回的Content-Type里带了charsetutf-8用 UnityWebRequest 拿到的downloadHandler.text一般就是对的。如果没有这个头那就得自己从data里转string text Encoding.UTF8.GetString(req.downloadHandler.data);CSV 和 Excel 导入也一样。如果策划给的表是 GBK 编码的 CSV读进 Unity 直接变乱码。建议在流程上就要求统一用 UTF-8 with BOM能省掉一大半沟通成本。5.3 中文路径、中文文件名和资源加载还有一个很容易被忽略的点中文路径。Resources.Load(模型/建筑)这种写法在编辑器里可能正常打包后可能失败。原因是打包后资源名可能被重新编码或者某些平台的打包工具不支持非 ASCII 文件名。我的建议很直接工程内的所有路径、文件名、资源名、Prefab 名全部用英文加数字。中文只出现在界面显示文案里。这条规则看着土但能规避掉一整类玄学问题。同理Application.persistentDataPath底下自己创建的文件夹也用英文。如果服务器下发的 JSON 里带中文键名也尽量在接口层就做映射转换别把中文键名一路带到 Unity 的数据层。6. 排查速查表与实测踩坑记录前面按类型拆完了这里补一张速查表方便你遇到问题时直接对号入座。6.1 症状、原因、处理对照表症状最可能的原因处理方式中文全是方块字体资产不含中文字形重新生成 Font Asset 或启用 Dynamic 模式中文处完全空白字体资产引用丢失 / 图集纹理未加载检查组件引用、材质球、资源打包设置只有少数生僻字是方块静态图集字符集不全扩字符集或开启动态补充编辑器正常真机缺失Include Font Data 未勾 / 代码裁剪勾选 Include Font Data补 link.xmlWebGL 上首次进入缺字图集未加载完就渲染做字体资产预加载场景微信小游戏缺字字符集过大被裁剪或内存不足收敛字符集到实际用字降采样字号串口/网络数据是乱码编码不匹配GBK vs UTF-8注册编码提供程序按约定编码解析输入框中文字符丢失Content Type 限制改为 Standard 或自定义允许所有字符标点跑到行首未配置中文避头尾字符在 TMP Settings 里填 Leading/Following 列表行距忽大忽小字体的 Face Info 度量不准手动调整 Line Height 和 Ascent/Descent6.2 我踩过的几个坑你可能也会遇到第一个坑换了字体但没重新生成 Font Asset。TMP 不会因为你替换了源字体文件就自动更新图集。你必须在 Font Asset Creator 里重新生成或者在 Font Asset 的 Inspector 里点 Update Atlas Texture。我第一次遇到的时候对着换了三次字体愣是没意识到改的是 TTFTMP 用的是之前烘好的图集。第二个坑图集预览正常实际显示缺字。有次生成时进度条卡了一下我点了取消结果 Unity 还是把半成品保存了。Inspector 预览里看着不错实际用的时候发现边界区域的字全是空的。所以生成完一定要滚动预览图看一圈特别是边缘。第三个坑URP 升级后中文变粉红。TMP 的 Shader 在 URP 下用的是TextMeshPro/Distance Field变体如果材质球的 Shader 没跟着换会显示成粉红或者透明。这时候重新创建材质或者跑一遍Window TextMeshPro Install TMP Essential Resources能解决。第四个坑中文输入测试数据用中文标点。写测试脚本的时候随手用了中文逗号做分隔符结果解析的时候一直失败。虽然不算显示问题但排查方向完全跑偏了半天。现在我的测试数据一律用英文标点显示文案才用中文。第五个坑字体资产的材质球被批处理改了。有次为了做性能优化写了个脚本批量给所有 TextMeshProUGUI 创建材质实例结果把字体资产共享的材质也改了导致所有中文的描边参数错乱。字体资产的材质是共享资源改之前一定要克隆。最后分享一个我现在的习惯每接一个新项目第一天就把中文字体资产的生成流程跑通写成一个编辑器菜单项把字符集来源常用字表加项目术语表也一起版本管理起来。这样后面文案有更新重新生成一次就行不会出现某个界面突然冒出一个方块字这种尴尬情况。字符集的收集其实不难遍历一下工程里所有 Prefab 和场景把 TextMeshProUGUI 和 Text 组件的文本内容抽出来去重再并上通用规范汉字表的一级字表基本就能覆盖 99% 的实际用字。
返回列表