使用 InputEvent
它是什么?
无论操作系统或平台如何,管理输入通常很复杂。为了稍微简化这一点,引擎提供了一种特殊的内置类型 InputEvent。该数据类型可以被配置为包含几种类型的输入事件。输入事件通过引擎传播,并可以根据目的在多个位置接收。
这里有一个简单的示例,按下 ESC 键时关闭你的游戏:
func _unhandled_input(event):
if event is InputEventKey:
if event.pressed and event.keycode == KEY_ESCAPE:
get_tree().quit()
public override void _UnhandledInput(InputEvent @event)
{
if (@event is InputEventKey eventKey)
if (eventKey.Pressed && eventKey.Keycode == Key.Escape)
GetTree().Quit();
}
但是,使用提供的 InputMap 功能将更简洁灵活,它允许你定义输入操作并为其分配不同的键。这样,你可以为同一动作定义多个键(例如键盘上的退出键和游戏手柄上的开始按钮)。然后,你可以更轻松地在项目设置中更改该映射,而无需更新代码,甚至可以在其上构建一个键映射功能,以允许你的游戏在运行时更改键映射!
你可以在项目 > 项目设置 > 按键映射下设置你的输入映射,这些动作的使用方法如下:
func _process(delta):
if Input.is_action_pressed("ui_right"):
# Move right.
public override void _Process(double delta)
{
if (Input.IsActionPressed("ui_right"))
{
// Move right.
}
}
工作原理是怎样的?
每个输入事件都来源于用户/玩家(虽然也可以自己生成 InputEvent 并提供给引擎,多用于手势)。各个平台的 DisplayServer 都会从操作系统读取事件,然后提供给根 Window。
窗口的 Viewport 会对收到的输入进行很多处理,依次为:

如果该 Viewport 内嵌了 Window,则该 Viewport 会尝试以窗口管理器的身份解释事件(例如对 Window 进行大小调整和移动)。
接下来,如果存在聚焦的内嵌 Window,则会将事件发送给该 Window,在该窗口的 Viewport 中进行处理,然后将事件标记为已处理。如果不存在聚焦的内嵌 Window,则会将事件发送给当前视口中的节点,顺序如下。
首先会调用标准的 Node._input() 函数,调用只会发生在覆盖了这个函数(并且输入处理没有通过 Node.set_process_input() 禁用)的节点上。如果某个函数消耗了该事件,可以调用 Viewport.set_input_as_handled(),事件就不会再继续传播。这样就确保你可以在 GUI 之前过滤自己感兴趣的事件。对于游戏输入,Node._unhandled_input() 通常更合适,因为这个函数能够让 GUI 拦截事件。
然后,它会尝试将输入提供给 GUI,并查看是否有控件可以接收它。如果有,该 Control 将通过虚函数 Control._gui_input() 被调用并发出“gui_input”信号(此函数可通过继承它的脚本重新实现)。如果该控件想“消耗”该事件,它将调用 Control.accept_event() 阻止事件的传播。请使用 Control.mouse_filter 属性来控制 Control 是否通过 Control._gui_input() 回调接收鼠标事件的通知,以及是否进一步传播这些事件。
如果事件到目前为止还没有被消耗,并且覆盖了 Node._shortcut_input() 函数(并且没有通过 Node.set_process_shortcut_input() 禁用),那么就会调用这个回调。只有 InputEventKey、InputEventShortcut 和 InputEventJoypadButton 才会如此。如果某个函数消耗了该事件,它可以调用 Viewport.set_input_as_handled(),那么事件就不会再继续传播。快捷键输入回调主要用于处理快捷键相关的事件。
如果事件到目前为止还没有被消耗,并且 Node._unhandled_key_input() 函数已被覆盖(并且没有通过 Node.set_process_unhandled_key_input() 禁用),那么该回调将被调用。仅当事件是 InputEventKey 时才会如此。如果某个函数消耗了该事件,它可以调用 Viewport.set_input_as_handled(),事件就不会再继续传播。未处理按键输入回调主要用于处理按键相关的事件。
如果事件到目前为止还没有被消耗,并且覆盖了 Node._unhandled_input() 函数(并且没有通过 Node.set_process_unhandled_input() 禁用),那么就会调用这个回调。如果某个函数消耗了该事件,它可以调用 Viewport.set_input_as_handled(),事件就不会再继续传播。未处理输入回调主要用于处理全屏游戏事件,因此 GUI 处于活动状态时不会收到。
如果到目前为止没有节点想要该事件,并且对象拾取已打开,则该事件将用于对象拾取。对于根视口,也可以在项目设置中启用该设置。在 3D 场景的情况下,如果将 Camera3D 分配给该 Viewport,则会向物理世界投射一条射线(以从点击开始的射线方向)。如果该射线击中物体,它将调用相关物理对象中的 CollisionObject3D._input_event() 函数。对于 2D 场景,从概念上讲,CollisionObject2D._input_event() 也会发生同样的情况。
视口会向子孙节点发送事件,如下图所示,发送时会按照逆深度优先顺序进行,从场景树最底部的节点开始,到根节点结束。这个过程中会跳过 Window 和 SubViewport。

备注
This order doesn't apply to Control._gui_input(), which uses a different method based on event location or focused Control. GUI mouse events also travel up the scene tree, subject to the Control.mouse_filter restrictions described above. However, since these events target specific Controls, only direct ancestors of the targeted Control node receive the event. GUI keyboard and joypad events do not travel up the scene tree, and can only be handled by the Control that received them. Otherwise, they will be propagated as non-GUI events through Node._unhandled_input().
由于 Viewport 不会将事件发送给其他 SubViewport,所以需要在下列方法中选择一个:
使用 SubViewportContainer,这个节点会在 Node._input() 或 Control._gui_input() 之后,自动将事件发送给其子级 SubViewport。
根据具体需求实现事件传播逻辑。
根据 Godot 基于节点的设计,这使得专门的子节点能够处理和消耗特定的事件,而它们的祖先以及最终的场景根,可以在需要时提供更通用的行为。
InputEvent 剖析
InputEvent 只是一个基本的内置类型,它不代表任何东西,只包含一些基本信息,例如事件 ID(每个事件都会增加)、设备索引等。
InputEvent 有几种专门的类型,如下表所述:
事件 |
描述 |
空输入事件。 |
|
包含键码和 Unicode 值以及修饰键。 |
|
包含点击信息,例如按钮、修饰键等。 |
|
包含运动信息,例如相对位置、绝对位置和速度。 |
|
包含操纵杆/操纵手柄模拟轴信息。 |
|
包含操纵杆/操纵手柄按钮信息。 |
|
包含多点触控按下/释放信息。(仅适用于移动设备) |
|
包含多点触控拖动信息。(仅适用于移动设备) |
|
包含位置、系数以及修饰键。 |
|
包含位置、增量以及修饰键。 |
|
包含 MIDI 相关的信息。 |
|
包含快捷键。 |
|
包含通用动作。这些事件通常由程序员生成作为反馈。(更多信息见下文) |
Input actions
Input actions are a grouping of zero or more InputEvents into a commonly understood title (for example, the default "ui_left" action grouping both joypad-left input and a keyboard's left arrow key). They are not required to represent an InputEvent but are useful because they abstract various inputs when programming the game logic.
这允许:
相同的代码可以在具有不同输入的不同设备(例如,PC 上的键盘、主机上的游戏手柄)上运行。
Input to be reconfigured at runtime.
Actions to be triggered programmatically at runtime.
动作可以从“项目设置”菜单中的输入映射选项卡创建并分配输入事件。
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)
var ev = new InputEventAction();
// Set as ui_left, pressed.
ev.Action = "ui_left";
ev.Pressed = true;
// Feedback.
Input.ParseInputEvent(ev);
参见
See 创建输入动作 for a tutorial on adding input actions in the project settings.
InputMap
Customizing and re-mapping input from code is often desired. If your whole workflow depends on actions, the InputMap singleton is ideal for reassigning or creating different actions at runtime. This singleton is not saved (must be modified manually) and its state is run from the project settings (project.godot). So any dynamic system of this type needs to store settings in the way the programmer best sees fit.