ARTICLE DETAIL

资讯详情

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

Godot 4中文字体优化:子集化、多语言与导出打包一站式指南

Godot 4中文字体优化:子集化、多语言与导出打包一站式指南 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-filechars.txt \ --output-filesubset-result.otf \ --flavorwoff2 \ --layout-features*关键参数解释一下--text-file指定你提取的字符集文件。--flavorwoff2输出 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], encodingutf-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}, --flavorwoff2]) 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 还是 POGodot 的本地化系统入口是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跟编辑器运行不一样——编辑器里字体是即时从工程目录加载的导出时则要走“导出器文件系统”。如果你的字体文件没有被导出器纳入包体运行时就会变成“找不到字体”。这里有三种常见命运字体作为资源被正常导入并导出这就是我们要的结果。字体文件太大被导出器排除Godot 的导出配置里如果启用了按后缀过滤如 exclude 掉.otf那就直接吊销了。字体是系统字体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.ttfWindows 下编辑器运行没事打包到 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 显示成方块从哪一步开始查症状运行游戏中文都变成“□□□□”。排查顺序检查字体文件本身是否支持中文字形。用字体查看器打开文件手动输入一个“中”看有没有字形轮廓。没有的话换字体。检查 TextServer 的 Debug 输出。Godot 会在控制台打印缺少 glyph 的警告看到Cannot find glyph之类的信息就说明字体真缺字不是代码问题。检查 Fallback 链。如果主字体缺字你有没有配置 fallback在 FontVariation 资源里把 fallback 字体的优先级排对。检查导出后字体是否还在。导出包解开看 .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能自动化生成的绝不手动导出。这样你的字体和本地化体系才算真正配到位后面不管是加五种语言还是适配多个平台都会轻松很多。
返回列表