Bloqueio do ponteiro e controles de tiro em primeira pessoa

John McCutchan
John McCutchan

Introdução

A API Pointer Lock ajuda a implementar corretamente controles de tiro em primeira pessoa em um jogo de navegador. Sem o movimento relativo do mouse, o cursor do jogador poderia, por exemplo, atingir a borda direita da tela e quaisquer outros movimentos para a direita seriam desconsiderados. A visão não continuaria se movendo para a direita, e o jogador não poderia perseguir os vilões e usá-los como metralhadoras. O jogador ficará frágil e frustrado. Com o bloqueio do ponteiro, esse comportamento abaixo do ideal não pode acontecer.

A API Pointer Lock permite que seu aplicativo faça o seguinte:

  • Tenha acesso a dados brutos do mouse, incluindo os movimentos relativos do mouse
  • Rotear todos os eventos do mouse para um elemento específico

Como efeito colateral da ativação do bloqueio de ponteiro, o cursor do mouse fica oculto, permitindo que você desenhe um ponteiro específico do aplicativo, se quiser, ou deixe o ponteiro do mouse oculto para que o usuário possa mover o frame com o mouse. O movimento relativo do mouse é o delta da posição do ponteiro do mouse em relação ao frame anterior, independentemente da posição absoluta. Por exemplo, se o ponteiro do mouse foi movido de (640, 480) para (520, 490), o movimento relativo foi (-120, 10). Veja abaixo um exemplo interativo que mostra deltas brutos de posição do mouse.

Este tutorial abrange dois tópicos: os princípios básicos da ativação e do processamento de eventos de bloqueio de ponteiro e a implementação do esquema de controle de tiro em primeira pessoa. Isso mesmo, quando terminar de ler este artigo, você saberá como usar o bloqueio de ponteiro e implementar controles no estilo Quake para seu próprio jogo de navegador.

Compatibilidade com navegadores

Compatibilidade com navegadores

  • Chrome: 37.
  • Borda: 13.
  • Firefox: 50.
  • Safari: 10.1.

Origem

Mecânica de bloqueio do ponteiro

Detecção de recursos

Para determinar se o navegador do usuário é compatível com o bloqueio de ponteiro, verifique se há pointerLockElement ou uma versão com o prefixo do fornecedor no objeto do documento. No código:

var havePointerLock = 'pointerLockElement' in document ||
    'mozPointerLockElement' in document ||
    'webkitPointerLockElement' in document;

No momento, o bloqueio de ponteiro está disponível apenas no Firefox e no Chrome. O Opera e o IE ainda não são compatíveis com ela.

Ativando

A ativação do bloqueio do ponteiro é um processo de duas etapas. Primeiro, o aplicativo solicita que o bloqueio do ponteiro seja ativado para um elemento específico e, logo após o usuário conceder a permissão, um evento pointerlockchange é disparado. O usuário pode cancelar o bloqueio do ponteiro a qualquer momento pressionando a tecla Esc. Seu aplicativo também pode sair de forma proativa do bloqueio do ponteiro. Quando o bloqueio do ponteiro é cancelado, um evento pointerlockchange é disparado.

element.requestPointerLock = element.requestPointerLock ||
                 element.mozRequestPointerLock ||
                 element.webkitRequestPointerLock;
// Ask the browser to lock the pointer
element.requestPointerLock();

// Ask the browser to release the pointer
document.exitPointerLock = document.exitPointerLock ||
               document.mozExitPointerLock ||
               document.webkitExitPointerLock;
document.exitPointerLock();

Basta usar o código acima. Quando o navegador bloqueia o ponteiro, um balão aparece informando ao usuário que o aplicativo bloqueou o ponteiro e informando que ele pode cancelá-lo pressionando a tecla "Esc" de dados.

Barra de informações do Pointer Lock no Chrome.
Barra de informações do bloqueio de ponteiro no Chrome.

Tratamento de eventos

Há dois eventos para os quais seu aplicativo precisa adicionar listeners. O primeiro é pointerlockchange, que é disparado sempre que ocorre uma mudança no estado de bloqueio do ponteiro. O segundo é mousemove, que é acionado sempre que o mouse se move.

// Hook pointer lock state change events
document.addEventListener('pointerlockchange', changeCallback, false);
document.addEventListener('mozpointerlockchange', changeCallback, false);
document.addEventListener('webkitpointerlockchange', changeCallback, false);

// Hook mouse move events
document.addEventListener("mousemove", this.moveCallback, false);

No callback pointerlockchange, é necessário verificar se o ponteiro acabou de ser bloqueado ou desbloqueado. Determinar se o bloqueio de ponteiro foi ativado é simples: verifique se document.pointerLockElement é igual ao elemento para o qual o bloqueio de ponteiro foi solicitado. Se estiver, o aplicativo bloqueou o ponteiro e, se não estiver, ele foi desbloqueado pelo usuário ou pelo seu próprio código.

if (document.pointerLockElement === requestedElement ||
  document.mozPointerLockElement === requestedElement ||
  document.webkitPointerLockElement === requestedElement) {
  // Pointer was just locked
  // Enable the mousemove listener
  document.addEventListener("mousemove", this.moveCallback, false);
} else {
  // Pointer was just unlocked
  // Disable the mousemove listener
  document.removeEventListener("mousemove", this.moveCallback, false);
  this.unlockHook(this.element);
}

Quando o bloqueio de ponteiro está ativado, clientX, clientY, screenX e screenY permanecem constantes. movementX e movementY são atualizados com o número de pixels que o ponteiro teria movido desde que o último evento foi enviado. Em pseudocódigo:

event.movementX = currentCursorPositionX - previousCursorPositionX;
event.movementY = currentCursorPositionY - previousCursorPositionY;

Dentro do callback mousemove, é possível extrair dados de movimento relativo do mouse dos campos movementX e movementY do evento.

function moveCallback(e) {
  var movementX = e.movementX ||
      e.mozMovementX          ||
      e.webkitMovementX       ||
      0,
  movementY = e.movementY ||
      e.mozMovementY      ||
      e.webkitMovementY   ||
      0;
}

Detecção de erros

Se um erro for gerado ao entrar ou sair do bloqueio de ponteiro, o evento pointerlockerror será disparado. Não há dados anexados a este evento.

document.addEventListener('pointerlockerror', errorCallback, false);
document.addEventListener('mozpointerlockerror', errorCallback, false);
document.addEventListener('webkitpointerlockerror', errorCallback, false);

O modo de tela cheia é obrigatório?

Originalmente, o bloqueio de ponteiro estava vinculado à API FullScreen. Isso significa que um elemento precisa estar no modo de tela cheia antes de bloquear o ponteiro. Isso não é mais verdadeiro, e o bloqueio de ponteiro pode ser usado para qualquer elemento no aplicativo em tela cheia ou não.

Exemplo de controles para tiro em primeira pessoa

Agora que o bloqueio de ponteiro está ativado e recebendo eventos, é hora de um exemplo prático. Você já quis saber como funcionam os controles do Quake? Aperte o cinto porque vou explicar tudo com código!

Os controles de tiro em primeira pessoa têm quatro mecânicas básicas:

  • Como avançar e voltar no vetor de visualização atual
  • Movendo para a esquerda e para a direita ao longo do vetor de estratificação atual
  • Girar a guinada da visualização (para a esquerda e para a direita)
  • Girar a inclinação da visualização (para cima e para baixo)

Um jogo que implementa esse esquema de controle precisa de apenas três dados: a posição da câmera, o vetor de aparência da câmera e um vetor de aumento constante. O vetor superior é sempre (0, 1, 0). As quatro mecânicas acima manipulam a posição e o vetor de aparência da câmera de maneiras diferentes.

Movimento

O primeiro ponto é o movimento. Na demonstração abaixo, o movimento é mapeado para as teclas padrão W, A, S e D. As teclas W e S levam a câmera para frente e para trás. Enquanto as teclas A e D movem a câmera para a esquerda e para a direita. É simples mover a câmera para frente e para trás:

// Forward direction
var forwardDirection = vec3.create(cameraLookVector);
// Speed
var forwardSpeed = dt * cameraSpeed;
// Forward or backward depending on keys held
var forwardScale = 0.0;
forwardScale += keyState.W ? 1.0 : 0.0;
forwardScale -= keyState.S ? 1.0 : 0.0;
// Scale movement
vec3.scale(forwardDirection, forwardScale * forwardSpeed);
// Add scaled movement to camera position
vec3.add(cameraPosition, forwardDirection);

O estiramento à esquerda e à direita exige uma direção de alongamento. A direção do estrafe pode ser calculada usando vários produtos:

// Strafe direction
var strafeDirection = vec3.create();
vec3.cross(cameraLookVector, cameraUpVector, strafeDirection);

Depois de definir a direção de estrangulamento, implementar o movimento de estrafe é o mesmo que avançar ou retroceder.

A próxima etapa é girar a visualização.

Yaw

A curva ou rotação horizontal da visualização da câmera é apenas uma rotação em torno do vetor superior constante. Veja abaixo um código geral para girar o vetor de aparência da câmera em torno de um eixo arbitrário. Ela cria um quatérnio que representa a rotação de deltaAngle radianos em torno de axis e usa o quatérnio para girar o vetor de aparência da câmera:

// Extract camera look vector
var frontDirection = vec3.create();
vec3.subtract(this.lookAtPoint, this.eyePoint, frontDirection);
vec3.normalize(frontDirection);
var q = quat4.create();
// Construct quaternion
quat4.fromAngleAxis(deltaAngle, axis, q);
// Rotate camera look vector
quat4.multiplyVec3(q, frontDirection);
// Update camera look vector
this.lookAtPoint = vec3.create(this.eyePoint);
vec3.add(this.lookAtPoint, frontDirection);

Apresentar

A implementação da inclinação ou da rotação vertical da visualização da câmera é semelhante, mas em vez de uma rotação em torno do vetor para cima, você aplica uma rotação em torno do vetor de largura. A primeira etapa é calcular o vetor de estratifa e, em seguida, girar o vetor de aparência da câmera em torno desse eixo.

Resumo

A API Pointer Lock permite controlar o cursor do mouse. Se você cria jogos na Web, seus jogadores vão adorar quando pararem de ser perfumados, porque tiraram o mouse da janela com entusiasmo e o jogo parou de receber atualizações. O uso é simples:

  • Adicionar o listener de eventos pointerlockchange para rastrear o estado do bloqueio do ponteiro
  • Solicitar o bloqueio de ponteiro para um elemento específico
  • Adicione o listener de eventos mousemove para receber atualizações

Demonstrações externas

Referências