背景
继续之前的MOD开发:为 Todolist(待办事项)模块增加中文输入支持
1. 自定义输入的局限性
目标:解决 Todolist 模块的中文输入法兼容性问题
原本通过监听键盘按键构筑了一个自定义输入框,但也因此只能监听键盘符号,无法输入中文
两种输入方式
- 自定义键盘监听 (
OnRawKey
):
- 工作原理: 直接捕获键盘的物理按键码(如
KEY_A
,KEY_B
)- 优点: 实现简单,对 ASCII 字符处理直接
- 缺点: 完全绕过了操作系统的输入法引擎(IME)。由于中文输入依赖于输入法对多个拼音按键的组合
OnRawKey
只能识别到独立的英文字母,无法处理组合后的汉字- 原生输入界面:
- 工作原理: 调用游戏引擎内置的 UI 控件,该控件已与操作系统的 IME 对接
- 优点: 完美支持所有系统级输入法,包括中文、日文等
- 缺点: 需要理解并正确调用游戏的 UI 框架和组件
结论:废弃自定义输入方案,调用原生输入界面
2. 利用原生 writeable
组件
查阅游戏原生代码 homesign.lua
(路牌),我发现输入功能由 writeable
组件提供。任何挂载了该组件的实体,都可以为玩家弹出一个支持输入法的标准输入框
新方案:
- 创建: 当用户点击“添加任务”时,在后台动态创建一个临时的、不可见的
homesign
实体 - 绑定: 为临时路牌的
writeable
组件设置SetOnWrittenFn
回调函数,用于接收输入结果 - 调用: 执行
writeable:BeginWriting()
,为玩家弹出原生输入界面 - 处理: 在回调函数中获取输入的文本,并调用
atlas_todolist
组件的AddTask
方法 - 销毁: 无论输入成功或取消,最终都销毁该临时路牌实体,防止资源泄漏
3. 全局变量与加载时机
方案实施过程中,游戏启动时遭遇了致命崩溃
第一次崩溃日志:
[string "../mods/CanKaoMod/modmain.lua"]:439: attempt to call global 'Vector3' (a nil value)
分析: 在 modmain.lua
的顶层作用域直接调用了 Vector3
。在饥荒 Mod 环境中,这类引擎内置对象必须在游戏完全初始化后才能使用,且需要通过 GLOBAL
表访问
修复:
- 将
Vector3(...)
修改为GLOBAL.Vector3(...)
。 - 将整个输入界面布局的配置代码块移入
AddSimPostInit(function() ... end)
中,确保其在游戏模拟(Simulation)完全初始化后执行
第二次崩溃日志:
[string "../mods/CanKaoMod/modmain.lua"]:443: variable 'writeables' is not declared
分析: 修复 Vector3
后,writeables
模块同样存在加载时机问题。直接在 AddSimPostInit
中访问 GLOBAL.writeables
仍然可能因为加载顺序问题而失败
修复:
在 AddSimPostInit
内部,使用 pcall
安全地 require
模块。这是游戏组件(如 writeable.lua
)自身的标准做法
-- modmain.lua
AddSimPostInit(function()
local success, writeables = GLOBAL.pcall(GLOBAL.require, "writeables")
if success and writeables and writeables.AddLayout then
-- ... 配置代码 ...
end
end)
4. UI 实时刷新失败
功能逻辑跑通后,发现了新的问题:新添加的任务不会立即显示,需要关闭再重新打开UI、切换视图才能看到
分析:
问题的在于 UI 事件监听器的生命周期管理
查阅 atlasbook_ui.lua
,可以得知事件监听器(如 task_update_listener
)的设置和移除与 UI 的可见状态绑定:
- 在
OnBecomeActive()
(UI 变为可见/活跃) 时,设置监听器 - 在
OnBecomeInactive()
(UI 变为不可见/非活跃) 时,移除监听器
当 UI 关闭后,它将无法接收到任何数据更新事件。只有当 UI 再次打开,监听器被重新设置后,才能刷新
修复: 重构事件监听器的生命周期,使其与 UI 实例的生命周期 绑定
- 创建时监听: 在 UI 的构造函数
_ctor
中创建并设置所有全局事件监听器 - 销毁时移除: 在 UI 的
Close()
函数(当 UI 实例被销毁时)中移除这些监听器 - 简化状态切换:
OnBecomeActive
和OnBecomeInactive
只负责处理游戏暂停/恢复等与可见状态相关的逻辑,不再干预监听器的生命周期
总结
- 加载时机: 饥荒 Mod 开发中,对游戏原生对象的访问(
Vector3
,writeables
)必须延迟到AddSimPostInit
之后,使用GLOBAL
表和pcall(require, ...)
进行安全调用- UI 生命周期: 事件监听器的生命周期应与 UI 实例的创建/销毁绑定,而不是与可见/不可见状态绑定,以确保 UI 在后台也能正确响应数据更新,避免出现“延迟刷新”问题。
- 官方组件优先 :优先复用官方提供组件,有效减少潜在的Bug并降低长期维护成本