Adicionar controles

Observação

Este tópico faz parte da série de tutoriais Criar um jogo simples para a Plataforma Universal do Windows (UWP) com o DirectX. O tópico na página do link define o contexto da série.

[ Atualizações para aplicativos UWP no Windows 10. Para artigos sobre o Windows 8.x, consulte o arquivo ]

Um bom jogo da Plataforma Universal do Windows (UWP) dá suporte para uma ampla variedade de interfaces. Um player em potencial pode ter o Windows 10 em um tablet sem botões físicos, um PC com um controlador de jogos anexado ou o mais recente equipamento de jogos para desktop com um mouse de alto desempenho e um teclado para jogos. No nosso jogo, os controles são implementados na classe MoveLookController. Essa classe agrega todos os três tipos de entrada (mouse e teclado, toque e gamepad) em um único controlador. O resultado final é um jogo de tiro em primeira pessoa que utiliza controles de visão de movimento padrão do gênero que funcionam com vários dispositivos.

Observação

Para obter mais informações sobre os controles, confira Controles de visão de movimento para jogos e Controles de toque para jogos.

Objetivo

Nesse ponto, temos um jogo que renderiza, mas não podemos mover nosso player nem atirar nos alvos. Vamos dar uma olhada em como nosso jogo implementa controles visão de movimento para um atirador em primeira pessoa para os seguintes tipos de entrada em nosso jogo UWP DirectX.

  • Mouse e teclado
  • Tocar
  • Gamepad

Observação

Se não tiver feito download do código de jogo mais recente para essa amostra, acesse jogo de exemplo do Direct3D. Esta amostra faz parte de uma grande coleção de amostras de recursos UWP. Para obter instruções sobre como fazer o download da amostra, confira Aplicativos de amostra para desenvolvimento do Windows.

Comportamentos controlados comuns

Os controles de toque e os controles de mouse/teclado têm um núcleo de implementação muito semelhante. Em um aplicativo UWP, um ponteiro é simplesmente um ponto na tela. Você pode movê-lo deslizando o mouse ou deslizando o dedo na tela de entrada por toque. Como resultado, você pode se registrar para um único conjunto de eventos e não se preocupar se o player está utilizando um mouse ou uma tela de entrada por toque para mover e pressionar o ponteiro.

Quando a classe MoveLookController do jogo de exemplo é inicializada, ela se registra para quatro eventos específicos de ponteiro e um evento específico de mouse:

Evento Descrição
CoreWindow::PointerPressed O botão esquerdo ou direito do mouse foi pressionado (e mantido), ou a superfície de toque foi tocada.
CoreWindow::PointerMoved O mouse se moveu ou foi feita uma ação de arrastar na superfície de entrada por toque.
CoreWindow::PointerReleased O botão esquerdo do mouse foi liberado ou o objeto em contato com a superfície de toque foi levantado.
CoreWindow::PointerExited O ponteiro saiu da janela principal.
Windows::Devices::Input::MouseMoved O mouse se moveu a uma certa distância. Esteja ciente de que estamos interessados apenas nos valores delta do movimento do mouse, e não na posição X-Y atual.

Esses manipuladores de eventos estão definidos como sendo os que começam a escutar a entrada do usuário assim que o MoveLookController é inicializado na janela do aplicativo.

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 });

    ...
}

O código completo de InitWindow pode ser visto no GitHub.

Para determinar quando o jogo deve estar escutando determinada entrada, a classe MoveLookController tem três estados específicos do controlador, independentemente do tipo de controlador:

State Descrição
Nenhuma Esse é o estado inicializado do controlador. Todas as entradas são ignoradas, pois o jogo não está antecipando nenhuma entrada do controlador.
WaitForInput O controlador está aguardando que o player confirme uma mensagem do jogo utilizando um clique com o botão esquerdo do mouse, um evento de toque ou o botão de menu em um gamepad.
Com atividade O controlador está no modo de jogo ativo.

Estado WaitForInput e pausa do jogo

O jogo insere o estado WaitForInput quando o jogo tiver sido pausado. Isso acontece quando o player move o ponteiro para fora da janela principal do jogo ou pressiona o botão de pausa (a tecla P ou o botão Iniciar do gamepad). O MoveLookController registra o pressionamento e informa o loop do jogo quando chama o método IsPauseRequested. Nesse ponto, se IsPauseRequested retornar verdadeiro, o loop do jogo chamará WaitForPress na MoveLookController para mover o controlador para o estado WaitForInput.

Uma vez no estado WaitForInput, o jogo para de processar quase todos os eventos de entrada do jogo até retornar ao estado Ativo. A exceção é o botão de pausa, que, ao ser pressionado, faz com que o jogo volte ao estado ativo. Além do botão de pausa, para que o jogo volte ao estado Ativo, o player precisa selecionar um item de menu.

O estado Ativo

Durante o estado Ativo, a instância MoveLookController está processando eventos de todos os dispositivos de entrada habilitados e interpretando as intenções do player. Como resultado, ele atualiza a velocidade e a direção da visão do player e compartilha os dados atualizados com o jogo após Atualizar ser chamado no loop do jogo.

Todas as entradas de ponteiro são rastreadas no estado Ativo, com IDs de ponteiro diferentes correspondendo a ações de ponteiro diferentes. Quando um evento PointerPressed é recebido, o MoveLookController obtém o valor da ID do ponteiro criado pela janela. A ID do ponteiro representa um tipo específico de entrada. Por exemplo, em um dispositivo multitoques, podem existir várias entradas ativas diferentes ao mesmo tempo. As IDs são usadas para acompanhar a entrada que o player está utilizando. Se um evento estiver no retângulo de movimentação da tela de entrada por toque, uma ID de ponteiro será atribuída para acompanhar todos os eventos de ponteiro no retângulo de movimentação. Outros eventos de ponteiro na área do retângulo de disparo são acompanhados separadamente, com uma ID de ponteiro separada.

Observação

A entrada do mouse e do thumbstick direito de um gamepad também tem IDs que são tratadas separadamente.

Após os eventos de ponteiro terem sido mapeados para uma ação específica do jogo, é hora de atualizar os dados que o objeto MoveLookController compartilha com o loop principal do jogo.

Quando chamado, o método Atualizar no jogo de exemplo processa a entrada e atualiza as variáveis de velocidade e direção da visão (m_velocity e m_lookdirection), que o loop do jogo recupera chamando os métodos públicos Velocity e LookDirection.

Observação

Mais detalhes sobre o método Atualizar podem ser vistos mais adiante nesta página.

O loop do jogo pode testar para ver se o player está disparando chamando o método IsFiring na instância MoveLookController. O MoveLookController verifica se o player pressionou o botão de disparo em um dos três 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;
}

Agora, vamos observar a implementação de cada um dos três tipos de controle com um pouco mais de detalhes.

Adição de controles relativos do mouse

Se o movimento do mouse for detectado, desejamos utilizar esse movimento para determinar a nova rotação sobre o eixo x e a rotação sobre o eixo y da câmera. Fazemos isso implementando controles relativos do mouse, em que tratamos a distância relativa que o mouse se moveu, o delta entre o início do movimento e a parada, em vez de registrar as coordenadas absolutas de pixel x-y do movimento.

Para fazer isso, obtemos as alterações nas coordenadas X (o movimento horizontal) e Y (o movimento vertical) examinando os campos MouseDelta::X e MouseDelta::Y no objeto de argumento Windows::Device::Input::MouseEventArgs::MouseDelta retornado pelo 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;
    }
}

Adição de suporte a entrada por toque

Os controles de entrada por toque são ótimos para dar suporte a usuários com tablets. Esse jogo coleta a entrada por toque ao zonear determinadas áreas da tela, cada uma delas alinhada a ações específicas no jogo. A entrada por toque desse jogo utiliza três zonas.

move look touch layout

Os comandos a seguir resumem o comportamento do nosso controle de entrada por toque. Entrada do usuário | Ação :------- | :-------- Mover retângulo | A entrada por toque é convertida em um joystick virtual, em que o movimento vertical será traduzido em movimento de posição para frente/para trás e o movimento horizontal será traduzido em movimento de posição esquerda/direita. Retângulo de disparo | Disparar uma esfera. Toque fora do retângulo de movimento e disparo | Altere a rotação (a rotação sobre o eixo x e a rotação sobre o eixo y) da exibição da câmera.

O MoveLookController verifica a ID do ponteiro para determinar em que ponto o evento ocorreu e executa uma das seguintes ações:

  • Se o evento PointerMoved tiver ocorrido no retângulo de movimentação ou de disparo, atualize a posição do ponteiro do controlador.
  • Se o evento PointerMoved ocorreu em algum lugar no resto da tela (definido como os controles de visão), calcule a alteração na rotação sobre o eixo x e na rotação sobre o eixo y do vetor de direção da aparência.

Uma vez que implementamos nossos controles de toque, os retângulos que desenhamos anteriormente usando Direct2D indicarão aos players onde estão as zonas de movimento, disparo e visualização.

touch controls

Agora vamos dar uma olhada em como implementamos cada controle.

Controlador de movimento e disparo

O retângulo do controlador de movimento no quadrante inferior esquerdo da tela é usado como um direcional. Deslizar o polegar para a esquerda e para a direita dentro desse espaço move o player para a esquerda e para a direita, enquanto para cima e para baixo move a câmera para frente e para trás. Após configurar isso, tocar no controlador de disparo no quadrante inferior direito da tela disparará uma esfera.

Os métodos SetMoveRect e SetFireRect criam nossos retângulos de entrada, utilizando dois vetores 2D para especificar as posições dos cantos superior esquerdo e inferior direito de cada retângulo na tela.

Os parâmetros são atribuídos a m_fireUpperLeft e m_fireLowerRight, que nos ajudarão a determinar se o usuário está tocando em um dos retângulos.

m_fireUpperLeft = upperLeft;
m_fireLowerRight = lowerRight;

Se a tela for redimensionada, esses retângulos serão redesenhados para o tamanho apropriado.

Agora que zoneamos nossos controles, é hora de determinar quando um usuário está realmente utilizando-os. Para isso, configuramos alguns manipuladores de eventos no método MoveLookController::InitWindow para quando o usuário pressionar, mover ou liberar o ponteiro.

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

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

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

Primeiro, determinaremos o que acontece quando o usuário pressiona pela primeira vez dentro dos retângulos mover ou disparar, utilizando o método OnPointerPressed. Aqui verificamos em que ponto eles estão tocando um controle e se um ponteiro já está nesse controlador. Se esse for o primeiro dedo a tocar o controle específico, faremos o seguinte.

  • Armazene a localização do toque inicial em m_moveFirstDown ou m_fireFirstDown como um vetor 2D.
  • Atribua a ID do ponteiro a m_movePointerID ou m_firePointerID.
  • Defina o sinalizador InUse adequado (m_moveInUse ou m_fireInUse) como true, pois agora temos um ponteiro ativo para esse controle.
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;
                ...
            }
        }
        ...

Agora que determinamos se o usuário está tocando em um controle de movimento ou de disparo, veremos se o player está fazendo algum movimento com o dedo pressionado. Utilizando o método MoveLookController::OnPointerMoved, verificamos qual ponteiro se moveu e, em seguida, armazenamos sua nova posição como um vetor 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;
    }
    ...

Uma vez que o usuário tenha feito seus gestos dentro dos controles, ele liberará o ponteiro. Utilizando o método MoveLookController::OnPointerReleased, determinamos qual ponteiro foi liberado e fazemos uma série de redefinições.

Se o controle de movimento tiver sido liberado, faremos o seguinte.

  • Defina a velocidade do player como 0 em todas as direções para evitar que ele se mova no jogo.
  • Alterne m_moveInUse para false, pois o usuário não está mais tocando o controlador de movimento.
  • Defina a ID do ponteiro de movimentação como 0, pois não existe mais um ponteiro no controlador de movimentação.
if (pointerID == m_movePointerID)
{
    // Stop on release.
    m_velocity = XMFLOAT3(0, 0, 0);
    m_moveInUse = false;
    m_movePointerID = 0;
}

Para o controle de disparo, se ele tiver sido liberado, tudo o que faremos é alternar o sinalizador m_fireInUse para false e a ID do ponteiro de disparo para 0, já que não existe mais um ponteiro no controle de disparo.

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

Controlador de visão

Tratamos os eventos do ponteiro do dispositivo de toque para as regiões não utilizadas da tela como o controlador de visão. Deslizar o dedo em torno dessa zona altera a inclinação e a guinada (rotação) da câmera do player.

Se o evento MoveLookController::OnPointerPressed for acionado em um dispositivo de toque nessa região e o estado do jogo estiver definido como Ativado, deverá ser atribuído a ele uma ID de ponteiro.

// 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;
}

Aqui, o MoveLookController atribui a ID do ponteiro que disparou o evento para uma variável específica que corresponde à região de observação. No caso de um toque que ocorra na região de observação, a variável m_lookPointerID está definida como a ID do ponteiro que disparou o evento. Uma variável booliana, m_lookInUse, também está definida como indicando que o controle ainda não foi liberado.

Agora, vejamos como o jogo de exemplo trata o evento PointerMoved da tela de entrada por toque.

No método MoveLookController::OnPointerMoved, verificamos que tipo de ID de ponteiro foi atribuído ao evento. Se for m_lookPointerID, calcularemos a alteração na posição do ponteiro. Em seguida, usamos esse delta para calcular o quanto a rotação deve ser alterada. Finalmente, chegamos a um ponto em que podemos atualizar o m_pitch e m_yaw para serem utilizados no jogo para alterar a rotação do player.

// 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);
    ...
}

A última parte que vamos observar é como o jogo de exemplo trata o evento PointerReleased da tela de entrada por toque. Uma vez que o usuário tenha concluído o gesto de toque e removido o dedo da tela, MoveLookController::OnPointerReleased é iniciado. Se a ID do ponteiro que disparou o evento PointerReleased for a ID do ponteiro de movimento previamente registrado, o MoveLookController definirá a velocidade como 0 porque o player parou de tocar na área de visualização.

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

Adição do suporte a mouse e teclado

Este jogo tem o seguinte layout de controle para teclado e mouse.

Entrada do usuário Ação
W Mover o player para frente
A Mover o player para a esquerda
S Mover o player para trás
D Mover o player para a direita
X Mover a exibição para cima
Barra de espaço Mover a exibição para baixo
P Pausar o jogo
Movimento do mouse Altere a rotação (a inclinação e a guinada) da exibição da câmera
Botão esquerdo do mouse Disparar uma esfera

Para utilizar o teclado, o jogo de exemplo registra dois novos eventos, CoreWindow::KeyUp e CoreWindow::KeyDown, dentro do método MoveLookController::InitWindow. Esses eventos tratam da pressão e da versão de uma tecla.

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

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

O mouse é tratado de forma um pouco diferente dos controles de toque, embora utilize um ponteiro. Para alinhar-se ao nosso layout de controle, o MoveLookController gira a câmera sempre que o mouse é movido e dispara quando o botão esquerdo do mouse é pressionado.

Esse tratamento é feito no método OnPointerPressed do MoveLookController.

Nesse método, verificamos que tipo de dispositivo de ponteiro está sendo usado com a enumeração Windows::Devices::Input::PointerDeviceType. Se o jogo estiver Ativo e o PointerDeviceType não for Toque, presumiremos que seja uma entrada de 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;

Quando o player deixa de pressionar um dos botões do mouse, o evento de mouse CoreWindow::PointerReleased é acionado, chamando o método MoveLookController::OnPointerReleased, e a entrada é concluída. Naquele momento, as esferas pararão de disparar se o botão esquerdo do mouse estiver pressionado e agora for liberado. Como a aparência está sempre habilitada, o jogo continua a utilizar o mesmo ponteiro do mouse para acompanhar os eventos de aparência em andamento.

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;

Agora vamos dar uma olhada no último tipo de controle que daremos suporte: gamepads. Os gamepads são tratados separadamente dos controles de toque e mouse, pois não utilizam o objeto ponteiro. Por isso, alguns métodos e manipuladores de eventos novos precisarão ser adicionados.

Adicionar suporte para gamepad

Para este jogo, o suporte ao gamepad é adicionado por meio de chamadas para as APIs Windows.Gaming.Input. Esse conjunto de APIs fornece acesso a entradas de controladores de jogos, como volantes de corrida e manches de voo.

A seguir estarão os controles do nosso gamepad.

Entrada do usuário Ação
Direcional analógico esquerdo Mover o player
Direcional analógico direito Altere a rotação (a inclinação e a guinada) da exibição da câmera
Gatilho direito Disparar uma esfera
Botão Iniciar/Menu Pausar ou retomar o jogo

No método InitWindow, adicionamos dois novos eventos para determinar se um gamepad foi adicionado ou removido. Esses eventos atualizam a propriedade m_gamepadsChanged. Isso é usado no método UpdatePollingDevices para verificar se a lista de gamepads conhecidos foi alterada.

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

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

Observação

Os aplicativos UWP não podem receber a entrada de um controlador de jogo enquanto o aplicativo não estiver em foco.

O método UpdatePollingDevices

O método UpdatePollingDevices da instância MoveLookController verifica imediatamente se um gamepad está conectado. Se for o caso, começaremos a ler seu estado com Gamepad.GetCurrentReading. Isso retorna a estrutura GamepadReading, o que nos permite verificar quais botões foram clicados ou quais thumbsticks foram movidos.

Se o estado do jogo for WaitForInput, só ouviremos o botão Iniciar/Menu do controlador para que o jogo possa ser retomado.

Se estiver Ativo, verificaremos a entrada do usuário e determinaremos qual ação no jogo precisa acontecer. Por exemplo, se o usuário moveu o direcional analógico esquerdo em uma direção específica, isso permite que o jogo saiba que precisamos mover o player na direção em que o analógico está sendo movido. O movimento do stick em uma direção específica deve ser registrado como maior que o raio da zona morta; caso contrário, nada acontecerá. Esse raio de zona morta é necessário para evitar o "descompasso," que ocorre quando o controlador capta pequenos movimentos do polegar do player na medida em que ele repousa no stick. Sem zonas mortas, os controladores podem parecer muito sensíveis para o usuário.

A entrada do Thumbstick está entre -1 e 1 para os eixos x e y. A seguinte constante especifica o raio da zona morta do thumbstick.

#define THUMBSTICK_DEADZONE 0.25f

Utilizando essa variável, começaremos a processar a entrada acionável do thumbstick. O movimento ocorreria com um valor de [-1, -,26] ou [,26, 1] em qualquer eixo.

dead zone for thumbsticks

Essa parte do método UpdatePollingDevices lida com os thumbsticks esquerdo e direito. Os valores X e Y de cada stick são verificados para ver se estão fora da zona morta. Se um ou ambos forem, faremos a atualização do componente correspondente. Por exemplo, se o thumbstick esquerdo estiver sendo movido para a esquerda ao longo do eixo X, adicionaremos -1 ao componente x do vetor m_moveCommand. Esse vetor é o que será usado para a agregação de todos os movimentos em todos os dispositivos e, posteriormente, será utilizado para calcular em que posição o player deve se mover.

// 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;
}

De modo semelhante ao que o stick esquerdo controla o movimento, o stick direito controla a rotação da câmera.

O comportamento do stick do polegar direito se alinha com o comportamento do movimento do mouse em nossa configuração de controle de teclado e mouse. Se o stick estiver fora da zona morta, calcularemos a diferença entre a posição atual do ponteiro e o local em que o usuário está tentando olhar. Essa alteração na posição do ponteiro (pointerDelta) é usada para atualizar a inclinação e a guinada da rotação da câmera que, posteriormente, são aplicadas em nosso método Atualizar. O vetor pointerDelta pode parecer familiar porque também é usado no método MoveLookController::OnPointerMoved para acompanhar a alteração na posição do ponteiro para nossas entradas de mouse e toque.

// 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);

Os controles do jogo não estariam concluídos sem a capacidade de disparar esferas!

Esse método UpdatePollingDevices também verifica se o gatilho correto está sendo pressionado. Se for, nossa propriedade m_firePressed é alterada para verdadeiro, sinalizando ao jogo que as esferas devem começar a disparar.

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

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

O método Atualização

Para encerrar, vamos nos aprofundar no método Atualizar. Esse método mescla todos os movimentos ou rotações que o player fez com qualquer entrada com suporte para gerar um vetor de velocidade e atualizar nossos valores de rotação sobre o eixo x e rotação sobre o eixo y para o nosso loop de jogo acessar.

O método Atualizar inicia o processo chamando UpdatePollingDevices para atualizar o estado do controlador. Esse método também coleta qualquer entrada de um gamepad e adiciona seus movimentos ao vetor m_moveCommand.

Em nosso método Atualizar, realizamos as seguintes verificações de entrada.

  • Se o player estiver utilizando o retângulo do controlador de movimento, determinaremos a alteração na posição do ponteiro e a usaremos para calcular se o usuário moveu o ponteiro para fora da zona morta do controlador. Se estiver fora da zona morta, a propriedade vetorial m_moveCommand será atualizada com o valor do joystick virtual.
  • Se qualquer uma das entradas de movimento do teclado for pressionada, um valor de 1.0f ou -1.0f será adicionado ao componente correspondente do vetor m_moveCommand - 1.0f para frente e -1.0f para a versão anterior.

Uma vez que toda a entrada de movimento tenha sido levada em conta, executamos o vetor m_moveCommand por meio de alguns cálculos para gerar um novo vetor que representa a direção do player em relação ao mundo do jogo. Em seguida, pegamos nossos movimentos em relação ao mundo e os aplicamos ao player como velocidade nessa direção. Em seguida, redefinimos o vetor m_moveCommand para (0.0f, 0.0f, 0.0f) para que tudo esteja pronto para o próximo quadro do jogo.

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);
}

Próximas etapas

Agora que adicionamos nossos controles, existe outro recurso que precisamos adicionar para criar um jogo imersivo: o som! A música e os efeitos sonoros são importantes para qualquer jogo, portanto, vamos discutir sobre adicionar som em seguida.