Prácticas 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 realizar un seguimiento de los jugadores y qué dispositivos de entrada y navegación están usando actualmente
- cómo detectar transiciones de botón (presionadas para liberarse, publicadas a presionar)
- cómo detectar arreglos de botones complejos 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 quiera enumerar todos los objetos RawGameController como línea base para asegurarse 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 seleccionada. Por ejemplo, si el dispositivo también es un Controlador para juegos, es posible que quiera ajustar la interfaz de usuario de asignación de botones para reflejarlo y proporcionar algunas asignaciones de botones predeterminadas razonables entre las que elegir. (Esto contrasta con exigir al jugador que configure manualmente las entradas del controlador para juegos si solo usas 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 que siguen siendo compatibles 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 reproductor desconecta su controlador o conecta uno nuevo? Debe controlar estos eventos y actualizar la lista en consecuencia. 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 eliminados se generan de forma asincrónica, puede obtener resultados incorrectos al tratar con la lista de controladores. Por lo tanto, siempre 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 tarda un segundo o dos en rellenarse. Por lo tanto, si solo asignas el controlador para juegos actual en el método start, 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 siguiente método simplemente devuelve el primer controlador para juegos de la lista (o nullptr si la lista está vacía). A continuación, solo tiene que recordar comprobar nullptr en cualquier momento que haga 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;
}
Reunirlo 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 están asociados a un usuario para que su identidad se pueda vincular a su juego, logros, cambios de configuración y otras actividades. Los usuarios pueden iniciar sesión o cerrar sesión a 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 para realizar un seguimiento de los jugadores y los dispositivos que están usando.
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 de los dispositivos que usan.
Detección de transiciones de botón
A veces quiere saber cuándo se presiona o suelta un botón por primera vez; es decir, precisamente cuando el estado del botón pasa de liberado a presionado o de presionado a liberado. Para determinar esto, debe recordar la lectura del dispositivo anterior y comparar la lectura actual con ella para ver lo que ha cambiado.
En el ejemplo siguiente se muestra un enfoque básico para recordar la lectura anterior; Los controladores para juegos se muestran aquí, pero los principios son los mismos para el stick arcade, la rueda de carreras y los 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 hacer cualquier otra cosa, mueve el valor existente de newReading
(el controlador para juegos que lee desde la iteración de bucle anterior) a oldReading
y, a continuación, Game::Loop
se rellena newReading
con una lectura del controlador para juegos nueva para la iteración actual. Esto le proporciona la información que necesita 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 oldReading
y, 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 no también contiene el estado de destino; de lo contrario, devuelven false.
Detección de arreglos complejos de botones
Cada botón de un dispositivo de entrada proporciona una lectura digital que indica si se presiona (hacia abajo) o se libera (hacia arriba). Para mejorar la eficacia, las lecturas de botón no se representan como valores booleanos individuales; en su lugar, todos se empaquetan en campos de bits representados por enumeraciones específicas del dispositivo, como GamepadButtons. Para leer botones específicos, se usa el enmascaramiento bit a bit para aislar los valores que le 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 únicos; Los controladores para juegos se muestran aquí, pero los principios son los mismos para el stick arcade, la rueda de carreras y los 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 quiera 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 individuales (especialmente con el potencial de estado de botón mixto), pero hay una fórmula sencilla para estas pruebas que se aplica a pruebas únicas y múltiples por igual.
En el ejemplo siguiente se determina si se presionan los botones del controlador para juegos A y B:
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 del controlador para juegos A y B:
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 expresión de enmascaramiento en el lado izquierdo del operador de igualdad especifica la disposición de botones para los que se va a probar.
En el ejemplo siguiente se muestra esta fórmula con más claridad mediante la reescritura del 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 se puede aplicar 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 proporciona información sobre la batería en el controlador. Puede obtener propiedades como la velocidad de carga de la batería (ChargeRateInMilliwatts), la capacidad estimada de energía de una nueva batería (DesignCapacityInMilliwattHours) y la capacidad de energía totalmente cargada de la batería actual (FullChargeCapacityInMilliwattHours).
Para los controladores de juegos 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 juegos no admiten ese nivel de informes de batería y, en su lugar, usan hardware de bajo costo. Para estos controladores, deberá 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 solo tratar 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 interfaz de usuario) en función del porcentaje de duración de la batería restante, 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 %.