Unity 文字显示带竖线问题,一个肉眼看不见的 U+200C,让位图字体渲染成竖线
一、问题现象
反馈:剧情对话里,出现了大量莫名其妙的竖线,穿插在正常文字之间,严重影响观感。

起初反馈视频画质模糊,且竖线集中出现在同一段对话里,一度怀疑是录屏或模拟器的显示问题。但随后在真机上也稳定复现,排除了设备因素。
跟进后确认:竖线直接出现在 NGUI 的 UILabel 渲染结果里,并非贴图或 UI 层叠导致。检查了相关代码逻辑,也没有任何"单独插入竖线"的代码------至此基本可以断定,问题出在文本数据本身(翻译文案)。
打开翻译源文件,肉眼却看不到任何竖线。直到把光标移到对应文字处、左右移动,才发现那里藏着一个不易察觉的字符------它不占明显宽度,肉眼几乎不可见,但渲染时却变成了一条竖线。
二、排查定位
2.1 第一反应:加空白字符筛查
既然定位到"看不见的字符",第一反应是在逻辑里加一层筛查替换,把各种不可见/空白字符过滤掉。于是写了一个 IsWhiteSpace 判断,把常见的空白字符枚举了一遍:
csharp
bool IsWhiteSpace(char c)
{
// 普通空格、制表符、换行符等
if (char.IsWhiteSpace(c))
return true;
// 其他不可见字符
switch (c)
{
case '\u00A0': // 不换行空格 (No-Break Space)
case '\u2000': // 半身空距 (En Quad)
case '\u2001': // 全身空距 (Em Quad)
case '\u2002': // 半身空距 (En Space)
case '\u2003': // 全身空距 (Em Space)
case '\u2004': // 三分之一全身空距 (Three-Per-Em Space)
case '\u2005': // 四分之一全身空距 (Four-Per-Em Space)
case '\u2006': // 六分之一全身空距 (Six-Per-Em Space)
case '\u2007': // 数字空距 (Figure Space)
case '\u2008': // 标点空距 (Punctuation Space)
case '\u2009': // 窄空距 (Thin Space)
case '\u200A': // 最窄空距 (Hair Space)
case '\u200B': // 零宽空格 (Zero Width Space)
case '\u202F': // 窄不换行空格 (Narrow No-Break Space)
case '\u205F': // 中数学空距 (Medium Mathematical Space)
case '\u3000': // 全角空格 (Ideographic Space)
case '\uFEFF': // 零宽不换行空格 (Zero Width No-Break Space)
case '\u0009': // 制表符
case '\u000A': // 换行
case '\u000D': // 回车
case '\u001C': // 文件分隔符
case '\u001D': // 组分隔符
case '\u001E': // 记录分隔符
case '\u001F': // 单元分隔符
return true;
default:
return false;
}
}
结果:问题字符串依然没有被筛查出来。
2.2 关键一步:逐字符打印 code point
既然枚举没覆盖到,索性放弃"猜字符",直接把字符串每个字符单独打印出来,看它到底是什么:
csharp
for (int i = 0; i < strValue.Length; i++)
{
Debug.Log($"char={i} code=U+{(int)strValue[i]:X4}");
}

打印结果立刻现形------那个"看不见的字符"是 U+200C。
三、根因:U+200C 是什么
U+200C 是 ZERO WIDTH NON-JOINER(零宽非连接符,简称 ZWNJ),一个 Unicode 格式控制字符:
| 属性 | 值 |
|---|---|
| 码点 | U+200C |
| 名称 | ZERO WIDTH NON-JOINER(零宽非连接符) |
| Unicode 类别 | Format(Cf),而非 White_Space(Zs) |
| 用途 | 抑制连字/合字,常出现在阿拉伯文、波斯文等需要控制字符连接关系的场景 |
两个关键点解释了整件事:
-
为什么
char.IsWhiteSpace抓不到它 :.NET 的char.IsWhiteSpace只认 Unicode 类别为Zs(空格分隔符)及少数\t\n\r等,而 U+200C 的类别是Format(Cf),因此char.IsWhiteSpace('\u200C')返回 false 。上面第一版IsWhiteSpace虽然手列了 20 来个空白字符,但漏掉了 U+200C(它压根不是空白,而是格式符),所以筛查失败。 -
为什么渲染成竖线 :U+200C 本是"零宽、不可见"的控制符。但 NGUI 的 UILabel 用的是位图字体(Bitmap Font) ,字体图集里只包含导出时收录的字形。当图集里没有 U+200C 对应的 glyph 时,NGUI 会把它当作"缺字"处理,渲染成替换字形------在视觉上就表现为一条竖线(或缺字方块)。
来源 :U+200C 这类字符多来自网页文档复制、在线翻译工具、富文本编辑器粘贴进翻译文案,从源头很难 100% 规避。
四、修复方案
通过日志打印发现,有几十条翻译都带了这个字符。既然源头无法百分百规避,决定两条腿走路:
- 工具筛查导出:对翻译文件做一次性扫描,把含 U+200C 的条目定位出来人工修正;
- 运行时兼容兜底 :加载文本时直接
Replace掉 U+200C,确保线上不再出现竖线。
csharp
const char ZWNJ = '\u200C';
bool ContainsU200C(string str)
{
if (string.IsNullOrEmpty(str))
return false;
return str.IndexOf(ZWNJ) >= 0;
}
// 定位:打印出问题字符的位置与前后文,便于定位是哪条翻译
for (int i = 0; i < strValue.Length; i++)
{
if (strValue[i] == '\u200C')
{
Debug.LogError(
$"发现 U+200C,index={i}, 前后=[{strValue[Mathf.Max(0, i - 1)]}|{strValue[i]}|{strValue[Mathf.Min(strValue.Length - 1, i + 1)]}]"
);
break;
}
}
// 兜底:直接删除零宽非连接符
strValue = strValue.Replace("\u200C", "");
经过测试,竖线问题得以修复。
五、排查要点总结
- "看不见的字符"别靠猜 :枚举再全也可能漏,最快的办法是逐字符打印 code point (
U+XXXX十六进制),一眼定位。 char.IsWhiteSpace有盲区 :它只覆盖Zs空白类别,Format(Cf)类的零宽控制符(如 U+200C)不在此列,需单独处理。- 零宽/格式字符家族要一起防:除 U+200C(ZWNJ)外,常见的还有 U+200D(ZWJ 零宽连接符)、U+200B(ZWSP 零宽空格)、U+FEFF(BOM/零宽不换行空格)等,翻译/粘贴场景建议一并筛查。
- 位图字体缺字会变形:NGUI/UIGUI 位图字体对图集外的码点没有 glyph,会渲染成替换字形(竖线/方块);排查文字异常时优先怀疑文本数据里混入了控制符。