Agregar controles

Nota:

Este tema forma parte de la serie de tutoriales Crear un juego sencillo para la Plataforma universal de Windows (UWP) con DirectX. El tema de ese vínculo establece el contexto de la serie.

[ Actualizado para aplicaciones para UWP en Windows 10. Para ver los artículos de Windows 8.x, vea el archivo ]

Un buen juego para la Plataforma universal de Windows (UWP) admite una amplia variedad de interfaces. Un posible jugador podría tener Windows 10 en una tableta sin botones físicos, un equipo con un dispositivo de juego conectado o la plataforma de juegos de escritorio más reciente con un mouse de alto rendimiento y un teclado para juegos. En nuestro juego, los controles se implementan en la clase MoveLookController. Esta clase agrega los tres tipos de entrada (mouse y teclado, táctil y controlador para juegos) en un solo control. El resultado final es un tirador de primera persona que usa controles de estilo de movimiento estándar de género que funcionan con varios dispositivos.

Nota:

Para obtener más información sobre los controles, vea Controles de movimiento para juegos y Controles táctiles para juegos.

Objetivo

En este punto tenemos un juego que se representa, pero no podemos mover a nuestro jugador o disparar a los objetivos. Veremos cómo nuestro juego implementa controles de movimiento de primera persona para los siguientes tipos de entrada en nuestro juego DirectX para UWP.

  • Ratón y teclado
  • Tocar
  • Controlador para juegos

Nota:

Si no has descargado el código de juego más reciente para este ejemplo, ve al juego de ejemplo de Direct3D. Este ejemplo forma parte de una gran colección de ejemplos de características de UWP. Para obtener instrucciones sobre cómo descargar el ejemplo, vea Aplicaciones de ejemplo para el desarrollo de Windows.

Comportamientos comunes de control

Los controles táctiles y los controles de mouse/teclado tienen una implementación básica muy similar. En una aplicación para UWP, un puntero es simplemente un punto en la pantalla. Puedes moverlo deslizando el mouse o deslizando el dedo en la pantalla táctil. Como resultado, puede registrarse para un único conjunto de eventos y no preocuparse de si el jugador usa un mouse o una pantalla táctil para mover y presionar el puntero.

Cuando se inicializa la clase MoveLookController del juego de ejemplo, se registra para cuatro eventos específicos del puntero y un evento específico del mouse:

Evento Descripción
CoreWindow::PointerPressed Se ha presionado el botón izquierdo o derecho del mouse (y se mantiene presionado) o se tocó la superficie táctil.
CoreWindow::PointerMoved El mouse se movió o se realizó una acción de arrastre en la superficie táctil.
CoreWindow::PointerReleased Se ha soltado el botón primario del mouse o se ha levantado el objeto que estaba en contacto con la superficie táctil.
CoreWindow::PointerExited El puntero se movió fuera de la ventana principal.
Windows::Devices::Input::MouseMoved El mouse se movió a cierta distancia. Tenga en cuenta que solo estamos interesados en los valores delta de movimiento del mouse y no en la posición X-Y actual.

Estos controladores de eventos están configurados para empezar a escuchar la entrada del usuario tan pronto como el MoveLookController se inicializa en la ventana de la aplicación.

void MoveLookController::InitWindow(_In_ CoreWindow const& window)
{
    ResetState();

    window.PointerPressed({ this, &MoveLookController::OnPointerPressed });

    window.PointerMoved({ this, &MoveLookController::OnPointerMoved });

    window.PointerReleased({ this, &MoveLookController::OnPointerReleased });

    window.PointerExited({ this, &MoveLookController::OnPointerExited });

    ...

    // There is a separate handler for mouse-only relative mouse movement events.
    MouseDevice::GetForCurrentView().MouseMoved({ this, &MoveLookController::OnMouseMoved });

    ...
}

El código completo de InitWindow se puede ver en GitHub.

Para determinar cuándo el juego debe escuchar cierta entrada, la clase MoveLookController tiene tres estados específicos del control, independientemente del tipo de control:

Valor Descripción
Ninguna Este es el estado inicializado para el control. Todas las entradas se omiten, ya que el juego no anticipa ninguna entrada del control.
WaitForInput El control espera a que el jugador confirme un mensaje del juego mediante un clic izquierdo del mouse, un evento táctil, ot el botón de menú de un controlador para juegos.
Activas El control está en modo de juego activo.

Estado WaitForInput y pausar el juego

El juego entra en el estado WaitForInput cuando el juego se ha pausado. Esto sucede cuando el jugador mueve el puntero fuera de la ventana principal del juego, o presiona el botón de pausa (la tecla P o el botón Inicio del controlador para juegos). MoveLookController registra la pulsación, e informa al bucle del juego cuando llama al método IsPauseRequested. En ese momento, si isPauseRequested devuelve verdadero, el bucle del juego llama a WaitForPress en el MoveLookController para mover el controlador al estado WaitForInput .

Una vez en el estado WaitForInput, el juego deja de procesar casi todos los eventos de entrada del juego hasta que vuelve al estado Active. La excepción es el botón de pausa, cuya pulsación hace que el juego vuelva al estado activo. Aparte del botón de pausa, para que el juego vuelva a la Activo estado que el jugador necesita seleccionar un elemento de menú.

Estado activo

Durante el estado de Active, la instancia de MoveLookController está procesando eventos desde todos los dispositivos de entrada habilitados e interpretando las intenciones del jugador. Como resultado, actualiza la velocidad y la dirección de la vista del jugador y comparte los datos actualizados con el juego después de que Update sea llamado desde el bucle del juego.

Todas las entradas de puntero se rastrean en el estado Active, con diferentes Id. de puntero correspondientes a diferentes acciones de puntero. Cuando se recibe un evento PointerPressed, MoveLookController obtiene el valor de id, de puntero creado por la ventana. El id. de puntero representa un tipo específico de entrada. Por ejemplo, en un dispositivo multitáctil, puede haber varias entradas activas diferentes al mismo tiempo. Los id. se usan para realizar un seguimiento de la entrada que está usando el jugador. Si un evento se encuentra en el rectángulo de movimiento de la pantalla táctil, se asigna un Id. de puntero para rastrear cualquier evento de puntero en el rectángulo de movimiento. Otros eventos de puntero en el rectángulo de disparo se rastrean por separado, con un Id. de puntero independiente.

Nota:

Las entradas procedentes del mouse y del stick derecho de un controlador para juegos también tienen Id. que se manejan por separado.

Una vez que los eventos de puntero se han asignado a una acción de juego específica, es el momento de actualizar los datos el MoveLookController objeto comparte con el bucle de juego principal.

Cuando se llama, el método Update del juego de ejemplo procesa la entrada y actualiza las variables de velocidad y dirección de la mirada (m_velocity y m_lookdirection), que el bucle del juego recupera llamando a los métodos públicos Velocity y LookDirection.

Nota:

Más información sobre el método Update se puede ver más adelante en esta página.

El bucle del juego puede comprobar si el jugador está disparando llamando al método IsFiring en la instancia MoveLookController. MoveLookController comprueba si el jugador ha pulsado el botón de disparo en uno de los tres tipos de entrada.

bool MoveLookController::IsFiring()
{
    if (m_state == MoveLookControllerState::Active)
    {
        if (m_autoFire)
        {
            return (m_fireInUse || (m_mouseInUse && m_mouseLeftInUse) || PollingFireInUse());
        }
        else
        {
            if (m_firePressed)
            {
                m_firePressed = false;
                return true;
            }
        }
    }
    return false;
}

Veamos ahora la implementación de cada uno de los tres tipos de control con un poco más de detalle.

Adición de controles relativos del mouse

Si se detecta movimiento del mouse, queremos usar ese movimiento para determinar el nuevo rotación alrededor del eje x (pitch) y rotación alrededor del eje y (yaw) de la cámara. Para ello, implementamos controles relativos del mouse, donde manejamos la distancia relativa que el mouse ha movido (el delta entre el inicio del movimiento y la parada), en lugar de registrar las coordenadas absolutas de píxeles x-y del movimiento.

Para ello, obtenemos los cambios en las coordenadas X (el movimiento horizontal) e Y (el movimiento vertical) examinando los campos MouseDelta::X y MouseDelta::Y del objeto argumento Windows::Device::Input::MouseEventArgs::MouseDelta devuelto por el evento MouseMoved.

void MoveLookController::OnMouseMoved(
    _In_ MouseDevice const& /* mouseDevice */,
    _In_ MouseEventArgs const& args
    )
{
    // Handle Mouse Input via dedicated relative movement handler.

    switch (m_state)
    {
    case MoveLookControllerState::Active:
        XMFLOAT2 mouseDelta;
        mouseDelta.x = static_cast<float>(args.MouseDelta().X);
        mouseDelta.y = static_cast<float>(args.MouseDelta().Y);

        XMFLOAT2 rotationDelta;
        // Scale for control sensitivity.
        rotationDelta.x = mouseDelta.x * MoveLookConstants::RotationGain;
        rotationDelta.y = mouseDelta.y * MoveLookConstants::RotationGain;

        // Update our orientation based on the command.
        m_pitch -= rotationDelta.y;
        m_yaw += rotationDelta.x;

        // Limit pitch to straight up or straight down.
        float limit = XM_PI / 2.0f - 0.01f;
        m_pitch = __max(-limit, m_pitch);
        m_pitch = __min(+limit, m_pitch);

        // Keep longitude in sane range by wrapping.
        if (m_yaw > XM_PI)
        {
            m_yaw -= XM_PI * 2.0f;
        }
        else if (m_yaw < -XM_PI)
        {
            m_yaw += XM_PI * 2.0f;
        }
        break;
    }
}

Adición de compatibilidad táctil

Los controles táctiles son estupendos para ayudar a los usuarios con tabletas. Este juego recopila la información táctil mediante la división en zonas de determinadas áreas de la pantalla, cada una de ellas alineada con acciones específicas del juego. La entrada táctil de este juego usa tres zonas.

move look touch layout

Los siguientes comandos resumen nuestro comportamiento de control táctil. Entrada del usuario | Acción :------- | :-------- Mover rectángulo | La entrada táctil se convierte en un joystick virtual donde el movimiento vertical se traducirá en movimiento de posición adelante/atrás y el movimiento horizontal se traducirá en movimiento de posición izquierda/derecha. Rectángulo de disparo | Dispara una esfera. Tocar fuera del rectángulo de movimiento y disparo | Cambie la rotación (la rotación alrededor del eje x y rotación alrededor del eje y) de la vista de cámara.

MoveLookController comprueba el Id. del puntero para determinar dónde se ha producido el evento y realiza una de las siguientes acciones:

  • Si el evento PointerMoved se produjo en el rectángulo de movimiento o disparo, actualiza la posición del puntero para el control.
  • Si el evento PointerMoved se produjo en algún lugar del resto de la pantalla (definido como controles de apariencia), calcule el cambio en la rotación alrededor del eje x (pitch) y rotación alrededor del eje y (yaw) del vector de dirección de apariencia.

Una vez que hayamos implementado nuestros controles táctiles, los rectángulos que dibujamos antes con Direct2D indicarán a los jugadores dónde están las zonas de movimiento, disparo y apariencia.

touch controls

Ahora echemos un vistazo a cómo implementamos cada control.

Control de movimiento y disparo

El rectángulo del control de movimiento situado en el cuadrante inferior izquierdo de la pantalla se utiliza como pad direccional. Deslizando el pulgar a izquierda y derecha dentro de este espacio, el jugador se mueve a izquierda y derecha, mientras que arriba y abajo la cámara avanza y retrocede. Después de configurarlo, toca el control de disparo en el cuadrante inferior derecho de la pantalla para disparar una esfera.

Los métodos SetMoveRect y SetFireRect crean nuestros rectángulos de entrada, tomando dos vectores 2D para especificar las posiciones de las esquinas superior izquierda e inferior derecha de cada rectángulo en la pantalla.

A continuación, los parámetros se asignan a m_fireUpperLeft y m_fireLowerRight que nos ayudarán a determinar si el usuario está tocando dentro de los rectángulos.

m_fireUpperLeft = upperLeft;
m_fireLowerRight = lowerRight;

Si se cambia el tamaño de la pantalla, estos rectángulos se redibujan al tamaño adecuado.

Ahora que hemos quitado las zonas de nuestros controles, es el momento de determinar cuándo un usuario los usa realmente. Para ello, configuramos algunos controladores de eventos en el método MoveLookController::InitWindow para cuando el usuario presiona, mueve o suelta su puntero.

window.PointerPressed({ this, &MoveLookController::OnPointerPressed });

window.PointerMoved({ this, &MoveLookController::OnPointerMoved });

window.PointerReleased({ this, &MoveLookController::OnPointerReleased });

Primero determinaremos qué ocurre cuando el usuario presiona por primera vez dentro de los rectángulos de movimiento o disparo utilizando el método OnPointerPressed. Aquí comprobamos dónde están tocando un control y si ya hay un puntero en ese controlador. Si este es el primer dedo para tocar el control específico, hacemos lo siguiente.

  • Almacene la ubicación del control táctil en m_moveFirstDown o m_fireFirstDown como un vector 2D.
  • Asigne el id. de puntero a m_movePointerID o m_firePointerID.
  • Establece la marca InUse adecuada (m_moveInUse o m_fireInUse) a true ya que ahora tenemos un puntero activo para ese control.
PointerPoint point = args.CurrentPoint();
uint32_t pointerID = point.PointerId();
Point pointerPosition = point.Position();
PointerPointProperties pointProperties = point.Properties();
auto pointerDevice = point.PointerDevice();
auto pointerDeviceType = pointerDevice.PointerDeviceType();

XMFLOAT2 position = XMFLOAT2(pointerPosition.X, pointerPosition.Y);

...
case MoveLookControllerState::Active:
    switch (pointerDeviceType)
    {
    case winrt::Windows::Devices::Input::PointerDeviceType::Touch:
        // Check to see if this pointer is in the move control.
        if (position.x > m_moveUpperLeft.x &&
            position.x < m_moveLowerRight.x &&
            position.y > m_moveUpperLeft.y &&
            position.y < m_moveLowerRight.y)
        {
            // If no pointer is in this control yet.
            if (!m_moveInUse)
            {
                // Process a DPad touch down event.
                // Save the location of the initial contact
                m_moveFirstDown = position;
                // Store the pointer using this control
                m_movePointerID = pointerID;
                // Set InUse flag to signal there is an active move pointer
                m_moveInUse = true;
            }
        }
        // Check to see if this pointer is in the fire control.
        else if (position.x > m_fireUpperLeft.x &&
            position.x < m_fireLowerRight.x &&
            position.y > m_fireUpperLeft.y &&
            position.y < m_fireLowerRight.y)
        {
            if (!m_fireInUse)
            {
                // Save the location of the initial contact
                m_fireLastPoint = position;
                // Store the pointer using this control
                m_firePointerID = pointerID;
                // Set InUse flag to signal there is an active fire pointer
                m_fireInUse = true;
                ...
            }
        }
        ...

Ahora que hemos determinado si el usuario está tocando un control de movimiento o disparo, vemos si el jugador está realizando movimientos con su dedo presionado. Con el método MoveLookController::OnPointerMoved, comprobamos qué puntero se ha movido y luego almacenamos su nueva posición como un vector 2D.

PointerPoint point = args.CurrentPoint();
uint32_t pointerID = point.PointerId();
Point pointerPosition = point.Position();
PointerPointProperties pointProperties = point.Properties();
auto pointerDevice = point.PointerDevice();

// convert to allow math
XMFLOAT2 position = XMFLOAT2(pointerPosition.X, pointerPosition.Y);

switch (m_state)
{
case MoveLookControllerState::Active:
    // Decide which control this pointer is operating.

    // Move control
    if (pointerID == m_movePointerID)
    {
        // Save the current position.
        m_movePointerPosition = position;
    }
    // Look control
    else if (pointerID == m_lookPointerID)
    {
        ...
    }
    // Fire control
    else if (pointerID == m_firePointerID)
    {
        m_fireLastPoint = position;
    }
    ...

Una vez que el usuario haya realizado sus gestos dentro de los controles, soltará el puntero. Con el método MoveLookController::OnPointerReleased, determinamos qué puntero se ha soltado y realiza una serie de restablecimientos.

Si se ha soltado el control de movimiento, hacemos lo siguiente.

  • Establezca la velocidad del jugador 0 en todas las direcciones para evitar que se muevan en el juego.
  • Cambie m_moveInUse a false, ya que el usuario ya no está tocando el controlador de movimiento.
  • Establezca el id. del puntero de movimiento en 0, ya que ya no hay un puntero en el controlador de movimiento.
if (pointerID == m_movePointerID)
{
    // Stop on release.
    m_velocity = XMFLOAT3(0, 0, 0);
    m_moveInUse = false;
    m_movePointerID = 0;
}

Para el control de disparo, si ha sido soltado todo lo que hacemos es cambiar la marca m_fireInUse a false y el Id. del puntero de disparo a 0, ya que ya no hay un puntero en el control de disparo.

else if (pointerID == m_firePointerID)
{
    m_fireInUse = false;
    m_firePointerID = 0;
}

Buscar control

Tratamos los eventos del puntero del dispositivo táctil para las regiones no utilizadas de la pantalla como el controlador de apariencia. Deslizar el dedo alrededor de esta zona cambia la rotación alrededor del eje x (pitch) y la rotación alrededor del eje y (yaw) (rotación) de la cámara del jugador.

Si el evento MoveLookController::OnPointerPressed se genera en un dispositivo táctil de esta región y el estado del juego se establece en Activo, se le asigna un Id. de puntero.

// If no pointer is in this control yet.
if (!m_lookInUse)
{
    // Save point for later move.
    m_lookLastPoint = position;
    // Store the pointer using this control.
    m_lookPointerID = pointerID;
    // These are for smoothing.
    m_lookLastDelta.x = m_lookLastDelta.y = 0;
    m_lookInUse = true;
}

Aquí el MoveLookController asigna el Id. del puntero que disparó el evento a una variable específica que corresponde a la región de la mirada. En el caso de que se produzca un toque en la región de la mirada, la variable m_lookPointerID se establece en el Id. del puntero que disparó el evento. También se establece una variable booleana, m_lookInUse, para indicar que el control aún no se ha soltado.

Ahora, veamos cómo el juego de ejemplo maneja el evento de pantalla táctil PointerMoved.

Dentro del método MoveLookController::OnPointerMoved, comprobamos qué tipo de Id. de puntero se ha asignado al evento. Si es m_lookPointerID, calculamos el cambio de posición del puntero. A continuación, usamos esta diferencia para calcular cuánto debe cambiar la rotación. Por último, estamos en un punto en el que podemos actualizar el m_pitch y m_yaw que se usarán en el juego para cambiar la rotación del jugador.

// This is the look pointer.
else if (pointerID == m_lookPointerID)
{
    // Look control.
    XMFLOAT2 pointerDelta;
    // How far did the pointer move?
    pointerDelta.x = position.x - m_lookLastPoint.x;
    pointerDelta.y = position.y - m_lookLastPoint.y;

    XMFLOAT2 rotationDelta;
    // Scale for control sensitivity.
    rotationDelta.x = pointerDelta.x * MoveLookConstants::RotationGain;
    rotationDelta.y = pointerDelta.y * MoveLookConstants::RotationGain;
    // Save for next time through.
    m_lookLastPoint = position;

    // Update our orientation based on the command.
    m_pitch -= rotationDelta.y;
    m_yaw += rotationDelta.x;

    // Limit pitch to straight up or straight down.
    float limit = XM_PI / 2.0f - 0.01f;
    m_pitch = __max(-limit, m_pitch);
    m_pitch = __min(+limit, m_pitch);
    ...
}

La última pieza que veremos es cómo el juego de ejemplo maneja el evento de pantalla táctil PointerReleased. Una vez que el usuario ha finalizado el gesto táctil y ha retirado el dedo de la pantalla, se inicia MoveLookController::OnPointerReleased. Si el Id. del puntero que disparó el evento PointerReleased es el Id. del puntero de movimiento previamente registrado, el MoveLookController establece la velocidad a 0 porque el jugador ha dejado de tocar el área de apariencia.

else if (pointerID == m_lookPointerID)
{
    m_lookInUse = false;
    m_lookPointerID = 0;
}

Adición de compatibilidad con el mouse y el teclado

Este juego tiene el siguiente diseño de control para el teclado y el mouse.

Entrada de usuario Acción
W Mover el jugador hacia delante
A Mover jugador a la izquierda
S Mover el jugador hacia atrás
D Mover el jugador a la derecha
X Mover vista hacia arriba
Barra espaciadora Mover vista hacia abajo
P Pausar el juego
Movimiento del ratón Cambiar la rotación (la rotación alrededor del eje x y la rotación alrededor del eje y) de la vista de cámara
Botón izquierdo del mouse Disparar una esfera

Para usar el teclado, el juego de ejemplo registra dos nuevos eventos, CoreWindow::KeyUp y CoreWindow::KeyDown, dentro del método MoveLookController::InitWindow. Estos eventos controlan la prensa y la liberación de una tecla.

window.KeyDown({ this, &MoveLookController::OnKeyDown });

window.KeyUp({ this, &MoveLookController::OnKeyUp });

El mouse se trata de una forma ligeramente distinta a los controles táctiles, a pesar de que usa un puntero. Para alinearse con nuestro diseño de control, el MoveLookController gira la cámara cada vez que se mueve el ratón, y se dispara cuando se pulsa el botón izquierdo del ratón.

Esto se controla en el método OnPointerPressed del MoveLookController.

En este método comprobamos qué tipo de dispositivo puntero se está utilizando con la enumeración Windows::Devices::Input::PointerDeviceType. Si el juego es Activo y PointerDeviceType no es de Entrada táctil, suponemos que es la entrada del mouse.

case MoveLookControllerState::Active:
    switch (pointerDeviceType)
    {
    case winrt::Windows::Devices::Input::PointerDeviceType::Touch:
        // Behavior for touch controls
        ...

    default:
        // Behavior for mouse controls
        bool rightButton = pointProperties.IsRightButtonPressed();
        bool leftButton = pointProperties.IsLeftButtonPressed();

        if (!m_autoFire && (!m_mouseLeftInUse && leftButton))
        {
            m_firePressed = true;
        }

        if (!m_mouseInUse)
        {
            m_mouseInUse = true;
            m_mouseLastPoint = position;
            m_mousePointerID = pointerID;
            m_mouseLeftInUse = leftButton;
            m_mouseRightInUse = rightButton;
            // These are for smoothing.
            m_lookLastDelta.x = m_lookLastDelta.y = 0;
        }
        break;
    }
    break;

Cuando el reproductor deja de presionar uno de los botones del mouse, se genera el evento del mouse CoreWindow::PointerReleased, llamando al método MoveLookController::OnPointerReleased y se completa la entrada. En este punto, las esferas dejarán de dispararse si se estaba pulsando el botón izquierdo del ratón y ahora se suelta. Como la apariencia siempre está habilitada, el juego sigue utilizando el mismo puntero del ratón para seguir los eventos de apariencia en curso.

case MoveLookControllerState::Active:
    // Touch points
    if (pointerID == m_movePointerID)
    {
        // Stop movement
        ...
    }
    else if (pointerID == m_lookPointerID)
    {
        // Stop look rotation
        ...
    }
    // Fire button has been released
    else if (pointerID == m_firePointerID)
    {
        // Stop firing
        ...
    }
    // Mouse point
    else if (pointerID == m_mousePointerID)
    {
        bool rightButton = pointProperties.IsRightButtonPressed();
        bool leftButton = pointProperties.IsLeftButtonPressed();

        // Mouse no longer in use so stop firing
        m_mouseInUse = false;

        // Don't clear the mouse pointer ID so that Move events still result in Look changes.
        // m_mousePointerID = 0;
        m_mouseLeftInUse = leftButton;
        m_mouseRightInUse = rightButton;
    }
    break;

Ahora echemos un vistazo al último tipo de control que admitiremos: controladores para juegos. Los controladores para juegos se controlan por separado de los controles táctiles y del mouse, ya que no usan el objeto de puntero. Debido a esto, es necesario agregar algunos nuevos controladores de eventos y métodos.

Agregar compatibilidad con el controlador para juegos

Para este juego, la compatibilidad con el controlador para juegos se agrega mediante llamadas a las API de Windows.Gaming.Input . Este conjunto de API proporciona acceso a las entradas de controladores de juegos como volantes de carreras y sticks de vuelo.

Los siguientes serán nuestros controles del controlador para juegos.

Entrada de usuario Acción
Stick analógico izquierdo Mover jugador
Stick analógico derecho Cambiar la rotación (la rotación alrededor del eje x y la rotación alrededor del eje y) de la vista de cámara
Gatillo derecho Disparar una esfera
Botón Inicio/menú Pausar o reanudar el juego

En el método InitWindow, agregamos dos nuevos eventos para determinar si se ha agregado o quitado un controlador para juegos. Estos eventos actualizan la propiedad m_gamepadsChanged. Esto se usa en el método UpdatePollingDevices para comprobar si ha cambiado la lista de controladores para juegos conocidos.

// Detect gamepad connection and disconnection events.
Gamepad::GamepadAdded({ this, &MoveLookController::OnGamepadAdded });

Gamepad::GamepadRemoved({ this, &MoveLookController::OnGamepadRemoved });

Nota:

Las aplicaciones para UWP no pueden recibir entradas de un dispositivo de juego mientras la aplicación no está en el foco.

El método UpdatePollingDevices

El método UpdatePollingDevices de la instancia MoveLookController comprueba inmediatamente si un controlador para juegos está conectado. Si lo es, empezaremos a leer su estado con Gamepad.GetCurrentReading. Esto devuelve la estructura GamepadReading, lo que nos permite comprobar qué botones se han pulsado o qué palancas de control se han movido.

Si el estado del juego es WaitForInput, solo escuchamos el botón Inicio/menú del mando para que se pueda reanudar el juego.

Si es Activo, comprobamos la entrada del usuario y determinamos qué acción del juego tiene que ocurrir. Por ejemplo, si el usuario mueve el stick analógico izquierdo en una dirección determinada, el juego sabrá que debe mover al jugador en esa dirección. El movimiento del stick en una dirección determinada debe registrarse como mayor que el radio de la zona muerta; de lo contrario, no ocurrirá nada. Este radio de zona muerta es necesario para evitar la "deriva", que se produce cuando el mando capta pequeños movimientos del pulgar del jugador al apoyarlo en el stick. Sin zonas muertas, los controles pueden parecer demasiado sensibles al usuario.

La entrada de la palanca está entre -1 y 1 tanto para el eje x como para el eje y. La siguiente consante especifica el radio de la zona muerta de la palanca del mando.

#define THUMBSTICK_DEADZONE 0.25f

Usando esta variable, comenzaremos a procesar la entrada accionable de la palanca del mando. El movimiento se produciría con un valor de [-1, -,26] o [,26, 1] en cualquiera de los ejes.

dead zone for thumbsticks

Esta parte del método UpdatePollingDevices controla la palanca izquierda y derecha. Los valores X e Y de cada stick se comprueban para ver si están fuera de la zona muerta. Si uno o ambos son, actualizaremos el componente correspondiente. Por ejemplo, si la palanca izquierda se mueve hacia la izquierda a lo largo del eje X, agregaremos -1 al componente x del vector m_moveCommand. Este vector es lo que se usará para agregar todos los movimientos en todos los dispositivos y posteriormente se usará para calcular dónde debe moverse el jugador.

// Use the left thumbstick to control the eye point position
// (position of the player).

// Check if left thumbstick is outside of dead zone on x axis
if (reading.LeftThumbstickX > THUMBSTICK_DEADZONE ||
    reading.LeftThumbstickX < -THUMBSTICK_DEADZONE)
{
    // Get value of left thumbstick's position on x axis
    float x = static_cast<float>(reading.LeftThumbstickX);
    // Set the x of the move vector to 1 if the stick is being moved right.
    // Set to -1 if moved left. 
    m_moveCommand.x -= (x > 0) ? 1 : -1;
}

// Check if left thumbstick is outside of dead zone on y axis
if (reading.LeftThumbstickY > THUMBSTICK_DEADZONE ||
    reading.LeftThumbstickY < -THUMBSTICK_DEADZONE)
{
    // Get value of left thumbstick's position on y axis
    float y = static_cast<float>(reading.LeftThumbstickY);
    // Set the y of the move vector to 1 if the stick is being moved forward.
    // Set to -1 if moved backwards.
    m_moveCommand.y += (y > 0) ? 1 : -1;
}

Al igual que el stick izquierdo controla el movimiento, el derecho controla la rotación de la cámara.

El comportamiento de la palanca derecha se alinea con el comportamiento del movimiento del ratón en nuestra configuración de control de mouse y teclado. Si el stick se encuentra fuera de la zona muerta, calculamos la diferencia entre la posición actual del puntero y el lugar donde el usuario intenta mirar ahora. Este cambio en la posición del puntero (pointerDelta) se utiliza entonces para actualizar el rotación alrededor del eje x (pitch) y la rotación alrededor del eje y (yaw) de la rotación de la cámara que más tarde se aplican en nuestro método Update. El vector pointerDelta puede resultarte familiar porque también se usa en el método MoveLookController::OnPointerMoved para realizar un seguimiento del cambio en la posición del puntero para nuestras entradas de mouse y táctiles.

// Use the right thumbstick to control the look at position

XMFLOAT2 pointerDelta;

// Check if right thumbstick is outside of deadzone on x axis
if (reading.RightThumbstickX > THUMBSTICK_DEADZONE ||
    reading.RightThumbstickX < -THUMBSTICK_DEADZONE)
{
    float x = static_cast<float>(reading.RightThumbstickX);
    // Register the change in the pointer along the x axis
    pointerDelta.x = x * x * x;
}
// No actionable thumbstick movement. Register no change in pointer.
else
{
    pointerDelta.x = 0.0f;
}
// Check if right thumbstick is outside of deadzone on y axis
if (reading.RightThumbstickY > THUMBSTICK_DEADZONE ||
    reading.RightThumbstickY < -THUMBSTICK_DEADZONE)
{
    float y = static_cast<float>(reading.RightThumbstickY);
    // Register the change in the pointer along the y axis
    pointerDelta.y = y * y * y;
}
else
{
    pointerDelta.y = 0.0f;
}

XMFLOAT2 rotationDelta;
// Scale for control sensitivity.
rotationDelta.x = pointerDelta.x * 0.08f;
rotationDelta.y = pointerDelta.y * 0.08f;

// Update our orientation based on the command.
m_pitch += rotationDelta.y;
m_yaw += rotationDelta.x;

// Limit pitch to straight up or straight down.
m_pitch = __max(-XM_PI / 2.0f, m_pitch);
m_pitch = __min(+XM_PI / 2.0f, m_pitch);

Los controles del juego no estarían completos sin la posibilidad de disparar esferas.

Este método UpdatePollingDevices también comprueba si se presiona el gatillo derecho. Si es así, nuestra propiedad m_firePressed se invierte a verdadero, indicando al juego que las esferas deben empezar a dispararse.

if (reading.RightTrigger > TRIGGER_DEADZONE)
{
    if (!m_autoFire && !m_gamepadTriggerInUse)
    {
        m_firePressed = true;
    }

    m_gamepadTriggerInUse = true;
}
else
{
    m_gamepadTriggerInUse = false;
}

El método Update

Para encapsular las cosas, vamos a profundizar en el método Update. Este método combina cualquier movimiento o rotación que el jugador haya realizado con cualquier entrada soportada para generar un vector de velocidad y actualizar nuestros valores de rotación alrededor del eje x (pitch) y rotación alrededor del eje y (yaw) para que nuestro bucle de juego acceda a ellos.

El método Update comienza llamando a UpdatePollingDevices para actualizar el estado del controlador. Este método también recopila cualquier entrada de un controlador para juegos y agrega sus movimientos al vector m_moveCommand.

En nuestro método Update , realizamos las siguientes comprobaciones de entrada.

  • Si el jugador está utilizando el rectángulo del controlador de movimiento, determinaremos el cambio en la posición del puntero y lo utilizaremos para calcular si el usuario ha movido el puntero fuera de la zona muerta del controlador. Si está fuera de la zona muerta, la propiedad vectorial m_moveCommand se actualiza con el valor del joystick virtual.
  • Si se presiona alguna de las entradas del teclado de movimiento, se agrega un valor de 1.0f o -1.0f en el componente correspondiente del vector de m_moveCommand ,1.0f para ir hacia adelante y -1.0f para ir hacia atrás.

Una vez que se ha tenido en cuenta toda la entrada de movimiento, entonces ejecutamos el vector de m_moveCommand a través de algunos cálculos para generar un nuevo vector que representa la dirección del jugador con respecto al mundo del juego. A continuación, tomamos nuestros movimientos en relación con el mundo y los aplicamos al jugador como velocidad en esa dirección. Por último, restablecemos el vector de m_moveCommand para (0.0f, 0.0f, 0.0f) que todo esté listo para el siguiente fotograma del juego.

void MoveLookController::Update()
{
    // Get any gamepad input and update state
    UpdatePollingDevices();

    if (m_moveInUse)
    {
        // Move control.
        XMFLOAT2 pointerDelta;

        pointerDelta.x = m_movePointerPosition.x - m_moveFirstDown.x;
        pointerDelta.y = m_movePointerPosition.y - m_moveFirstDown.y;

        // Figure out the command from the virtual joystick.
        XMFLOAT3 commandDirection = XMFLOAT3(0.0f, 0.0f, 0.0f);
        // Leave 32 pixel-wide dead spot for being still.
        if (fabsf(pointerDelta.x) > 16.0f)
            m_moveCommand.x -= pointerDelta.x / fabsf(pointerDelta.x);

        if (fabsf(pointerDelta.y) > 16.0f)
            m_moveCommand.y -= pointerDelta.y / fabsf(pointerDelta.y);
    }

    // Poll our state bits set by the keyboard input events.
    if (m_forward)
    {
        m_moveCommand.y += 1.0f;
    }
    if (m_back)
    {
        m_moveCommand.y -= 1.0f;
    }
    if (m_left)
    {
        m_moveCommand.x += 1.0f;
    }
    if (m_right)
    {
        m_moveCommand.x -= 1.0f;
    }
    if (m_up)
    {
        m_moveCommand.z += 1.0f;
    }
    if (m_down)
    {
        m_moveCommand.z -= 1.0f;
    }

    // Make sure that 45deg cases are not faster.
    if (fabsf(m_moveCommand.x) > 0.1f ||
        fabsf(m_moveCommand.y) > 0.1f ||
        fabsf(m_moveCommand.z) > 0.1f)
    {
        XMStoreFloat3(&m_moveCommand, XMVector3Normalize(XMLoadFloat3(&m_moveCommand)));
    }

    // Rotate command to align with our direction (world coordinates).
    XMFLOAT3 wCommand;
    wCommand.x = m_moveCommand.x * cosf(m_yaw) - m_moveCommand.y * sinf(m_yaw);
    wCommand.y = m_moveCommand.x * sinf(m_yaw) + m_moveCommand.y * cosf(m_yaw);
    wCommand.z = m_moveCommand.z;

    // Scale for sensitivity adjustment.
    // Our velocity is based on the command. Y is up.
    m_velocity.x = -wCommand.x * MoveLookConstants::MovementGain;
    m_velocity.z = wCommand.y * MoveLookConstants::MovementGain;
    m_velocity.y = wCommand.z * MoveLookConstants::MovementGain;

    // Clear movement input accumulator for use during next frame.
    m_moveCommand = XMFLOAT3(0.0f, 0.0f, 0.0f);
}

Pasos siguientes

Ahora que hemos agregado nuestros controles, hay otra característica que necesitamos agregar para crear un juego inmersivo: el sonido. La música y los efectos de sonido son importantes para cualquier juego, así que vamos a hablar de cómo agregar sonido a continuación.