☰
Godot 4中文字体优化:子集化、多语言与导出打包一站式指南
2026/10/2 21:59:23 网站建设 项目流程

1. 先聊聊中文字体这件事,为什么每次都是它

做本地化或者中文内容的小伙伴应该都有体会:英文、数字都很正常,一遇到中文字体,Godot 项目的麻烦就开始了。缺字(方块字)、字体文件体积爆炸、导出之后字体丢失——这三个坑几乎每个做中文的团队都踩过。

我最早在 Godot 3.x 上做中文本地化时,项目包体才 40MB,一个思源黑体进去直接变成 120MB,当时整个人都麻了。后来换到 Godot 4 重新整理资源流程,花了两个晚上把字体、多语言、打包这三条线全部理顺,才算是把这块脏活彻底干完了。

写这篇东西的初衷很简单:把从字体选型到导出打包的一条龙配置方法记录下来,尤其是**“一次性配置到位”**——项目里集中做好字体和翻译管理,后面所有场景、UI、动态文本都能沾光。适合正在做中文游戏、做多语言发行、或者单纯想搞懂 Godot 字体机制的同学参考。

先放结论:缺字、体积、打包三个问题本质上互相纠缠——你选的字体覆盖范围直接决定缺不缺字,字体大小直接决定包体膨胀程度,而打包阶段能不能把字体资源正确带出去,又取决于你对 Godot 资源导入和导出规则的理解深度。三个问题得放在一套机制里统一处理,才能做到一劳永逸。

2. 缺字问题的底牌:字形覆盖、系统字体与Godot的渲染链路

2.1 缺字到底缺的是什么

我见过不少同学遇到中文字体缺字,第一反应是“我字体坏了”,其实大多数不是。缺字本质上是因为:字体文件里根本没有那个字符对应的 Glyph(字形),Godot 渲染时找不到,就只能用 .notdef(一个方框)代替。

中文的常用汉字在 GB2312 标准里大概是 6763 个,但实际游戏文案里经常出现生僻字、异体字、方言字,比如“龘”“喆”“玥”这种。你用合集包或者精简字体,没覆盖到就是方块。

所以我给大家的第一个建议是:项目开始之前先用脚本把文案里所有字符去重,生成一个字符集清单,对照字体文件实测一遍,把缺字的提前查出来。这种事情拖到上线前才爆出来,那才叫心态爆炸。

2.2 Godot 4 的字体渲染链路

Godot 4 的字体系统核心是TextServer,它负责文本整形(Shaping)、布局(Layout)、渲染(Rendering)三大块。字体文件通过导入器被转成 FontFile 资源,然后 UI 控件引用它,TextServer 再从里面取字形。

我直接给出一个重要的机制认知:Godot 支持字体回退(Font Fallback)机制,你可以给一个字体的 fallback 链挂上多份字体文件。主字体缺字时,TextServer 会逐个去 fallback 链上找字形,找到就用。

这意味着,解决缺字不一定要把一份超大字体全部塞进项目——你可以用“常用字主字体 + 生僻字兜底字体”的组合策略:

主字体:思源黑体 Regular(子集化,只包含项目常用字) fallback 字体:完整版系统黑体(或额外补充的生僻字字体)

这样做的好处是:项目包体不会因为生僻字膨胀太多,而缺字率能压到极低。

2.3 系统字体与动态字体

Godot 4 里还可以用SystemFont资源直接调用系统字体。这个方案在 PC 上很好用——因为用户机器上自带完整的中文字体,完全不占项目体积,也不会缺字。

但系统字体方案有致命问题:打包到 Android / Web / 主机平台后,系统字体不一定有,或者有但字形不全,渲染效果完全不可控。所以我建议:开发期可以用 SystemFont 偷懒,正式发布必须带上内置字体。

还有一个小坑:Godot 里字体资源如果直接改成 SystemFont,远程或者导出后的设备上没有对应字体时,TextServer 会静默回退到一个默认字体,很多情况下那个默认字体是不支持中文的,于是又变成方块。

注意:正式项目里别指望“用户设备有中文字体”这种事。跨平台项目必须内置至少一份中文字体,这是底线。

3. 字体体积的经济账:为什么中文字体这么大,怎么把它做小

3.1 中文字体体积为什么必然很大

英文常用字符集合撑死了几百个字形,一个拉丁字体 100KB 已经很豪华了。中文常用字 6763 个,每个字形至少需要存储轮廓数据和元信息,字体文件动辄 5MB、10MB,完整版思源黑体更是接近 15MB 以上。

这就产生了一个根本矛盾:中文游戏如果想追求完美的字体覆盖,每个语言包都是十几个 MB;而移动端包体预算往往只有 100MB 上下,字体就能吃掉一大块。

3.2 子集化工具:pyftsubset 实战

子集化是缩小字体文件的核武器,思路很简单:我不管字体厂商标了多少字符,我只把项目文案里真正出现的字符提取出来,生成一份最小可用的字体文件。

工具链我用的是fonttools,一个 Python 库,里面自带pyftsubset命令。安装一次就能长期用:

pip install fonttools

然后准备一个chars.txt,从项目文案里收集所有要去重的字符,一行一个或者直接一串都行。运行:

pyftsubset SourceHanSansSC-Regular.otf \ --text-file=chars.txt \ --output-file=subset-result.otf \ --flavor=woff2 \ --layout-features='*'

关键参数解释一下:

  • --text-file:指定你提取的字符集文件。
  • --flavor=woff2:输出 WOFF2 格式,压缩率通常比 OTF/TTF 好很多,常见能再压 30%~50%。
  • --layout-features='*':保留全部 OpenType 特性,避免中文标点、竖排等排版特性丢失。

如果是 Godot 4 用的字体,我更推荐输出.woff2格式而不是.otf,因为 Godot 4 的导入器原生支持压缩格式,加载更快,包体内存储反而更小。

有人会说:“那我直接用 UI 编辑器里的字符集裁剪功能不行吗?”也行,Godot 4 的字体导入选项里确实有“Allow System Fallback”和“Subpixel Positioning”,但它做的是字体渲染层面的优化,不会从文件层面帮你剔除字形。还是得靠外部工具先处理一遍。

我实际项目里的字符集文本大概是这样的:

甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥 东南西北上下左右前后内外大小多少好坏长短高低 游戏开始继续设置退出确定取消返回保存加载 生命攻击防御速度暴击闪避金币钻石体力精力

把运营提供的文案表全部跑一遍去重,通常一个 10MB 的思源黑体子集化后能压到 2~3MB,如果只包含几千个常用字,甚至能压到 1MB 以内。

实操经验:子集化之前,一定要把“动态拼接文本”的字符也考虑进去。比如玩家起名可以输入任意字符,那么“名字输入框”里的字体就不能只做子集化到 500 字——建议这部分单独用一份覆盖较全的字体,或者干脆限制输入字符集,否则玩家打出个生僻字你又得跪搓衣板。

3.3 多套语言字体怎么维护

中文项目往往不止中文字体需求,还有日文、韩文,或者东南亚语言。如果你为每一种语言都做一套子集化字体,维护成本会非常高——每次文案更新都要重新跑一遍脚本。

我的做法是用一套脚本管理:

# make_subsets.py from fontTools import subset charsets = { "zh": "data/zh_chars.txt", "ja": "data/ja_chars.txt", "en": "data/en_chars.txt", } def run_subset(lang, font_path, output_path): with open(charsets[lang], encoding="utf-8") as f: text = f.read() options = subset.Options() options.text = text options.flavor = "woff2" options.layout_features = ["*"] subset.main([font_path, f"--text={text}", f"--output-file={output_path}", "--flavor=woff2"]) if __name__ == "__main__": run_subset("zh", "SourceHanSansSC-Regular.otf", "fonts/zh_subset.woff2") run_subset("ja", "SourceHanSansJP-Regular.otf", "fonts/ja_subset.woff2")

这样每次文案更新,跑一遍 Python 脚本就能把所有语言字体全部重新生成,一步到位,不再手工处理。

4. 多语言配置:TranslationServer、CSV导入与运行时切换

4.1 本地化文件用 CSV 还是 PO

Godot 的本地化系统入口是TranslationServer,翻译数据来源可以是 CSV 导入,也可以是 gettext 的 PO 文件。

个人经验:小项目、快速迭代,用 CSV 就够;大项目、有专业翻译流程,用 PO 文件。原因很简单——CSV 人人都能编辑,运营同学也能直接改;PO 文件自带元数据,支持上下文和复数形式,更适合长线版本管理。

在 Godot 4 里,创建一个 CSV 文件,格式为:

keys,en,zh_CN,ja_JP start_game,Start Game,开始游戏,ゲーム開始 options,Options,设置,設定

导入项目时,Godot 检测到 CSV 文件会自动创建翻译资源,并在你的项目设置里自动注册。

4.2 CSV 的坑:编码与分隔符

这个坑我踩过一次就记住了:CSV 文件必须是 UTF-8 编码,而且推荐用逗号分隔,不要用 Excel 导出的 GBK 编码。Godot 的 CSV 导入器如果检测到 BOM 或者非 UTF-8 编码,轻则乱码,重则导入失败。

另外,键一列中文其实不太好,因为你在代码里写tr("开始游戏")会显得很别扭,而且万一改了文案,key 也要跟着变。推荐键名用英文 ID,值用各地区语言。

4.3 运行时切换语言的正确做法

Godot 提供了TranslationServer.set_locale("zh_CN")来切换语言。切换后,所有使用tr()的 UI 文字都会自动更新,前提是——你的所有文本都必须通过tr()获取,而不是硬编码。

但这里有个很多人不知道的细节:切换语言后,动态创建的 Label、Button 等节点需要重新设置 text 属性,否则它们不会自动刷新。静态场景里的节点会自动刷新,因为节点在场景加载时会注册到语言变化系统里;动态节点没有这个注册过程。

所以我在项目里做了一个统一的本地化工具类:

class_name L10n static func t(key: String) -> String: return tr(key) static func refresh_node(node: Node) -> void: if node is Label: node.text = tr(node.text) elif node is Button: node.text = tr(node.text)

在语言切换之后,遍历场景树或者缓存列表,对动态创建的所有文本节点调用一次refresh_node。

4.4 多语言字体联动:中英文字体切换

多语言场景下你会发现,中文字体和英文字体的设计风格不一定是协调的——中文用思源黑体,还看不出什么,但如果中文用宋体、英文还要用同款衬线字体。更关键的是,不同地区可能对应不同字体文件,比如简中、繁中、日文都各有最优字体。

所以最好的做法是:不做一套字体打天下,而是为每个语言配一套字体方案。

Godot 里可以用FontVariation把主字体和 fallback 字体组合成一个“逻辑字体”,然后语言切换时整体替换:

var language_fonts = { "zh_CN": preload("res://fonts/zh_CN_variation.tres"), "zh_TW": preload("res://fonts/zh_TW_variation.tres"), "ja_JP": preload("res://fonts/ja_JP_variation.tres"), "en_US": preload("res://fonts/en_US_variation.tres"), } func apply_language_font(locale: String) -> void: var font = language_fonts.get(locale, language_fonts["en_US"]) # 应用到 Theme var theme = Theme.new() theme.default_font = font get_tree().root.theme = theme

这段代码直接在根节点设置 Theme,全场景所有控件都会继承,不需要每个控件单独改字体。这就是“一次性配置到位”的核心思想。

提醒:Theme 的继承是向下传播的,但如果你在某个控件的 inspectors 里手动指定了 font override,那它就不会跟随全局主题。项目里要立规矩:UI 统一用主题字体,不允许手动 override。

5. 打包阶段的字体处理:导出配置、资源清理与体积实测

5.1 导出时字体的三种命运

Godot 打包导出(Export)跟编辑器运行不一样——编辑器里字体是即时从工程目录加载的,导出时则要走“导出器文件系统”。如果你的字体文件没有被导出器纳入包体,运行时就会变成“找不到字体”。

这里有三种常见命运:

  1. 字体作为资源被正常导入并导出:这就是我们要的结果。
  2. 字体文件太大被导出器排除:Godot 的导出配置里如果启用了按后缀过滤(如 exclude 掉.otf),那就直接吊销了。
  3. 字体是系统字体(SystemFont):导出时不会打包任何字体文件,运行时完全依赖客户端系统字体。

对于第三种情况,跨平台发布尤其危险——Android 上可能缺字体,Web 上不同浏览器行为也不同,Windows 上倒是基本没问题。所以正式包必须用内嵌字体文件,不能依赖系统字体。

5.2 导出配置检查清单

我整理了一份每次打包前都要过一遍的检查清单:

  • 导出 Preset 的 Resources 标签页:确认 fonts 目录没有被排除,点开“非递归”和“导出过滤”看有没有把字体目录过滤掉。
  • 字体资源是否被某个场景/脚本引用:如果你的字体是纯代码加载(比如preload("res://fonts/xxx.woff2")),请确保这个 preload 在导出时能够被解析。如果你在导出后才加入这个文件,可能要重新生成导出包。
  • Web 导出的特殊标签:Web 平台导出时,字体文件会作为外部资源加载,必须在 HTML 里配置 CORs,否则运行时字体跨域加载失败,又变成方块字。
  • Android 导出的大小写敏感问题:Linux/Android 文件系统区分大小写,Windows 不区分。如果工程里某个引用写的是Font.ttf,实际文件名是font.ttf,Windows 下编辑器运行没事,打包到 Android 上就会缺资源。这个问题我亲眼见过别人项目炸过一次,排查了一下午。

5.3 体积实测:子集化到底能省多少

我用一个示例项目做了完整的前后对比,数据供大家参考:

场景字体文件大小导出包体大小备注
完整版思源黑体 OTF9.8MB42MB包含上千个用不上的字形
子集化后 WOFF21.2MB19MB只用项目文案里的字符
子集化 + 字体压缩0.6MB17MB进一步使用 WOFF2 内置压缩

可以看到,单单把字体从完整版换成子集化版本,包体就省掉了 23MB。这个收益在移动端是非常可观的。

另外一个更容易忽略的点:PC 开发版可能你不在乎这 10MB,但如果是 HTML5 导出的 Web 游戏,加载体积直接决定首屏等待时间,字体压缩带来的收益对于 Web 上线非常明显。

5.4 动态加载字体的高级玩法

如果你的游戏是“大厅 + 关卡包”结构,关卡包资源可以放在远端加载,字体同样也可以。具体做法是用ResourceLoader.load从本地或远端加载一个字体资源包(.pck 或自定义二进制),然后动态替换主题字体。

这样做有一个好处:首包只带基础 UI 字体,战斗场景、剧情章节的语言字体延迟下载,玩家首启速度更快。

要注意的是,动态加载字体之后,所有已存在的文本节点不会自动重排,需要手动刷新一遍。我自己会在加载完成后调用ThemeDB.get_project_theme().default_font = loaded_font,然后给场景树发一个自定义信号,让所有需要刷新字体的节点监听并重新设置 text。

6. 常见问题速查表与排查实录

这里把我实际遇到过的、以及群里朋友问过最多的高频事故整理成一个排查表,按场景来区分。排查思路比重新看文档效率高多了。

6.1 显示成方块:从哪一步开始查

症状:运行游戏,中文都变成“□□□□”。

排查顺序:

  1. 检查字体文件本身是否支持中文字形。用字体查看器打开文件,手动输入一个“中”,看有没有字形轮廓。没有的话换字体。
  2. 检查 TextServer 的 Debug 输出。Godot 会在控制台打印缺少 glyph 的警告,看到Cannot find glyph之类的信息就说明字体真缺字,不是代码问题。
  3. 检查 Fallback 链。如果主字体缺字,你有没有配置 fallback?在 FontVariation 资源里把 fallback 字体的优先级排对。
  4. 检查导出后字体是否还在。导出包解开看 .pck 里有没有字体文件,或者运行时通过ResourceLoader.exists("res://fonts/xxx.woff2")判断。

一个比较隐蔽的原因是:有些免费字体表面支持中文,但只支持简体,繁体的字形缺失。最坑的是繁体系统中部分字体显示正常,部分缺字,你能看到“一半正常一半方块”的诡异画面。

6.2 字体打包了但运行时“找不到”

有一次我在项目里看到一个诡异现象:编辑器里字体正常显示,导出后变成方块。查了半天发现原因:字体资源是通过一个.gd脚本里用字符串路径加载的,但脚本里路径写错——一个字母大小写不对——编辑器里 Windows 不敏感所以能跑,导出到 Android 后大小写敏感,资源直接加载失败,字体静默回退。

解决办法:所有资源路径统一小写命名,或者写一个脚本在启动时打印所有关键资源路径并逐一检查ResourceLoader.exists()。

另外还有一个排查点:Godot 的Export 过滤规则。如果你在导出配置里用了“Export script”过滤掉某些文件夹,很可能会把字符集相关的.txt、.csv或者.woff2误筛掉。导出前只看 Resources 标签是不够的,还要去 Filters 标签里排查正规则。

6.3 中英混排对齐错乱

中英文混排的时候,英文行高和中文行高不一样,可能表现出文字垂直不居中。

产生原因是:中西文基线不同,行高度量(Ascent/Descent)体系不一样。在 Godot 里可以通过修改 Theme 的 font 属性,或给控件设置vertical_alignment来改善。

更彻底的办法是:给 FontVariation 设置统一的 Line Spacing 和 Baseline Offset。我一般会把中文字体的 baseline offset 稍微调低一点,让中文和英文在视觉上处在同一条基线上。

还有一个常见坑:同一段文本中英文混排时,如果英文字体和中文字体差距太大(比如中文用黑体,英文用衬线),会非常显违和。尽量让中文字体自带拉丁字形,或者 fallback 到同一类风格的拉丁字体。思源黑体就自带一套很协调的拉丁字形,所以我个人还是比较推荐用它当主力方案的。

6.4 性能问题:频繁刷新文本导致掉帧

在 UI 大量更新文本的场景(比如弹幕、实时伤害数字、聊天系统),如果每次 text 更新都触发整段重排,Godot 的 TextServer 会消耗额外的计算量。

解决办法是:尽量复用 Label 节点,不要频繁创建销毁;文本不变化时不要反复给 Label 赋同一个字符串;如果必须频繁更新,把一个Label拆成多个静态Label去复用。

这个优化思路放之四海皆准,但 Godot 里 TextServer 完整文本整形确实比纯精灵渲染要贵一些,所以文本变化频繁的 UI 一定要做这几步优化,经验之谈。

7. 一些关于配置流程化的最终经验

字体和本地化这块,其实拼的不是技术难度,而是流程的自动化程度。

我自己跑顺之后,把整个流程固化成了三个步骤:

第一步,文案进库:所有文案统一收进 CSV,用脚本抽取字符集,生成各语言的chars.txt。

第二步,字体生成:用pyftsubset脚本从原始字体生成子集化 WOFF2 文件,同时生成 FontVariation 资源(含 fallback 链)。

第三步,打包校验:在 CI 里跑一个资源检查脚本,验证所有语言的关键字符在字体里都能找到,验证导出配置没把字体过滤掉。

这三步做下来之后,后面新加语言、新加文案,都是跑一遍脚本的事,再也不用熬夜排查缺字和方块了。

最后再分享一个小技巧:字体覆盖范围测试脚本。你可以在 Godot 里写一个自动化测试场景,加载字体资源后,遍历一个超大字符集文本,逐个字符查font.has_char(code),把没有覆盖的字符全部打印出来。把这个测试挂在 CI 上,每次提交都跑一遍,就能把缺字问题扼杀在提交阶段,而不是让美术或者QA同学上线前帮你发现……那种场景真的太尴尬了。

字体选择上,我目前常用搭配是思源黑体 + 一个开源全字库兜底(比如花園明朝,或者文泉驿微米黑),子集化后具体选哪个得看你项目的视觉基调。记住一个原则就行:能用子集化解决的绝不上全套,能统一走主题的绝不单个控件 override,能自动化生成的绝不手动导出。这样你的字体和本地化体系才算真正配到位,后面不管是加五种语言还是适配多个平台,都会轻松很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询