Procedimientos de entrada para juegos

En este tema se describen patrones y técnicas para usar eficazmente dispositivos de entrada en juegos de Plataforma universal de Windows (UWP).

Al leer este tema, aprenderá lo siguiente:

  • Cómo hacer un seguimiento de los jugadores y de los dispositivos de entrada y navegación que están usando actualmente
  • Cómo detectar las transiciones de botón (presionado a no presionado, sin presionar a presionado)
  • Cómo detectar disposiciones de botones complejas con una sola prueba

Elección de una clase de dispositivo de entrada

Hay muchos tipos diferentes de API de entrada disponibles, como ArcadeStick, FlightStick y Gamepad. ¿Cómo decides qué API usar para tu juego?

Debes elegir la API que te proporcione la entrada más adecuada para tu juego. Por ejemplo, si estás haciendo un juego de plataforma 2D, probablemente solo puedes usar la clase Gamepad y no molestarte con la funcionalidad adicional disponible a través de otras clases. Esto restringiría el juego a admitir solo controladores para juegos y proporcionar una interfaz coherente que funcionará en muchos controladores para juegos diferentes sin necesidad de código adicional.

Por otro lado, para simulaciones complejas de vuelo y carreras, es posible que quieras enumerar todos los objetos RawGameController como línea base para asegurarte de que admiten cualquier dispositivo de nicho que los jugadores entusiastas puedan tener, incluidos dispositivos como pedales independientes o aceleradores que todavía usan un solo jugador.

Desde allí, puedes usar el método FromGameController de una clase de entrada, como Gamepad.FromGameController, para ver si cada dispositivo tiene una vista más mantenida. Por ejemplo, si el dispositivo también es un Controlador para juegos, es posible que desee ajustar la interfaz de usuario de asignación de botones para reflejarlo y proporcionar algunas asignaciones de botones predeterminadas razonables para elegir. (Esto contrasta con exigir al jugador que configure manualmente las entradas del controlador para juegos si solo usa RawGameController).

Como alternativa, puede ver el identificador de proveedor (VID) y el identificador de producto (PID) de un RawGameController (mediante HardwareVendorId y HardwareProductId, respectivamente) y proporcionar asignaciones de botones sugeridas para dispositivos populares, mientras sigue siendo compatible con dispositivos desconocidos que salen en el futuro a través de asignaciones manuales por parte del jugador.

Seguimiento de los controladores conectados

Aunque cada tipo de controlador incluye una lista de controladores conectados (como Gamepad.Gamepads), es una buena idea mantener su propia lista de controladores. Consulta La lista de controladores para juegos para obtener más información (cada tipo de controlador tiene una sección con nombre similar en su propio tema).

Sin embargo, ¿qué ocurre cuando el jugador desconecta su controlador o conecta uno nuevo? Debe controlar estos eventos y actualizar la lista según corresponda. Consulta Agregar y quitar controladores para juegos para obtener más información (de nuevo, cada tipo de controlador tiene una sección con nombre similar en su propio tema).

Dado que los eventos agregados y quitados se generan de forma asincrónica, puede obtener resultados incorrectos al tratar con la lista de controladores. Por lo tanto, cada vez que acceda a la lista de controladores, debe colocar un bloqueo alrededor de él para que solo un subproceso pueda acceder a ella a la vez. Esto se puede hacer con el runtime de simultaneidad, específicamente la clase critical_section, en <ppl.h>.

Otra cosa que hay que pensar es que la lista de controladores conectados estará vacía inicialmente y tardará un segundo o dos en rellenarse. Por lo tanto, si solo asignas el controlador para juegos actual en el método de inicio, será null!

Para rectificar esto, debes tener un método que "actualice" el controlador para juegos principal (en un juego de un solo jugador; los juegos multijugador requerirán soluciones más sofisticadas). A continuación, debe llamar a este método en los controladores de eventos agregados y eliminados del controlador, o en el método de actualización.

El método siguiente simplemente devuelve el primer controlador para juegos de la lista (o nullptr si la lista está vacía). Después, solo tienes que recordar comprobar nullptr cada vez que hagas cualquier cosa con el controlador. Es para ti si quieres bloquear el juego cuando no hay ningún controlador conectado (por ejemplo, pausando el juego) o simplemente hacer que el juego continúe, mientras ignora la entrada.

#include <ppl.h>

using namespace Platform::Collections;
using namespace Windows::Gaming::Input;
using namespace concurrency;

Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();

Gamepad^ GetFirstGamepad()
{
    Gamepad^ gamepad = nullptr;
    critical_section::scoped_lock{ m_lock };

    if (m_myGamepads->Size > 0)
    {
        gamepad = m_myGamepads->GetAt(0);
    }

    return gamepad;
}

Juntarlo todo, este es un ejemplo de cómo controlar la entrada desde un controlador para juegos:

#include <algorithm>
#include <ppl.h>

using namespace Platform::Collections;
using namespace Windows::Foundation;
using namespace Windows::Gaming::Input;
using namespace concurrency;

static Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();
static Gamepad^          m_gamepad = nullptr;
static critical_section  m_lock{};

void Start()
{
    // Register for gamepad added and removed events.
    Gamepad::GamepadAdded += ref new EventHandler<Gamepad^>(&OnGamepadAdded);
    Gamepad::GamepadRemoved += ref new EventHandler<Gamepad^>(&OnGamepadRemoved);

    // Add connected gamepads to m_myGamepads.
    for (auto gamepad : Gamepad::Gamepads)
    {
        OnGamepadAdded(nullptr, gamepad);
    }
}

void Update()
{
    // Update the current gamepad if necessary.
    if (m_gamepad == nullptr)
    {
        auto gamepad = GetFirstGamepad();

        if (m_gamepad != gamepad)
        {
            m_gamepad = gamepad;
        }
    }

    if (m_gamepad != nullptr)
    {
        // Gather gamepad reading.
    }
}

// Get the first gamepad in the list.
Gamepad^ GetFirstGamepad()
{
    Gamepad^ gamepad = nullptr;
    critical_section::scoped_lock{ m_lock };

    if (m_myGamepads->Size > 0)
    {
        gamepad = m_myGamepads->GetAt(0);
    }

    return gamepad;
}

void OnGamepadAdded(Platform::Object^ sender, Gamepad^ args)
{
    // Check if the just-added gamepad is already in m_myGamepads; if it isn't, 
    // add it.
    critical_section::scoped_lock lock{ m_lock };
    auto it = std::find(begin(m_myGamepads), end(m_myGamepads), args);

    if (it == end(m_myGamepads))
    {
        m_myGamepads->Append(args);
    }
}

void OnGamepadRemoved(Platform::Object^ sender, Gamepad^ args)
{
    // Remove the gamepad that was just disconnected from m_myGamepads.
    unsigned int indexRemoved;
    critical_section::scoped_lock lock{ m_lock };

    if (m_myGamepads->IndexOf(args, &indexRemoved))
    {
        if (m_gamepad == m_myGamepads->GetAt(indexRemoved))
        {
            m_gamepad = nullptr;
        }

        m_myGamepads->RemoveAt(indexRemoved);
    }
}

Seguimiento de usuarios y sus dispositivos

Todos los dispositivos de entrada se asocian con un usuario para que su identidad pueda vincularse a su juego, logros, cambios de configuración y otras actividades. Los usuarios pueden iniciar sesión o cerrar sesión en voluntad, y es habitual que un usuario diferente inicie sesión en un dispositivo de entrada que permanezca conectado al sistema después de que el usuario anterior haya cerrado la sesión. Cuando un usuario inicia o cierra sesión, se genera el evento IGameController.UserChanged . Puedes registrar un controlador de eventos para este evento a fin de realizar un seguimiento de los jugadores y de los dispositivos que usan.

La identidad del usuario también es la forma en que un dispositivo de entrada está asociado a su controlador de navegación de interfaz de usuario correspondiente.

Por estos motivos, se debe realizar un seguimiento de la entrada del reproductor y correlacionarse con la propiedad User de la clase de dispositivo (heredada de la interfaz IGameController ).

La aplicación de ejemplo UserGamepadPairingUWP en GitHub muestra cómo puede realizar un seguimiento de los usuarios y los dispositivos que usan.

Detección de transiciones de botón

A veces quieres saber cuándo se presiona o se suelta un botón; es decir, cuándo cambia exactamente el estado del botón de no presionado a presionado o de presionado a no presionado. Para determinarlo, debes recordar la lectura de dispositivo anterior y compararla con la lectura actual para ver qué ha cambiado.

En el ejemplo siguiente se muestra un enfoque básico para recordar la lectura anterior; Aquí se muestran controladores para juegos, pero los principios son los mismos para stick arcade, volante y otros tipos de dispositivos de entrada.

Gamepad gamepad;
GamepadReading newReading();
GamepadReading oldReading();

// Called at the start of the game.
void Game::Start()
{
    gamepad = Gamepad::Gamepads[0];
}

// Game::Loop represents one iteration of a typical game loop
void Game::Loop()
{
    // move previous newReading into oldReading before getting next newReading
    oldReading = newReading, newReading = gamepad.GetCurrentReading();

    // process device readings using buttonJustPressed/buttonJustReleased (see below)
}

Antes de realizar alguna cosa, Game::Loop mueve el valor existente de newReading (la lectura del controlador para juegos de la iteración de bucle anterior) a oldReading i, después, rellena newReading con una nueva lectura de controlador para juegos para la iteración actual. Esto te ofrece la información que necesitas para detectar transiciones de botón.

En el ejemplo siguiente se muestra un enfoque básico para detectar transiciones de botón:

bool ButtonJustPressed(const GamepadButtons selection)
{
    bool newSelectionPressed = (selection == (newReading.Buttons & selection));
    bool oldSelectionPressed = (selection == (oldReading.Buttons & selection));

    return newSelectionPressed && !oldSelectionPressed;
}

bool ButtonJustReleased(GamepadButtons selection)
{
    bool newSelectionReleased =
        (GamepadButtons.None == (newReading.Buttons & selection));

    bool oldSelectionReleased =
        (GamepadButtons.None == (oldReading.Buttons & selection));

    return newSelectionReleased && !oldSelectionReleased;
}

Estas dos funciones derivan primero el estado booleano de la selección de botón de newReading y oldReadingy, a continuación, realizan lógica booleana para determinar si se ha producido la transición de destino. Estas funciones devuelven true solo si la nueva lectura contiene el estado de destino (presionado o liberado, respectivamente) y la lectura anterior tampoco contiene el estado de destino; de lo contrario, devuelven false.

Detección de disposiciones de botones complejas

Cada botón de un dispositivo de entrada proporciona una lectura digital que indica si está presionado (hacia abajo) o liberado (hacia arriba). Por motivos de eficacia, las lecturas de los botones no se representan como valores booleanos individuales; en su lugar, se empaquetan todas en campos de bits que se representan mediante enumeraciones específicas de dispositivo como GamepadButtons. Para leer botones específicos, se usa el enmascaramiento bit a bit para aislar los valores que te interesan. Se presiona (abajo) un botón cuando se establece su bit correspondiente; de lo contrario, se libera (arriba).

Recuerde cómo se determina que se presionan o liberan los botones individuales; Aquí se muestran controladores para juegos, pero los principios son los mismos para stick arcade, volante y otros tipos de dispositivos de entrada.

GamepadReading reading = gamepad.GetCurrentReading();

// Determines whether gamepad button A is pressed.
if (GamepadButtons::A == (reading.Buttons & GamepadButtons::A))
{
    // The A button is pressed.
}

// Determines whether gamepad button A is released.
if (GamepadButtons::None == (reading.Buttons & GamepadButtons::A))
{
    // The A button is released (not pressed).
}

Como puede ver, determinar el estado de un solo botón es directo, pero a veces es posible que desee determinar si se presionan o liberan varios botones, o si un conjunto de botones se organizan de una manera determinada, algunos presionados, algunos no. Probar varios botones es más complejo que probar botones únicos (especialmente con el potencial de estado de botón mixto), pero hay una fórmula sencilla para estas pruebas que se aplican a las pruebas de un solo y varios botones por igual.

En el ejemplo siguiente se determina si se presionan los botones A y B del controlador para juegos:

if ((GamepadButtons::A | GamepadButtons::B) == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
    // The A and B buttons are both pressed.
}

En el ejemplo siguiente se determina si se liberan los botones A y B del controlador para juegos:

if ((GamepadButtons::None == (reading.Buttons & GamepadButtons::A | GamepadButtons::B))
{
    // The A and B buttons are both released (not pressed).
}

En el ejemplo siguiente se determina si se presiona el botón A del controlador para juegos mientras se suelta el botón B:

if (GamepadButtons::A == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
    // The A button is pressed and the B button is released (B is not pressed).
}

La fórmula que los cinco ejemplos tienen en común es que la disposición de los botones que se van a probar se especifica mediante la expresión de la izquierda del operador de igualdad mientras que los botones que se deben tomar en consideración se seleccionan mediante la expresión de enmascaramiento de la derecha.

En el ejemplo siguiente se muestra esta fórmula con más claridad si se vuelve a escribir el ejemplo anterior:

auto buttonArrangement = GamepadButtons::A;
auto buttonSelection = (reading.Buttons & (GamepadButtons::A | GamepadButtons::B));

if (buttonArrangement == buttonSelection)
{
    // The A button is pressed and the B button is released (B is not pressed).
}

Esta fórmula puede aplicarse para probar cualquier número de botones en cualquier disposición de sus estados.

Obtener el estado de la batería

Para cualquier controlador de juego que implemente la interfaz IGameControllerBatteryInfo , puedes llamar a TryGetBatteryReport en la instancia del controlador para obtener un objeto BatteryReport que proporcione información sobre la batería en el controlador. Puede obtener propiedades como la velocidad de carga de la batería (ChargeRateInMilliwatts), la capacidad de energía estimada de una nueva batería (DesignCapacityInMilliwattHours) y la capacidad de energía totalmente cargada de la batería actual (FullChargeCapacityInMilliwattHours).

Para los controladores de juego que admiten informes detallados de batería, puedes obtener esto y más información sobre la batería, como se detalla en Obtener información de la batería. Sin embargo, la mayoría de los controladores de juego no admiten ese nivel de informes de batería y, en su lugar, usan hardware de bajo costo. Para estos controladores, debe tener en cuenta las siguientes consideraciones:

  • ChargeRateInMilliwatts y DesignCapacityInMilliwattHours siempre serán NULL.

  • Puedes obtener el porcentaje de batería calculando RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours. Debe omitir los valores de estas propiedades y tratar solo con el porcentaje calculado.

  • El porcentaje del punto de viñeta anterior siempre será uno de los siguientes:

    • 100% (completo)
    • 70% (medio)
    • 40% (Bajo)
    • 10% (crítico)

Si el código realiza alguna acción (como dibujar la interfaz de usuario) en función del porcentaje de duración restante de la batería, asegúrese de que se ajusta a los valores anteriores. Por ejemplo, si quiere advertir al jugador cuando la batería del controlador sea baja, hágalo cuando alcance el 10 %.

Vea también