Using InputEvent

¿Qué es eso?

Administrar inputs (o entradas) es normalmente complejo, sin importar en qué SO o plataforma. Para aliviar un poco esto se provee un tipo built-in especial, InputEvent, este puede ser configurado para contener varios tipos de eventos. Los eventos de entrada van a través del motor y pueden ser recibidos en múltiples lugares, dependiendo de su propósito.

Aquí hay un ejemplo rápido de cerrar el juego cuando se presiona la tecla "escape":

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

Sin embargo, es más limpio y flexible usar la funcionalidad InputMap, la que permite definir acciones de entrada y asignarlas a diferentes teclas. De este modo, se pueden definir múltiples teclas para la misma acción (por ejemplo, la tecla escape y el botón start de un gamepad). Así se puede cambiar el mapeo fácilmente en el proyecto sin necesidad de modificar el código, incluso se puede crear una funcionalidad de mapeo que permita cambiar las teclas utilizadas dentro del juego en ejecución.

Puedes configurar el InputMap dentro de Proyecto > Ajustes del Proyecto > Mapa de Entradas y luego usa las acciones de este modo:

func _process(delta):
    if Input.is_action_pressed("ui_right"):
        # Move right.
public override void _Process(float delta)
{
    if (Input.IsActionPressed("ui_right"))
    {
        // Move right.
    }
}

¿Cómo funciona?

Cada evento de entrada es originado por el usuario/jugador (aunque también es posible generar un InputEvent y enviarlos al motor, algo útil para funciones como gestos). El objeto OS de cada plataforma leerá eventos desde un dispositivo y los transmitirá a MainLoop. Como SceneTree es la implementación por defecto de MainLoop, los eventos irán a este. Godot provee una función para obtener el objeto SceneTree actual: get_tree().

Pero SceneTree no sabe qué hacer con el evento, así que lo envía a los viewports, comenzando por el Viewport "root" (el primero nodo del árbol de escenas). Viewport hace varias cosas cuando recibe una entrada, en este orden:

../../_images/input_event_flow.png
  1. Primero, la función estándar Node._input() será llamada en cualquier nodo que la haya sobreescrito (y que no tenga deshabilitado el procesamiento de entradas con Node.set_process_input()). Si una función consume el evento, podrá llamar a SceneTree.set_input_as_handled() y el evento no se propagará más. Esto asegura que se puedan filtrar eventos de interés, incluso antes de que lleguen a la GUI. Para entradas de gameplay, generalmente es mejor utilizar Node._unhandled_input() porque permite que la GUI intercepte los eventos previamente.

  2. Second, it will try to feed the input to the GUI, and see if any control can receive it. If so, the Control will be called via the virtual function Control._gui_input() and the signal "gui_input" will be emitted (this function is re-implementable by script by inheriting from it). If the control wants to "consume" the event, it will call Control.accept_event() and the event will not spread any more. Use the Control.mouse_filter property to control whether a Control is notified of mouse events via Control._gui_input() callback, and whether these events are propagated further.

  3. Si hasta el momento ningún evento fue consumido, se utilizará la llamada a _unhandled_input si fue redefinida en el script (y no desactivada con Node.set_process_unhandled_input()). Si alguna función consume el evento, esta podrá llamar a SceneTree.set_input_as_handled(), y el evento no se propagará más. La llamada a _unhandled_input es ideal para eventos de gameplay de pantalla completa que deben ser ignorados si hay elementos de GUI activos.

  4. If no one wanted the event so far, and a Camera is assigned to the Viewport with Object Picking turned on, a ray to the physics world (in the ray direction from the click) will be cast. (For the root viewport, this can also be enabled in Project Settings) If this ray hits an object, it will call the CollisionObject._input_event() function in the relevant physics object (bodies receive this callback by default, but areas do not. This can be configured through Area properties).

  5. Finalmente, si el evento no fue capturado por ningún objeto, se pasará al siguiente Viewport del árbol o ignorado si no hay ninguno.

Cuando se envían eventos a todos los nodos que están escuchando en la escena, el viewport lo hará en un orden depth-first (primero en profundidad): Comenzando por el nodo al final del árbol de escenas y finalizando en el nodo raíz:

../../_images/input_event_scene_flow.png

Los eventos de GUI irán hacia arriba del árbol de escenas, como los eventos apuntan a Controls específicos, sólo se dirige a ancestros de los nodos Control indicados para recibir el evento.

De acuerdo al diseño basado en nodos de Godot, esto habilita que nodos hijos especializados manipulen y consuman eventos particulares, mientras que los ancestros y finalmente el árbol de escenas, pueden proveer un comportamiento más generalizado si es necesario.

Anatomía de un InputEvent

InputEvent es un tipo base integrado, no representa nada y sólo contiene información básica, como el identificador del evento (ID, el cual es incrementado por cada evento), índice de dispositivo, etc.

Existen varios tipos de InputEvent especializados, descriptos en la siguiente tabla:

Evento

Índice de Tipo

Descripción

InputEvent

NONE

Evento de entrada vacío.

InputEventKey

KEY

Contiene un scancode y un valor unicode, así como modificadores.

InputEventMouseButton

MOUSE_BUTTON

Contiene información de click como botón, modificadores, etc.

InputEventMouseMotion

MOUSE_MOTION

Contiene información de movimiento, como posiciones relativas y absolutas así como velocidad.

InputEventJoypadMotion

JOYSTICK_MOTION

Contiene información de ejes análogos de Joystick/Joypad.

InputEventJoypadButton

JOYSTICK_BUTTON

Contiene información de botones de Joystick/Joypad.

InputEventScreenTouch

SCREEN_TOUCH

Contiene información sobre press/release de multi-touch. (sólo disponible en dispositivos móviles)

InputEventScreenDrag

SCREEN_DRAG

Contiene información de arrastre multi-touch. (sólo disponible en dispositivos móviles)

InputEventAction

SCREEN_ACTION

Contiene una acción genérica. Estos eventos a menudo son generados por el programador como feedback. (más información a continuación)

Acciones

Un InputEvent puede representar o no una acción predefinida. Las acciones son útiles porque abstraen el dispositivo de entrada utilizado cuando se programa la lógica del juego. Esto permite:

  • Que el mismo código funcione para diferentes dispositivos con distintos dispositivos de entrada (por ejemplo, teclado en PC, Joypad en consolas).

  • Las entradas pueden ser reconfiguradas en tiempo de ejecución.

Las acciones pueden ser creadas en Project Settings, pestaña Mapa de Entradas.

Todos los eventos tienen los métodos InputEvent.is_action(), InputEvent.is_pressed() y InputEvent.

Alternativamente, es posible que quieras enviar una acción al juego desde el código (un ejemplo de esto es la detección de gestos). El singleton Input tiene un método para esto: Input.parse_input_event(). Se usa normalmente así:

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

InputMap

A menudo se desea personalizar y re-mapear la entrada desde código. Si todo el flujo de trabajo depende de las acciones, el singleton InputMap es ideal para reasignar o crear diferentes acciones en tiempo de ejecución. Este singleton no se guarda (debe ser modificado manualmente) y su estado se ejecuta desde la configuración del proyecto (project.godot). Por lo tanto, cualquier sistema dinámico de este tipo necesita almacenar los ajustes de la forma que el programador considere más adecuada.