按键映射

按键映射

按键映射(或按键绑定)定义与输入关联的特定操作,例如鼠标点击或按键。客户端能够接收输入时,可以检查映射定义的操作。每个映射还可通过控制选项菜单分配给任意输入。

客户端按键映射关系:在物理客户端的模组事件总线上通过 RegisterKeyMappingsEvent 注册 KeyMapping;游戏内在 Forge 事件总线消费 consumeClick,GUI 中在 keyPressed 或 mouseClicked 中用 isActiveAndMatches 检查;GUI 上下文仅屏幕打开时活跃,IN_GAME 仅无屏幕时活跃。

原创说明图,依据 Forge 1.21.x 官方文档 绘制。

注册 KeyMapping

仅在物理客户端的模组事件总线上监听 RegisterKeyMappingsEvent,调用 #register 注册 KeyMapping。

// 在仅物理客户端使用的类中;延迟初始化,直到注册时才创建
public static final Lazy<KeyMapping> EXAMPLE_MAPPING = Lazy.of(() -> /*...*/);

// 仅物理客户端的模组事件总线
@SubscribeEvent
public void registerBindings(RegisterKeyMappingsEvent event) {
  event.register(EXAMPLE_MAPPING.get());
}

创建 KeyMapping

通过构造函数创建 KeyMapping。它接收定义映射名称的翻译键、默认输入,以及定义控制选项菜单中所属分类的翻译键。

提示:提供原版没有的分类翻译键,即可将映射加入自定义分类。自定义分类翻译键应包含模组 ID,例如 key.categories.examplemod.examplecategory。

默认输入

每个映射都有通过 InputConstants$Key 提供的默认输入。输入由定义设备的 InputConstants$Type 和定义设备上对应输入标识符的整数构成。

原版提供三种输入类型:KEYSYM 通过 GLFW 按键标记定义键盘;SCANCODE 通过平台特定扫描码定义键盘;MOUSE 定义鼠标。

注意:强烈建议键盘使用 KEYSYM 而不是 SCANCODE,因为 GLFW 按键标记不绑定特定系统。详情见 GLFW 文档。

整数取决于输入类型。所有输入代码都在 GLFW 中定义:KEYSYM 标记以 GLFW_KEY_* 开头,MOUSE 代码以 GLFW_MOUSE_* 开头。

new KeyMapping(
  "key.examplemod.example1", // 用此翻译键本地化
  InputConstants.Type.KEYSYM, // 默认使用键盘
  GLFW.GLFW_KEY_P, // 默认按键 P
  "key.categories.misc" // 位于 misc 分类
)

注意:如果不应有默认绑定,将输入设为 InputConstants#UNKNOWN。原版构造函数要求通过 InputConstants$Key#getValue 提取输入代码;Forge 构造函数可以直接接收输入字段。

IKeyConflictContext

并非所有映射都用于所有上下文:有的仅用于 GUI,有的仅用于游戏中。可分配 IKeyConflictContext,避免不同上下文中同一按键的映射互相冲突。

冲突上下文有两个方法:#isActive 定义当前游戏状态下映射能否使用;#conflicts 定义它是否与相同或不同冲突上下文中的按键冲突。

Forge 通过 KeyConflictContext 定义三种基本上下文:默认的 UNIVERSAL 表示所有上下文均可使用;GUI 表示仅打开 Screen 时可用;IN_GAME 表示仅未打开 Screen 时可用。实现 IKeyConflictContext 可以创建新上下文。

new KeyMapping(
  "key.examplemod.example2",
  KeyConflictContext.GUI, // 仅屏幕打开时可用
  InputConstants.Type.MOUSE, // 默认使用鼠标
  GLFW.GLFW_MOUSE_BUTTON_LEFT, // 默认鼠标左键
  "key.categories.examplemod.examplecategory" // 自定义分类
)

KeyModifier

开发者可能不希望按住修饰键时映射行为相同,例如 G 与 CTRL + G。Forge 构造函数增加 KeyModifier 参数,可为任意输入添加 Control(KeyModifier#CONTROL)、Shift(#SHIFT)或 Alt(#ALT)。默认 KeyModifier#NONE 不添加修饰键。

可在控制选项菜单中同时按住修饰键和对应输入,添加修饰键。

new KeyMapping(
  "key.examplemod.example3",
  KeyConflictContext.UNIVERSAL,
  KeyModifier.SHIFT, // 默认需要按住 Shift
  InputConstants.Type.KEYSYM,
  GLFW.GLFW_KEY_G, // 默认 G
  "key.categories.misc"
)

检查 KeyMapping

可检查映射是否被点击,并在适当时机通过条件语句执行相应逻辑。

游戏中

在Forge 事件总线上监听 ClientTickEvent,在 while 循环中检查 KeyMapping#consumeClick。该方法只会按尚未处理的输入次数返回 true,因此不会无限阻塞游戏。

// 仅在物理客户端的 Forge 事件总线上
public void onClientTick(ClientTickEvent event) {
  if (event.phase == TickEvent.Phase.END) { // tick 事件每刻触发两次,只执行一次
    while (EXAMPLE_MAPPING.get().consumeClick()) {
      // 在此执行点击逻辑
    }
  }
}

警告:不要用 InputEvent 替代 ClientTickEvent。键盘和鼠标有各自的事件,不能处理其他输入。

GUI 内

在 GUI 中,可在 GuiEventListener 方法中通过 IForgeKeyMapping#isActiveAndMatches 检查映射。最常用的是 #keyPressed 和 #mouseClicked。

#keyPressed 接收 GLFW 按键标记、平台特定扫描码和按住的修饰键位字段。通过 InputConstants#getKey 创建输入后可与映射比较,映射方法本身已检查修饰键。

@Override
public boolean keyPressed(int key, int scancode, int mods) {
  if (EXAMPLE_MAPPING.get().isActiveAndMatches(InputConstants.getKey(key, scancode))) {
    // 执行按键逻辑
    return true;
  }
  return super.keyPressed(key, scancode, mods);
}

原文示例的父类调用使用了未声明的 x, y, button;译文已改为该方法的 key, scancode, mods 参数。

注意:如果屏幕不属于你的代码,可在 Forge 事件总线上监听 ScreenEvent$KeyPressed 的 Pre 或 Post 事件。

#mouseClicked 接收鼠标 x/y 坐标和点击的按钮。使用 InputConstants$Type#getOrCreate 与 MOUSE 输入创建输入后,可与映射比较。

@Override
public boolean mouseClicked(double x, double y, int button) {
  if (EXAMPLE_MAPPING.get().isActiveAndMatches(InputConstants.Type.MOUSE.getOrCreate(button))) {
    // 执行鼠标点击逻辑
    return true;
  }
  return super.mouseClicked(x, y, button);
}

原文示例使用 InputConstants.TYPE,与上文所示的嵌套类型 InputConstants.Type 大小写不一致;译文采用后者。

注意:如果屏幕不属于你的代码,可在 Forge 事件总线上监听 ScreenEvent$MouseButtonPressed 的 Pre 或 Post 事件。

原文:Key Mappings。