使用 InputEvent

这是什么?

无论操作系统或平台如何,管理输入通常很复杂。为了稍微简化这一点,引擎提供了一种特殊的内置类型 InputEvent。该数据类型可以被配置为包含几种类型的输入事件。输入事件通过引擎传播,并可以根据目的在多个位置接收。

这里有一个简单的示例,按下 ESC 键时关闭你的游戏:

func _unhandled_input(event):
    if event is InputEventKey:
        if event.pressed and event.keycode == KEY_ESCAPE:
            get_tree().quit()

但是,使用提供的 InputMap 功能将更简洁灵活,它允许你定义输入操作并为其分配不同的键。这样,你可以为同一动作定义多个键(例如键盘上的退出键和游戏手柄上的开始按钮)。然后,你可以更轻松地在项目设置中更改该映射,而无需更新代码,甚至可以在其上构建一个键映射功能,以允许你的游戏在运行时更改键映射!

你可以在项目 > 项目设置 > 按键映射下设置你的输入映射,这些动作的使用方法如下:

func _process(delta):
    if Input.is_action_pressed("ui_right"):
        # Move right.

工作原理是怎样的?

每个输入事件都来源于用户/玩家(虽然也可以自己生成 InputEvent 并提供给引擎,多用于手势)。各个平台的 DisplayServer 都会从操作系统读取事件,然后提供给根 Window

窗口的 Viewport 会对收到的输入进行很多处理,依次为:

../../_images/input_event_flow.webp
  1. 如果该 Viewport 内嵌了 Window,则该 Viewport 会尝试以窗口管理器的身份解释事件(例如对 Window 进行大小调整和移动)。

  2. 接下来,如果存在聚焦的内嵌 Window,则会将事件发送给该 Window,在该窗口的 Viewport 中进行处理,然后将事件标记为已处理。如果不存在聚焦的内嵌 Window,则会将事件发送给当前视口中的节点,顺序如下。

  3. 首先会调用标准的 Node._input() 函数,调用只会发生在覆盖了这个函数(并且输入处理没有通过 Node.set_process_input() 禁用)的节点上。如果某个函数消耗了该事件,可以调用 Viewport.set_input_as_handled(),事件就不会再继续传播。这样就确保你可以在 GUI 之前过滤自己感兴趣的事件。对于游戏输入,Node._unhandled_input() 通常更合适,因为这个函数能够让 GUI 拦截事件。

  4. 然后,它会尝试将输入提供给 GUI,并查看是否有控件可以接收它。如果有,该 Control 将通过虚函数 Control._gui_input() 被调用并发出“gui_input”信号(此函数可通过继承它的脚本重新实现)。如果该控件想“消耗”该事件,它将调用 Control.accept_event() 阻止事件的传播。请使用 Control.mouse_filter 属性来控制 Control 是否通过 Control._gui_input() 回调接收鼠标事件的通知,以及是否进一步传播这些事件。

  5. 如果事件到目前为止还没有被消耗,并且覆盖了 Node._shortcut_input() 函数(并且没有通过 Node.set_process_shortcut_input() 禁用),那么就会调用这个回调。只有 InputEventKeyInputEventShortcutInputEventJoypadButton 才会如此。如果某个函数消耗了该事件,它可以调用 Viewport.set_input_as_handled(),那么事件就不会再继续传播。快捷键输入回调主要用于处理快捷键相关的事件。

  6. 如果事件到目前为止还没有被消耗,并且 Node._unhandled_key_input() 函数已被覆盖(并且没有通过 Node.set_process_unhandled_key_input() 禁用),那么该回调将被调用。仅当事件是 InputEventKey 时才会如此。如果某个函数消耗了该事件,它可以调用 Viewport.set_input_as_handled(),事件就不会再继续传播。未处理按键输入回调主要用于处理按键相关的事件。

  7. 如果事件到目前为止还没有被消耗,并且覆盖了 Node._unhandled_input() 函数(并且没有通过 Node.set_process_unhandled_input() 禁用),那么就会调用这个回调。如果某个函数消耗了该事件,它可以调用 Viewport.set_input_as_handled(),事件就不会再继续传播。未处理输入回调主要用于处理全屏游戏事件,因此 GUI 处于活动状态时不会收到。

  8. 如果到目前为止没有节点想要该事件,并且对象拾取已打开,则该事件将用于对象拾取。对于根视口,也可以在项目设置中启用该设置。在 3D 场景的情况下,如果将 Camera3D 分配给该 Viewport,则会向物理世界投射一条射线(以从点击开始的射线方向)。如果该射线击中物体,它将调用相关物理对象中的 CollisionObject3D._input_event() 函数。对于 2D 场景,从概念上讲,CollisionObject2D._input_event() 也会发生同样的情况。

视口会向子孙节点发送事件,如下图所示,发送时会按照逆深度优先顺序进行,从场景树最底部的节点开始,到根节点结束。这个过程中会跳过 Window 和 SubViewport。

../../_images/input_event_scene_flow.webp

备注

该顺序不适用于 Control._gui_input(),它使用的方法不同,基于事件位置或聚焦的 Control。GUI 鼠标事件也会沿场景树向上传播,受上述 Control.mouse_filter 限制的约束。不过由于这些事件针对的是特定的 Control,只有目标 Control 节点的直接父节点会接收到该事件。GUI 键盘和手柄事件不会沿场景树向上传播,只能由接收到事件的 Control 处理。否则就会通过 Node._unhandled_input() 作为非 GUI 事件传播。

由于 Viewport 不会将事件发送给其他 SubViewport,所以需要在下列方法中选择一个:

  1. 使用 SubViewportContainer,这个节点会在 Node._input()Control._gui_input() 之后,自动将事件发送给其子级 SubViewport

  2. 根据具体需求实现事件传播逻辑。

根据 Godot 基于节点的设计,这使得专门的子节点能够处理和消耗特定的事件,而它们的祖先以及最终的场景根,可以在需要时提供更通用的行为。

InputEvent 剖析

InputEvent 只是一个基本的内置类型,它不代表任何东西,只包含一些基本信息,例如事件 ID(每个事件都会增加)、设备索引等。

InputEvent 有几种专门的类型,如下表所述:

事件

描述

InputEvent

空输入事件。

InputEventKey

包含键码和 Unicode 值以及修饰键。

InputEventMouseButton

包含点击信息,例如按钮、修饰键等。

InputEventMouseMotion

包含运动信息,例如相对位置、绝对位置和速度。

InputEventJoypadMotion

包含操纵杆/操纵手柄模拟轴信息。

InputEventJoypadButton

包含操纵杆/操纵手柄按钮信息。

InputEventScreenTouch

包含多点触控按下/释放信息。(仅适用于移动设备)

InputEventScreenDrag

包含多点触控拖动信息。(仅适用于移动设备)

InputEventMagnifyGesture

包含位置、系数以及修饰键。

InputEventPanGesture

包含位置、增量以及修饰键。

InputEventMIDI

包含 MIDI 相关的信息。

InputEventShortcut

包含快捷键。

InputEventAction

包含通用动作。这些事件通常由程序员生成作为反馈。(更多信息见下文)

输入动作

输入动作是对若干 InputEvent 的分组,为每一组事件赋予能够普遍理解标题(例如默认的“ui_left”动作将手柄向左的输入和键盘上的左方向键分到了一组)。使用输入动作来代表 InputEvent 不是必须的,但之所以有用,是因为输入动作对游戏逻辑编程时的各种输入进行了抽象。

这样就可以:

  • 用相同的代码在不同的设备上处理不同的输入(例如,PC 上的键盘、主机上的游戏手柄)。

  • 在运行时重新配置输入。

  • 在运行时以编程的方式触发动作。

动作可以从“项目设置”菜单中的输入映射选项卡创建并分配输入事件。

Any event has the methods InputEvent.is_action(), InputEvent.is_pressed() and InputEvent.is_echo().

或者,可能需要从游戏代码中向游戏提供一个动作(一个很好的例子是检测手势)。Input 单例有一个方法 Input.parse_input_event() 来用于此。通常会像这样使用它:

var ev = InputEventAction.new()
# Set as ui_left, pressed.
ev.action = "ui_left"
ev.pressed = true
# Feedback.
Input.parse_input_event(ev)

参见

有关在项目设置中添加输入操作的教程,请参阅 创建输入动作

InputMap

通常需要从代码中自定义输入和重新映射输入。如果你的整个工作流程都依赖于动作,则 InputMap 单例非常适合在运行时重新分配或创建不同的动作。该单例不会被保存(必须手动修改),其状态从项目设置(project.godot)运行。因此,任何该类型的动态系统都需要以程序员认为最合适的方式来存储设置。