Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Observação
Este tópico faz parte da série de tutoriais Criar um jogo simples da Plataforma Universal do Windows (UWP) com DirectX. O tópico nesse link define o contexto da série.
[ Atualizado para aplicativos UWP no Windows 10. Para artigos do Windows 8.x, consulte o arquivo ]
Um bom jogo da Plataforma Universal do Windows (UWP) suporta uma grande variedade de interfaces. Um jogador em potencial pode ter o Windows 10 em um tablet sem botões físicos, um PC com um controlador de jogo conectado ou a mais recente plataforma de jogos de desktop com mouse e teclado para jogos de alto desempenho. No nosso jogo, os controlos 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 usa controles de movimento padrão de gênero que funcionam com vários dispositivos.
Observação
Para saber mais sobre controles, veja controles de movimento e visualização para jogos e controles de toque para jogos.
Objetivo
Neste momento temos um jogo que renderiza, mas não podemos mover o nosso jogador ou atirar nos alvos. Vamos dar uma olhada em como nosso jogo implementa controles de movimento de tiro em primeira pessoa para os seguintes tipos de entrada em nosso jogo UWP DirectX.
- Mouse e teclado
- Toque
- Gamepad
Observação
Se ainda não tiver transferido o código mais recente do jogo para este exemplo, aceda ao jogo de exemplo Direct3D . Este exemplo faz parte de uma grande coleção de exemplos de recursos UWP. Para obter instruções sobre como descarregar o exemplo, consulte Aplicações de exemplo para desenvolvimento Windows.
Comportamentos de controlo comuns
Os controles de toque e os controles de mouse/teclado têm uma implementação central muito semelhante. Em um aplicativo UWP, um ponteiro é simplesmente um ponto na tela. Pode movê-lo deslizando o rato ou deslizando o dedo no ecrã táctil. Como resultado, você pode se registrar para um único conjunto de eventos, e não se preocupar se o jogador está usando um mouse ou uma tela sensível ao toque para mover e pressionar o ponteiro.
Quando a classe MoveLookController no jogo de exemplo é inicializada, ela registra quatro eventos específicos do ponteiro e um evento específico do mouse:
| Evento | Descrição |
|---|---|
| CoreWindow::P ointerPressed | O botão esquerdo ou direito do mouse foi pressionado (e mantido), ou a superfície de toque foi tocada. |
| CoreWindow::P ointerMoved | O mouse se moveu ou uma ação de arrastar foi feita na superfície de toque. |
| CoreWindow::P ointerReleased | O botão esquerdo do mouse foi liberado ou o objeto que entrou em contato com a superfície de toque foi levantado. |
| CoreWindow::P ointerExited | O ponteiro saiu da janela principal. |
| Windows::D evices::Input::MouseMoved | O rato moveu-se 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 são definidos para começar a escutar a entrada do usuário assim que o MoveLookController for 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 para InitWindow pode ser visto no GitHub.
Para determinar quando o jogo deve escutar determinadas entradas, a classe MoveLookController tem três estados específicos do controlador, independentemente do tipo de controlador:
| Estado | Descrição |
|---|---|
| Nenhum | Este é o estado inicializado para o controlador. Todas as entradas são ignoradas, uma vez que o jogo não está a antecipar qualquer entrada do controlador. |
| AguardarEntrada | O controle está esperando que o jogador reconheça uma mensagem do jogo usando um clique esquerdo do mouse, um evento de toque, ou o botão de menu em um gamepad. |
| Activo | O controle está no modo de jogo ativo. |
Estado "WaitForInput" e pausa do jogo
O jogo entra no estado WaitForInput quando foi pausado. Isso acontece quando o jogador 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 a imprensa e informa o loop do jogo quando ele chama o o método IsPauseRequested. Nesse ponto, se IsPauseRequested retornar verdadeiro, o loop do jogo chamará WaitForPress no MoveLookController para mover o controlador para o estado WaitForInput.
Uma vez no estado WaitForInput, o jogo para de processar quase todos os eventos de input do jogo até retornar ao estado Ativo. A exceção é o botão de pausa, com um toque fazendo com que o jogo volte ao estado ativo. Além do botão de pausa, para que o jogo volte ao estado Ativo , o jogador precisa selecionar um item de menu.
O estado Ativo
Durante o estado Ativo, a instância do MoveLookController está processando eventos de todos os dispositivos de entrada habilitados e interpretando as intenções do jogador. Como resultado, atualiza a velocidade e a direção da visão do jogador e compartilha os dados atualizados com o jogo depois que a Atualização é chamada a partir do loop do jogo.
Toda a entrada de ponteiro é rastreada no estado Ativo, com diferentes IDs de ponteiro correspondentes a diferentes ações de ponteiro. Quando um evento PointerPressed é recebido, o MoveLookController obtém o valor de ID de ponteiro criado pela janela. O ID do ponteiro representa um tipo específico de entrada. Por exemplo, em um dispositivo multitoque, pode haver várias entradas ativas diferentes ao mesmo tempo. Os IDs são usados para manter o controle de qual entrada o jogador está usando. Se um evento estiver no retângulo de movimento da tela sensível ao toque, um ID de ponteiro será atribuído para rastrear quaisquer eventos de ponteiro no retângulo de movimento. Outros eventos de ponteiro no retângulo de incêndio são rastreados separadamente, com um ID de ponteiro separado.
Observação
A entrada do mouse e o polegar direito de um gamepad também têm IDs que são manipulados separadamente.
Depois que os eventos de ponteiro tiverem sido mapeados para uma ação de jogo específica, é hora de atualizar os dados que o objeto MoveLookController compartilha com o loop principal do jogo.
Quando chamado, o método Update no jogo de exemplo processa a entrada e atualiza as variáveis de velocidade e direção de visão (m_velocity e m_lookdirection). O loop do jogo então recupera essas variáveis chamando os métodos públicos Velocity e LookDirection.
Observação
Mais detalhes sobre o método Update podem ser vistos mais adiante nesta página.
O loop do jogo pode verificar se o jogador está a disparar chamando o método IsFiring na instância MoveLookController. O MoveLookController verifica se o jogador pressionou o botão de fogo 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 examinar a implementação de cada um dos três tipos de controle com um pouco mais de detalhes.
Adicionando controles relativos ao mouse
Se for detetado movimento do rato, queremos usar esse movimento para determinar a nova inclinação e guinada da câmara. Fazemos isso implementando controles relativos do mouse, onde lidamos com a distância relativa que o mouse 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 o MouseDelta::X e campos MouseDelta::Y no Windows::D evice::Input::MouseEventArgs::MouseDelta objeto de argumento retornado pelo MouseMoved evento.
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;
}
}
Adicionar suporte de toque
Os controles de toque são ótimos para suportar usuários com tablets. Este jogo reúne a entrada de toque, delimitando certas áreas do ecrã onde cada uma se alinha com ações específicas dentro do jogo. A entrada por toque deste jogo usa três zonas.
Os comandos a seguir resumem nosso comportamento de controle de toque. Entrada do utilizador | Ação :------- | :-------- Mover o retângulo | A entrada por toque é convertida em um joystick virtual onde o movimento vertical será traduzido em movimento na posição para a frente/para trás e o movimento horizontal será traduzido em movimento na posição esquerda/direita. Disparar um retângulo | Disparar uma esfera. Toque fora do retângulo de movimento e disparo | Altere a rotação (a inclinação e guinada) da visão da câmera.
O MoveLookController verifica o ID do ponteiro para determinar onde o evento ocorreu e executa uma das seguintes ações:
- Se o evento PointerMoved ocorrer no retângulo de movimento ou de disparo, atualize a posição do ponteiro no controlador.
- Se o evento PointerMoved ocorrer em qualquer parte do restante da tela (definido como os controlos de visão), calcule a mudança na inclinação e na guinada do vetor de visão.
Depois de implementarmos nossos controles de toque, os retângulos que desenhamos anteriormente usando Direct2D indicarão aos jogadores onde estão as zonas de movimento, fogo e aparência.
Agora vamos dar uma olhada em como implementamos cada controle.
Controlador de movimentação e incêndio
O retângulo do controlador de movimento no quadrante inferior esquerdo da tela é usado como um painel direcional. Deslizar o polegar para a esquerda e para a direita dentro deste espaço move o jogador para a esquerda e para a direita, enquanto para cima e para baixo move a câmara para a frente e para trás. Depois de configurar isso, tocar no controlador de incêndio no quadrante inferior direito da tela dispara uma esfera.
Os métodos SetMoveRect e SetFireRect criam os 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 então atribuídos a m_fireUpperLeft e m_fireLowerRight que nos ajudarão a determinar se o utilizador está a tocar dentro de um dos retângulos.
m_fireUpperLeft = upperLeft;
m_fireLowerRight = lowerRight;
Se a tela for redimensionada, esses retângulos são redesenhados para o tamanho apropriado.
Agora que delimitámos os controlos, é hora de determinar quando um utilizador está realmente a utilizá-los. Para fazer isso, configuramos alguns manipuladores de eventos no método MoveLookController::InitWindow para quando o usuário pressiona, move ou libera seu ponteiro.
window.PointerPressed({ this, &MoveLookController::OnPointerPressed });
window.PointerMoved({ this, &MoveLookController::OnPointerMoved });
window.PointerReleased({ this, &MoveLookController::OnPointerReleased });
Primeiro, determinaremos o que acontece quando o utilizador pressiona pela primeira vez dentro dos retângulos de movimento ou disparo usando o método OnPointerPressed. Aqui verificamos onde estão a tocar num controlador e se um ponteiro já está nesse controlador. Se este for o primeiro dedo a tocar no controle específico, fazemos o seguinte.
- Armazene a localização do touchdown em m_moveFirstDown ou m_fireFirstDown como um vetor 2D.
- Atribua o ID do ponteiro a m_movePointerID ou m_firePointerID.
- Defina o sinalizador InUse adequado (m_moveInUse ou m_fireInUse) para
true, já que 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 determinámos se o utilizador está a tocar nos controlos de movimento ou de disparo, verificamos se o jogador está a realizar algum movimento com o seu dedo pressionado. Usando o método MoveLookController::OnPointerMoved, verificamos qual ponteiro foi movido 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;
}
...
Depois que o usuário fizer seus gestos dentro dos controles, ele liberará o ponteiro. Usando o método MoveLookController::OnPointerReleased, determinamos qual ponteiro foi solto e fazemos uma série de redefinições.
Se o controle de movimentação tiver sido liberado, fazemos o seguinte.
- Defina a velocidade do jogador em
0todas as direções para impedi-lo de se mover no jogo. - Mude m_moveInUse para
falseuma vez que o utilizador já não está a tocar no controlador de movimento. - Defina o ID do ponteiro de movimento como
0uma vez que não há mais um ponteiro no controlador de movimento.
if (pointerID == m_movePointerID)
{
// Stop on release.
m_velocity = XMFLOAT3(0, 0, 0);
m_moveInUse = false;
m_movePointerID = 0;
}
Para o controle de incêndio, se ele foi liberado, tudo o que fazemos é mudar a bandeira m_fireInUse para false e o ID do ponteiro de fogo para 0, já que não há mais um ponteiro no controle de incêndio.
else if (pointerID == m_firePointerID)
{
m_fireInUse = false;
m_firePointerID = 0;
}
Controlador de visualização
Tratamos os eventos de ponteiro do dispositivo de toque para as regiões não utilizadas do ecrã como o controlador de visão. Deslizar o dedo em torno desta zona altera a inclinação e a guinada (rotação) da câmara do jogador.
Se o evento MoveLookController::OnPointerPressed for gerado num dispositivo de toque nesta região e o estado do jogo estiver definido como Activo, será atribuído um 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 o ID do ponteiro que disparou o evento a uma variável específica que corresponde à região de visualização. No caso de ocorrer um toque na região visual, a variável m_lookPointerID é atribuída ao ID do ponteiro que disparou o evento. Uma variável booleana, m_lookInUse, também é definida para indicar que o controle ainda não foi liberado.
Agora, vamos ver como o jogo de exemplo lida com o PointerMoved evento de tela sensível ao toque.
Dentro do método MoveLookController::OnPointerMoved, verificamos que tipo de ID de ponteiro foi atribuído ao evento. Se for m_lookPointerID, calculamos a mudança de posição do ponteiro. Em seguida, usamos esse delta para calcular o quanto a rotação deve mudar. Finalmente, estamos num ponto em que podemos atualizar o m_pitch e o m_yaw para serem usados no jogo para mudar a rotação do jogador.
// 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 veremos é como o jogo de exemplo lida com o evento de tela sensível ao toque PointerReleased.
Uma vez que o utilizador tenha terminado o gesto de toque e removido o dedo do ecrã, MoveLookController::OnPointerReleased é iniciado.
Se o ID do ponteiro que disparou o evento PointerReleased for o ID do ponteiro de movimento gravado anteriormente, o MoveLookController define a velocidade para 0 porque o jogador parou de tocar na área de visualização.
else if (pointerID == m_lookPointerID)
{
m_lookInUse = false;
m_lookPointerID = 0;
}
Adicionando suporte a mouse e teclado
Este jogo tem o seguinte layout de controle para teclado e mouse.
| Entrada do usuário | Ação |
|---|---|
| Q | Mover jogador para a frente |
| Um | Mover o jogador para a esquerda |
| S | Mover o jogador para trás |
| D | Mover o jogador para a direita |
| X | Mover a vista para cima |
| Barra de espaço | Mover a vista para baixo |
| P | Pausar o jogo |
| Movimento do rato | Alterar a rotação (o passo e bocejo) da visão da câmera |
| Botão esquerdo do rato | Lançar uma esfera |
Para usar o teclado, o jogo de exemplo registra dois novos eventos, CoreWindow::KeyUp e CoreWindow::KeyDown, dentro do método MoveLookController::InitWindow . Esses eventos lidam com o pressionar e soltar 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 use um ponteiro. Para alinhar com 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.
Isso é tratado no método OnPointerPressed do MoveLookController.
Neste método, verificamos que tipo de dispositivo de ponteiro está sendo usado com o Windows::Devices::Input::PointerDeviceType enum.
Se o jogo estiver Ative e o PointerDeviceType não estiver Touch, assumimos que é entrada do 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 jogador deixa de pressionar um dos botões do rato, é gerado o evento de mouse CoreWindow::PointerReleased, chamando o método MoveLookController::OnPointerReleased, e a entrada é concluída. Neste ponto, as esferas pararão de disparar se o botão esquerdo do mouse estiver sendo pressionado e agora estiver liberado. Como a visualização está sempre ativada, o jogo continua a usar o mesmo ponteiro para rastrear os eventos de visualização 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 ver o último tipo de controle que vamos suportar: gamepads. Os gamepads são manipulados separadamente dos controles de toque e mouse, pois não usam o objeto de ponteiro. Por isso, alguns novos manipuladores de eventos e métodos precisarão ser adicionados.
Adicionando suporte a gamepad
Para este jogo, o suporte ao gamepad é adicionado por chamadas para as APIs Windows.Gaming.Input . Este conjunto de APIs fornece acesso a entradas de controladores de jogos, como volantes de corrida e manípulos de voo.
A seguir estarão nossos controles de gamepad.
| Entrada do usuário | Ação |
|---|---|
| Manípulo analógico esquerdo | Mover jogador |
| Stick analógico direito | Alterar a rotação (o passo e bocejo) da visão da câmera |
| Gatilho direito | Lançar 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 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 o estado dele com Gamepad.GetCurrentReading. Isso retorna a estrutura GamepadReading, permitindo-nos verificar quais botões foram pressionados ou alavancas foram movidas.
Se o estado do jogo for WaitForInput, apenas escutamos o botão Iniciar/Menu do comando para que o jogo possa ser retomado.
Se for Ative, verificamos a entrada do usuário e determinamos qual ação no jogo precisa acontecer. Por exemplo, se o usuário moveu o stick analógico esquerdo em uma direção específica, isso permite que o jogo saiba que precisamos mover o jogador na direção em que o stick está sendo movido. O movimento do bastão numa direção específica deve registar-se como maior do que o raio da zona morta; caso contrário, nada acontecerá. Este raio de zona morta é necessário para evitar a "deriva", que é quando o controlador capta pequenos movimentos do polegar do jogador enquanto este se apoia no bastão. Sem zonas mortas, os controles 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 constante a seguir especifica o raio da zona morta do polegar.
#define THUMBSTICK_DEADZONE 0.25f
Usando 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 um dos eixos.
Esta parte do método UpdatePollingDevices do
// 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;
}
Semelhante a como o manípulo esquerdo controla o movimento, o manípulo direito controla a rotação da câmera.
O comportamento do polegar direito alinha-se com o comportamento do movimento do rato na nossa configuração de controlo do rato e teclado.
Se o stick estiver fora da zona morta, calculamos a diferença entre a posição atual do ponteiro e onde o usuário está tentando olhar.
Essa alteração na posição do ponteiro (
// 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 completos sem a capacidade de disparar esferas!
Este método UpdatePollingDevices também verifica se o gatilho correto está sendo pressionado. Se for, a nossa propriedade m_firePressed é alterada para true, indicando ao jogo que as esferas devem iniciar o disparo.
if (reading.RightTrigger > TRIGGER_DEADZONE)
{
if (!m_autoFire && !m_gamepadTriggerInUse)
{
m_firePressed = true;
}
m_gamepadTriggerInUse = true;
}
else
{
m_gamepadTriggerInUse = false;
}
O método Update
Para concluir, vamos aprofundar-nos mais no método Update. Este método mescla quaisquer movimentos ou rotações que o jogador fez com qualquer entrada suportada para gerar um vetor de velocidade e atualizar os valores de pitch e yaw para que possam ser acedidos pelo loop do jogo.
O método Update inicia as coisas chamando UpdatePollingDevices para atualizar o estado do controlador. Este método também reúne qualquer entrada de um gamepad e adiciona seus movimentos ao vetor m_moveCommand .
Em nosso método Update , executamos as seguintes verificações de entrada.
- Se o jogador estiver usando o retângulo do controlador de movimento, determinaremos a mudança na posição do ponteiro e usaremos isso para calcular se o usuário moveu o ponteiro para fora da zona morta do controle. Se estiver fora da zona morta, a propriedade do vetor m_moveCommand será atualizada com o valor do joystick virtual.
- Se qualquer uma das entradas do teclado de movimento for pressionada, um valor de
1.0fou-1.0fserá adicionado no componente correspondente do vetor m_moveCommand —1.0fpara frente e-1.0fpara trás.
Uma vez que toda a entrada de movimento tenha sido levada em conta, então executamos o vetor m_moveCommand através de alguns cálculos para gerar um novo vetor que representa a direção do jogador em relação ao mundo do jogo.
Em seguida, tomamos os nossos movimentos em relação ao mundo e aplicamo-los ao jogador, convertendo-os em velocidade nessa direção.
Finalmente, redefinimos o vetor m_moveCommand para (0.0f, 0.0f, 0.0f) 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óximos passos
Agora que adicionamos nossos controles, há outro recurso que precisamos adicionar para criar um jogo imersivo: som! A música e os efeitos sonoros são importantes para qualquer jogo, então vamos discutir a adição de som na próxima parte.