使用 InputEvent

它是什么?

无论是在操作系统或平台上, 管理输入通常很复杂. 为了简化输入管理, 引擎提供了一个特殊的内置类型 InputEvent. 此类型可被设置成包含多种类型的输入事件. 输入事件通过引擎传递, 可在多个位置接收, 具体位置取决于目的.

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

GDScriptC#

  1. func _unhandled_input(event):
  2. if event is InputEventKey:
  3. if event.pressed and event.scancode == KEY_ESCAPE:
  4. get_tree().quit()
  1. public override void _UnhandledInput(InputEvent @event)
  2. {
  3. if (@event is InputEventKey eventKey)
  4. if (eventKey.Pressed && eventKey.Scancode == (int)KeyList.Escape)
  5. GetTree().Quit();
  6. }

但是,使用所提供的 InputMap 功能更简洁灵活,它允许您定义输入操作并分配不同的键。这样,您可以定义多个键的相同动作,例如键盘ESC键和游戏手柄上的启动按钮。然后,你可以在不更新代码的情况下轻松更改项目设置中的此映射,甚至在上面构建键映射功能,以便您的游戏在运行时更改键值映射!

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

GDScriptC#

  1. func _process(delta):
  2. if Input.is_action_pressed("ui_right"):
  3. # Move right.
  1. public override void _Process(float delta)
  2. {
  3. if (Input.IsActionPressed("ui_right"))
  4. {
  5. // Move right.
  6. }
  7. }

工作原理是怎样的?

每个输入事件都来源于用户/角色(尽管可以生成一个 InputEvent 并将其反馈给引擎,这在手势操作中非常有用)。每个平台的操作对象都将从设备读取事件,然后将它们发送到 MainLoop。因为 SceneTree 是默认的 MainLoop 实现,所以事件会被提交给它。Godot 提供了一个获取当前 SceneTree 对象的函数:get_tree()

但是 SceneTree 不知道如何处理这个事件,所以 SceneTree 把它交给视区,从“根”Viewport(场景树的第一个节点)开始查找。Viewport 会对接收到的输入做很多事情,依次为:

../../_images/input_event_flow.png

  1. 首先,标准的 Node._input() 函数将在任何覆写它的节点中被调用(在没有被 Node.set_process_input() 禁用的情况下 )。如果任何函数消耗了该输入事件,它可以调用 SceneTree.set_input_as_handled(),该事件将不再传播。这让您可以在 GUI 响应之前过滤事件。对于游戏性的输入,Node._unhandled_input() 通常更合适,因为它允许 GUI 拦截事件。

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

  3. 如果到目前为止没有函数消耗该事件,则在被覆盖时将调用未处理回调(并且未通过 Node.set_process_unhandled_input() 禁用)。如果任何函数消耗了该事件,它可以调用 SceneTree.set_input_as_handled() 来设置已经处理了该事件,它就将不再传播。未处理的输入回调是全屏游戏事件的理想选择,因为当 GUI 处于激活状态时不会收到它们。

  4. 如果到目前为止没有人想要这个事件,并且 Viewport 中分配的 Camera 启用了对象拾取,就会(从点击的射线方向)往物理世界中投射一条射线。(如果是根视图,则是在项目设置中启用。)如果这条射线命中了某个对象,就会调用相关物理对象的 CollisionObject._input_event() 函数(物理实体默认接受这个回调,区域则不会。可以使用 Area 的属性进行设置)。

  5. 最后,如果事件未被处理,它将被传递给树中的下一个 Viewport,否则将被忽略。

将事件发送到场景中的所有侦听节点时, 视区将以反向深度优先顺序执行: 从场景树底部的节点开始, 到根节点结束:

../../_images/input_event_scene_flow.png

GUI事件也在场景树上传播,但由于这些事件针对的是特定的控件,所以只有目标控件节点的第一个父节点才会收到该事件。

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

InputEvent 剖析

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

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

事件

类型索引

描述

InputEvent

NONE

空输入事件.

InputEventKey

包含一个键盘扫描码和Unicode值, 以及修饰键.

InputEventMouseButton

MOUSE_BUTTON

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

InputEventMouseMotion

MOUSE_MOTION

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

InputEventJoypadMotion

JOYSTICK_MOTION

包含操纵杆/ Joypad模拟轴信息.

InputEventJoypadButton

JOYSTICK_BUTTON

包含操纵杆/ Joypad按钮信息.

InputEventScreenTouch

SCREEN_TOUCH

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

InputEventScreenDrag

SCREEN_DRAG

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

InputEventAction

SCREEN_ACTION

包含一般动作. 这些事件通常由程序员作为反馈生成. (以下更多内容)

动作

InputEvent可能代表也可能不代表预定义的动作. 动作很有用, 因为它们在编写游戏逻辑时抽象输入设备. 这允许:

  • 相同的代码可以在具有不同输入的不同设备上工作(例如,PC上的键盘, 控制台上的Joypad).

  • 输入要在运行时重新配置.

动作可以在“项目设置”菜单的“动作”选项卡中创建。

任何事件都有方法 InputEvent.is_action(), InputEvent.is_pressed() and InputEvent.

或者, 可能希望从游戏代码中向游戏提供一个动作, 一个很好的例子是检测手势.Input单例有一个方法来实现这个功能 Input.parse_input_event() . 通常这样使用它:

GDScriptC#

  1. var ev = InputEventAction.new()
  2. # Set as move_left, pressed.
  3. ev.action = "move_left"
  4. ev.pressed = true
  5. # Feedback.
  6. Input.parse_input_event(ev)
  1. var ev = new InputEventAction();
  2. // Set as move_left, pressed.
  3. ev.SetAction("move_left");
  4. ev.SetPressed(true);
  5. // Feedback.
  6. Input.ParseInputEvent(ev);

InputMap

经常需要的从代码中定制和重新映射输入. 如果你的整个运行流程依赖于动作, 那么 InputMap 单例是在运行时重新分配或创建不同动作的理想选择. 这个单例不被保存(必须手动修改), 其状态从项目设置进行(project.godot). 所以任何这种类型的动态系统, 都需要以程序员认为最合适的方式来存储设置.