跳转至

03 输出与输入

上一节脚本只写了日志。日志要打开文件才能看到,玩家在游戏画面里看不到任何变化。本节做两件事:在游戏画面里显示一条消息,以及读取键盘按键让脚本对玩家操作做出反应。

做完以后,能看到两个现象:游戏加载时画面上出现一条文字消息,按下 F9 时画面上再出现一条消息。

两种输出位置

脚本有两种途径把信息展示给用户:

方式 输出位置 什么时候看到
ctx.LogInfo(...) ModLoader/ModLoader.log 文件 打开日志文件时
BML::UI::AddMessage(...) 游戏画面左下方的 BML 短消息区域 消息发出的瞬间,在画面里

日志适合记录"之后回头查"的信息,比如初始化步骤、错误原因、调试数据。它不会打扰游戏画面,也不会因为消息太快而看不清。

游戏内消息适合"马上让玩家看到"的反馈,比如"mod 已加载"、"按键触发了"、"配置已切换"。它显示几秒后会自动消失。

实际开发中,经常两边都写。日志里记详细信息方便排查,游戏内消息给一个简短的反馈让玩家知道发生了什么。

在 OnLoad 里发送游戏内消息

HelloMod.mod.asOnLoad 改成:

void OnLoad(const BML::ModContext &in ctx) {
    BML::UI::AddMessage("HelloMod loaded.");
    ctx.LogInfo("HelloMod loaded from ModLoader/Mods/HelloMod.mod.as");
}

保存后,已加载的脚本会自动热重载,游戏画面上会出现 HelloMod loaded. 的消息, 日志文件里也有对应记录。

BML::UI::AddMessage(...) 接收一个字符串参数,直接显示在画面里。不需要获取句柄,也不需要 null 检查;调用一次就添加一条消息。

直接记录日志

ModContext 直接提供三个带当前 Mod 前缀的日志方法:

ctx.LogInfo("normal information");
ctx.LogWarn("recoverable problem");
ctx.LogError("operation failed");

普通 Mod 代码优先使用这些方法。ctx.BorrowLogger() 是进阶接口,只在确实需要 保存一个可重新验证的 Logger wrapper 时使用;它不是记录一行日志的必经步骤。

键盘输入:OnProcess 回调

到目前为止,脚本只在游戏启动时执行一次(OnLoad)。要响应键盘,需要每帧检查按键状态。BML 提供了 OnProcess 回调,每一帧调用一次。

HelloMod 类里加一个新方法:

void OnProcess(const BML::ModContext &in ctx) {
    BML::InputHook@ input = ctx.BorrowInputManager();
    if (input is null) {
        return;
    }

    if (input.IsKeyPressed(CKKEY_F9)) {
        BML::UI::AddMessage("F9 pressed!");
        ctx.LogInfo("F9 pressed");
    }
}

保存并等待热重载后按 F9,画面上出现 F9 pressed!

轮询模型

BML 的输入系统是轮询(polling)模型,不是事件(event)模型。这个区别很重要:

  • 事件模型:按下按键时,系统主动通知你"F9 被按下了"。你写一个处理函数等着被调用。
  • 轮询模型:你每帧主动去问"F9 现在是什么状态?"。如果是按下的状态,你自己决定做什么。

BML 用的是轮询模型。所以检查按键的代码必须放在 OnProcess 里,因为它每帧都执行。如果放在 OnLoad 里只执行一次,那一次检查之后就再也不检查了。

BorrowInputManager

ctx.BorrowInputManager() 返回一个 BML::InputHook@ 句柄。和 Logger 一样,用 @ 句柄并检查是否为空。InputHook 提供了检查键盘和鼠标状态的方法。

IsKeyDown 和 IsKeyPressed 的区别

InputHook 提供了两个检查按键的方法,它们的行为不同:

方法 含义 按住 F9 不放时的行为
IsKeyPressed(key) 按键刚按下的那一帧 只在第一帧返回 true,后续帧全部返回 false
IsKeyDown(key) 按键当前是否处于按下状态 每一帧都返回 true,直到松开

用一个具体的场景来理解:假设你按住 F9 不放,持续了 60 帧(大约 1 秒),然后松开。

帧:  1    2    3    4    5   ...  60   61
     v按下                          ^松开
IsKeyPressed:  true  false false false false ... false false
IsKeyDown:     true  true  true  true  true  ... true  false

什么时候用哪个

IsKeyPressed 适合"执行一次"的动作:

  • 按 F9 打开/关闭一个窗口
  • 按 F9 执行一条命令
  • 按 F9 切换一个开关

如果用 IsKeyDown 做这些事情,玩家按住稍微久一点,动作就会重复执行很多次(窗口反复开关、命令重复触发)。

IsKeyDown 适合"持续进行"的动作:

  • 按住 W 向前移动
  • 按住空格施加力
  • 按住某个键让数值持续增长

如果用 IsKeyPressed 做这些事情,玩家按住按键时只有第一帧有效果,后续帧没有反应。

实验

把代码改成 IsKeyDown

if (input.IsKeyDown(CKKEY_F9)) {
    BML::UI::AddMessage("F9 is held!");
}

保存并等待热重载后按住 F9 不放,会看到消息连续出现。这就是 IsKeyDown 在每帧 触发的效果。改回 IsKeyPressed 后,同样按住 F9,消息只出现一次。

按键常量

BML 使用 Virtools 的键盘常量命名。常用的几个:

CKKEY_F1 ~ CKKEY_F12     功能键
CKKEY_A ~ CKKEY_Z        字母键
CKKEY_0 ~ CKKEY_9        数字键(主键盘)
CKKEY_SPACE              空格
CKKEY_RETURN             回车
CKKEY_ESCAPE             Esc
CKKEY_LSHIFT             左 Shift
CKKEY_LCONTROL           左 Ctrl

这些常量定义在 as.predefined 里,编辑器能识别和补全。如果输入 CKKEY_ 后没有补全提示,检查 as.predefined 文件是否在工作区里。

当前完整代码

把本节的内容整合起来,HelloMod.mod.as 现在应该是:

[bml.mod id="hello.script" name="Hello Mod" version="1.0.0" author="Tutorial" bml="0.3.13" description="Minimal tutorial script mod"]
class HelloMod {
    void OnLoad(const BML::ModContext &in ctx) {
        BML::UI::AddMessage("HelloMod loaded.");
        ctx.LogInfo("HelloMod loaded from ModLoader/Mods/HelloMod.mod.as");
    }

    void OnProcess(const BML::ModContext &in ctx) {
        BML::InputHook@ input = ctx.BorrowInputManager();
        if (input is null) {
            return;
        }

        if (input.IsKeyPressed(CKKEY_F9)) {
            BML::UI::AddMessage("F9 pressed!");
            ctx.LogInfo("F9 pressed");
        }
    }
}

排查

现象 检查
游戏画面里没有消息 OnLoad 有没有执行(看日志);热重载是否成功
按 F9 没反应 OnProcess 是否在 class 内部;方法签名是否正确
一按键消息刷很多条 是否错用了 IsKeyDown,应该换成 IsKeyPressed
编译提示找不到 CKKEY_F9 as.predefined 是否在 VS Code 工作区里(编辑器报错);实际运行中这个常量由 BML 注入,不会找不到
编译提示 BorrowInputManager 不存在 检查 as.predefined 版本是否和 BML 版本匹配

完成状态

脚本能在游戏画面显示消息(BML::UI::AddMessage),能写日志(Logger.Info),并能响应键盘按键(IsKeyPressed)。已经掌握了两种输出方式的区别和两种按键检测的区别。

-> 下一节:04 ImGui 窗口