从自制游戏中提取出日语输入组件,作为 Godot 4 用的 addon 公开了
自作ゲームから日本語タイピング部品を切り出して、Godot 4用のaddonとして公開しました
引言
我把自制打字枪动作游戏《TypeGunner》(Godot 4)中用于用罗马字输入日语的输入组件切割出来并公开,以便也能用于其他游戏。
- Godot 4.6 系列,typed GDScript
- MIT License、v0.1.0
- 无外部依赖
本文先简要介绍它能做什么,然后写从游戏中剥离时考虑的设计,以及切割的步骤。如果只想了解导入方法,请看 README 的“导入”。
能做什么
游戏侧只需“给作为打字对象的 Node 附上假名并注册”即可。
TypingEngine.register_target(enemy, {"kanji": "日本語", "kana": "にほんご"})
之后玩家按键时,addon 会与罗马字候选进行比对,并在每输入一个字符时、以及单词输入完成时通过 signal 通知。
罗马字并非只有一种写法,常见的输入方式都会被一并接受。例如下面这些输入都算正确。
| 假名 | 接受的输入方式(部分) |
|---|---|
| し |
shi / si / ci
|
| ち |
chi / ti
|
| ふ |
fu / hu
|
| しゃ |
sha / sya / shixya(拆分输入) |
| きって |
kitte / kixtsute / kiltute 等 |
| かんじ |
kanji / kannji / kaxnji
|
| ティー |
thi- / texi-
|
由于片假名会按平假名处理,所以像 コーヒー 这样的词也可以直接登记。
此外,还包含以下部件。
-
TypingDisplay.tscn:显示假名以及罗马字的部件,其中已输入部分、下一个字符和剩余部分用不同颜色区分 -
KeyboardDisplay.gd:绘制 JIS/US 配列键盘,显示负责手指的颜色和下一个按键的练习用部件 -
WordPicker.gd:从词汇中挑选出题词的static函数
为什么单独拆出来
契机是我想在接下来要做的游戏中也使用日语打字。虽然也可以复制来使用,但考虑到要把罗马字输入方式的修正(比如“ん”的处理等)加到两边很麻烦,还是在一个地方修改再分发给两边更轻松。
因此采用“1 addon = 1 仓库”的方式,游戏侧则以 git submodule 的形式引入到 addons/typing_core 中。
设计中决定的事项
依赖只有“游戏 → addon”这一个方向
在抽离出来时,把 addon 可以知道的内容限定在以下范围内。
- 哪个 Node 是让输入哪个假名的对象
- 现在已经输入到第几个字符、正在聚焦哪个对象
伤害、分数、击破演出全都是游戏侧的工作。addon 只发出 signal,不参与接收后如何处理。
signal word_completed(unit: Node)
signal char_typed(unit: Node)
signal mistype(unit: Node)
signal no_target_hit
signal focus_changed(unit: Node)
对象的生命周期交给 Node 管理
类型目标就是场景树上的 Node 本身。注册时预先连接到 tree_exiting,Node 消失后会自动解除注册。
var exit_callable: Callable = _on_target_tree_exiting.bind(unit)
if not unit.tree_exiting.is_connected(exit_callable):
unit.tree_exiting.connect(exit_callable, CONNECT_ONE_SHOT)
得益于此,游戏侧只要在打倒敌人时执行 queue_free() 即可,不容易再因为忘记解除而出现「能攻击不在画面上的敌人」的情况。另外,未加入树的 Node 不会收到 tree_exiting,所以只有这种情况需要自己执行 unregister_target()。
用「第一个字符」来 focus
画面上有多个对象时,在未 focus 任何对象的状态下输入的第一个字符,会在候选匹配的对象中 focus 先注册的那个。此后,直到用 Tab 或 Esc 取消 focus 之前,输入只会进入该对象。
按这个规则,当开头字符相同的词并列出现时,就无法选中后出现的词。于是给出题侧的 WordPicker.pick() 加了一个用于排除画面上词语开头字符的参数。当条件导致候选为空时,只去掉开头字符的条件;如果仍为空,就从所有词中挑选——按这个顺序必定返回 1 个词。
把罗马字转换拆分为无状态的部件
当初输入判定和罗马字候选生成都在一个 Autoload 里,但在公开前把候选生成拆分到了 RomajiConverter.gd。它是一个只接收假名字符串、返回每个输入单位候选的无状态部件。
# 「かんじ」を区切った結果
# か ["ka", "ca"] / ん ["nn", "xn", "n"] / じ ["ji", "zi", "nji", "nzi"]
转换表已外置到 kana_romaji.json(假名 → 罗马字的数组)中,想增加输入方式时只需编辑 JSON 即可。
不过只有「っ」和「ん」不是由表、而是由代码决定的。因为这两者的正确输入都会随前后的假名而变化。
- 「っ」:接受重复下一个假名辅音的输入(
kitte)。末尾的「っ」只接受xtsu等单独输入 - 「ん」:当下一个假名以元音、や行、な行开头时,只打一个
n无法与下一个假名区分,因此要求输入nn/xn(「きんえん」的「ん」为nn或xn)。末尾的「ん」只用n - 在用一个
n就能打出「ん」的位置,也接受接着再打一个n的输入(kannji)
由于不持有状态,因此无需启用 Autoload 就能只测试这个部件。这类边界情况已固定在 tests/run_romaji_converter_tests.gd 中。
显示跟随输入的拼写
TypingDisplay 会把剩余分段的首个候选连接到已输入的字符上显示。当把「にほんご」打到 nihonn 时,显示会变成 nihonn + go,并按照玩家选择的拼写显示下一个字符。
候选的排列顺序直接关系到显示,因此,例如在「ん」后面加上 n 的候选会放在原候选的后面。因为如果放在前面,「せんせい」就会显示成 sennnsei。
不依赖放置位置的读取
由于不知道 addon 会被放在哪里,所以 JSON 和其他 script 都从与自己 script 相同的文件夹读取。
var base_dir: String = get_script().resource_path.get_base_dir()
var converter_script: GDScript = load(base_dir.path_join(ROMAJI_CONVERTER_FILE)) as GDScript
另外,没有使用 class_name 和 preload。由于不使用 class_name,所以不会与导入目标游戏中的全局类名发生冲突。
使其能够单独对 addon 进行测试
tests/run_tests.sh 会在临时文件夹中创建一个最小的 Godot 项目,复制 addon,将 TypingEngine 注册为 Autoload,然后运行测试。即使不引入游戏本体,仅靠 addon 仓库也能进行回归测试。
GODOT=/path/to/godot sh tests/run_tests.sh
拆分步骤
并非一下子移到另一个仓库,而是分阶段推进。
- 在 TypeGunner 中,把与打字相关的文件集中到
addons/typing_core/,并去除对游戏固有 Autoload 的引用 - 把分散在 3 处的出题词抽选集中到
WordPicker.gd(与旧实现比对 3000 次,确认结果一致) -
用
git subtree split把该文件夹连同其历史记录一起迁移到新仓库 - 将 TypeGunner 侧的
addons/typing_core替换为 submodule - 作为发布前的整理,进行罗马字转换的拆分、键位布局的 JSON 化、片假名与外来语假名(「てぃ」「ゔぁ」等)的追加、MIT License 与
plugin.cfg的添加
在步骤 1 的阶段已经在游戏内切断了依赖,因此 3 和 4 只需移动文件即可完成。迁移后也在 TypeGunner 中确认了运行情况。
制限
在 v0.1.0 阶段,以下事情无法做到。
- 汉字的输入。若在
kana中包含转换表中没有的字符,则该词无法打出(注册时会给出警告) - 符号和数字的输入。可接受的按键只有
a~z和-(长音符号)。 -
传给
WordPicker的词汇的romaji需要自行编写。它用于判断首字符和长度。
结语
制作日语打字游戏时,罗马字输入方式的差异以及“ん”“っ”的处理,都是颇费工夫的地方。如果能对用 Godot 制作类似游戏的人有所帮助,我会很高兴。导入步骤和 API 已整理在 README 中。
来源:zenn · 游戏开发社区 · zenn.dev