饥荒Mod调试日志:从致命崩溃到UI实时刷新

2025-08-24

背景

继续之前的MOD开发:为 Todolist(待办事项)模块增加中文输入支持


1. 自定义输入的局限性

目标:解决 Todolist 模块的中文输入法兼容性问题

原本通过监听键盘按键构筑了一个自定义输入框,但也因此只能监听键盘符号,无法输入中文

两种输入方式

  • 自定义键盘监听 (OnRawKey):
    • 工作原理: 直接捕获键盘的物理按键码(如 KEY_A, KEY_B
    • 优点: 实现简单,对 ASCII 字符处理直接
    • 缺点: 完全绕过了操作系统的输入法引擎(IME)。由于中文输入依赖于输入法对多个拼音按键的组合OnRawKey 只能识别到独立的英文字母,无法处理组合后的汉字
  • 原生输入界面:
    • 工作原理: 调用游戏引擎内置的 UI 控件,该控件已与操作系统的 IME 对接
    • 优点: 完美支持所有系统级输入法,包括中文、日文等
    • 缺点: 需要理解并正确调用游戏的 UI 框架和组件

结论:废弃自定义输入方案,调用原生输入界面

2. 利用原生 writeable 组件

查阅游戏原生代码 homesign.lua(路牌),我发现输入功能由 writeable 组件提供。任何挂载了该组件的实体,都可以为玩家弹出一个支持输入法的标准输入框

新方案

  1. 创建: 当用户点击“添加任务”时,在后台动态创建一个临时的、不可见的 homesign 实体
  2. 绑定: 为临时路牌的 writeable 组件设置 SetOnWrittenFn 回调函数,用于接收输入结果
  3. 调用: 执行 writeable:BeginWriting(),为玩家弹出原生输入界面
  4. 处理: 在回调函数中获取输入的文本,并调用 atlas_todolist 组件的 AddTask 方法
  5. 销毁: 无论输入成功或取消,最终都销毁该临时路牌实体,防止资源泄漏

3. 全局变量与加载时机

方案实施过程中,游戏启动时遭遇了致命崩溃

第一次崩溃日志:

[string "../mods/CanKaoMod/modmain.lua"]:439: attempt to call global 'Vector3' (a nil value)

分析: 在 modmain.lua 的顶层作用域直接调用了 Vector3。在饥荒 Mod 环境中,这类引擎内置对象必须在游戏完全初始化后才能使用,且需要通过 GLOBAL 表访问

修复:

  1. Vector3(...) 修改为 GLOBAL.Vector3(...)
  2. 将整个输入界面布局的配置代码块移入 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 实例的生命周期 绑定

  1. 创建时监听: 在 UI 的构造函数 _ctor 中创建并设置所有全局事件监听器
  2. 销毁时移除: 在 UI 的 Close() 函数(当 UI 实例被销毁时)中移除这些监听器
  3. 简化状态切换: OnBecomeActiveOnBecomeInactive 只负责处理游戏暂停/恢复等与可见状态相关的逻辑,不再干预监听器的生命周期

总结

  • 加载时机: 饥荒 Mod 开发中,对游戏原生对象的访问( Vector3, writeables)必须延迟到 AddSimPostInit 之后,使用 GLOBAL 表和 pcall(require, ...) 进行安全调用
  • UI 生命周期: 事件监听器的生命周期应与 UI 实例的创建/销毁绑定,而不是与可见/不可见状态绑定,以确保 UI 在后台也能正确响应数据更新,避免出现“延迟刷新”问题。
  • 官方组件优先 :优先复用官方提供组件,有效减少潜在的Bug并降低长期维护成本