InputEvent

What is it?

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. Seguido, intentará transmitir el evento a la GUI y ver si algún Control la puede recibir. De ser así, el Control será llamado mediante la función virtual Control._gui_input() y la señal «input_event» será emitida (esta función se puede re-implementar por script en caso utilizar herencia). Si el Control quiere «consumir» el evento, llamará a Control.accept_event() y el evento no se propagará más. Los eventos que no son consumidos, se propagarán hacia arriba hasta sus ancestros. Utiliza la propiedad Control.mouse_filter para controlar si un Control es notificado de eventos de ratón mediante Control._gui_input() y si esos eventos deberán propagarse más.
  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. Si nadie ha querido realizar el evento hasta ahora, y una Camera es asignada al Viewport, se emitirá un rayo al mundo de la física (en la dirección correspondiente al rayo del clic). Si este rayo golpea un objeto, llamará a la función CollisionObject._input_event() en el objeto físico correspondiente (los cuerpos reciben esta llamada de retorno por defecto, pero las áreas no lo hacen. Esto puede configurarse a través de las propiedades de Area).
  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 para manipular y consumir 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.