使用 InputEvent
它是什么?
无论是在操作系统或平台上, 管理输入通常很复杂. 为了简化输入管理, 引擎提供了一个特殊的内置类型 InputEvent. 此类型可被设置成包含多种类型的输入事件. 输入事件通过引擎传递, 可在多个位置接收, 具体位置取决于目的.
这里有一个简单的示例,按下 ESC 键时关闭你的游戏:
GDScriptC#
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 功能更简洁灵活,它允许你定义输入操作并分配不同的键。这样,你可以定义多个键的相同动作,例如键盘ESC键和游戏手柄上的启动按钮。然后,你可以在不更新代码的情况下轻松更改项目设置中的此映射,甚至在上面构建键映射功能,以便你的游戏在运行时更改键值映射!
你可以在项目 > 项目设置 > 按键映射下设置你的输入映射,这些动作的使用方法如下:
GDScriptC#
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(),事件就不会再继续传播。快捷键输入回调主要用于处理快捷键相关的事件。
If so far no one consumed the event, the Node._unhandled_key_input() callback will be called if overridden (and not disabled with Node.set_process_unhandled_key_input()). This happens only if the event is an InputEventKey. If any function consumes the event, it can call Viewport.set_input_as_handled(), and the event will not spread any more. The unhandled key input callback is ideal for key events.
如果事件到目前为止还没有被消耗,并且覆盖了 Node._unhandled_input() 函数(并且没有通过 Node.set_process_unhandled_input() 禁用),那么就会调用这个回调。如果某个函数消耗了该事件,就可以调用 Viewport.set_input_as_handled(),事件就不会再继续传播。未处理输入回调主要用于处理全屏游戏事件,GUI 处于活动状态时不会收到。
If no one wanted the event so far, and Object Picking is turned on, the event is used for object picking. For the root viewport, this can also be enabled in Project Settings. In the case of a 3D scene if a Camera3D is assigned to the Viewport, a ray to the physics world (in the ray direction from the click) will be cast. If this ray hits an object, it will call the CollisionObject3D._input_event() function in the relevant physics object. In the case of a 2D scene, conceptually the same happens with CollisionObject2D._input_event().
视口会向子孙节点发送事件,如下图所示,发送时会按照逆深度优先顺序进行,从场景树最底部的节点开始,到根节点结束。这个过程中会跳过 Window 和 SubViewport。
这个顺序并不适用于 Control._gui_input(),这个函数的调用顺序与事件的位置和聚焦的 Control 有关,使用的是不同的方法。
由于 Viewport 不会将事件发送给其他 SubViewport,所以需要在下列方法中选择一个:
使用 SubViewportContainer,这个节点会在 Node._input() 或 Control._gui_input() 之后,自动将事件发送给子级 SubViewport。
根据具体需求实现事件传播逻辑。
GUI 事件也会沿着场景树向上传播,但由于这些事件针对的是特定的 Control,所以只有目标 Control 节点的直接父节点才会收到该事件。
根据Godot基于节点的设计, 这使得专门的子节点能够处理和消费特定的事件, 而它们的父级节点, 以及最终的场景根节点, 可以在需要时提供更通用的行为.
InputEvent 剖析
InputEvent 只是一个基本的内置类型, 它不代表任何东西, 只包含一些基本信息, 如事件ID(每个事件增加), 设备索引等.
InputEvent有几种专门的类型, 如下表所述:
事件 | 描述 |
空输入事件. | |
Contains a keycode and Unicode value, as well as modifiers. | |
包含点击信息, 例如按钮, 修饰键等. | |
Contains motion information, such as relative and absolute positions and speed. | |
包含操纵杆/ Joypad模拟轴信息. | |
包含操纵杆/ Joypad按钮信息. | |
包含多点触控按下/释放信息. (仅适用于移动设备) | |
包含多点触控拖动信息. (仅适用于移动设备) | |
Contains a position, a factor as well as modifiers. | |
Contains a position, a delta as well as modifiers. | |
Contains MIDI-related information. | |
容器布局。 | |
包含一般动作. 这些事件通常由程序员作为反馈生成. (以下更多内容) |
动作
动作是将零个或多个输入事件群组为通常理解的标题(例如,预设的「ui_left」动作将手柄左输入和键盘左箭头键群组)。它们不需要表示输入事件,但很有用,因为它们在编写游戏逻辑时抽象化了各种输入。
This allows for:
相同的代码可以在具有不同输入的不同设备上工作(例如,PC上的键盘, 控制台上的Joypad).
输入要在运行时重新配置.
Actions to be triggered programmatically at run-time.
Actions can be created from the Project Settings menu in the Input Map tab and assigned input events.
任何事件都有方法 InputEvent.is_action(), InputEvent.is_pressed() and InputEvent.
或者, 可能希望从游戏代码中向游戏提供一个动作, 一个很好的例子是检测手势.Input单例有一个方法来实现这个功能 Input.parse_input_event() . 通常这样使用它:
GDScriptC#
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);
InputMap
经常需要的从代码中定制和重新映射输入. 如果你的整个运行流程依赖于动作, 那么 InputMap 单例是在运行时重新分配或创建不同动作的理想选择. 这个单例不被保存(必须手动修改), 其状态从项目设置进行(project.godot). 所以任何这种类型的动态系统, 都需要以程序员认为最合适的方式来存储设置.