Bloqueio do ponteiro e controles de tiro em primeira pessoa

John mcCutchan
John McCutchan

Introdução

A API Pointer Lock ajuda a implementar corretamente os controles de tiro em primeira pessoa em um jogo para navegador. Sem o movimento relativo do mouse, o cursor do jogador poderia, por exemplo, atingir a borda direita da tela, e movimentos adicionais para a direita seriam descontados. A visualização não continuaria se movendo para a direita e o jogador não seria capaz de perseguir os criminosos nem tropeçar neles com sua metralhadora. O jogador vai ficar cansado e frustrado. Com o bloqueio de ponteiro, esse comportamento abaixo do ideal não pode acontecer.

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

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

Como efeito colateral de ativar o bloqueio do ponteiro, o cursor do mouse fica oculto, permitindo que você escolha desenhar 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 passar de (640, 480) para (520, 490), o movimento relativo será (-120, 10). Veja abaixo um exemplo interativo que mostra deltas brutos de posição do mouse.

Este tutorial aborda dois tópicos: os detalhes de ativar e processar 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ê vai saber como usar o bloqueio com ponteiro e implementar controles estilo Quake no seu próprio jogo de navegador.

Compatibilidade com navegadores

Compatibilidade com navegadores

  • 37
  • 13
  • 50
  • 10.1

Origem

Mecânica do Pointer Lock

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. Opera e IE ainda não são compatíveis com ele.

Ativando

A ativação do bloqueio de ponteiro é um processo de duas etapas. Primeiro, seu aplicativo solicita que o bloqueio de ponteiro para um elemento específico e, imediatamente após o usuário conceder a permissão, um evento pointerlockchange seja disparado. O usuário pode cancelar o bloqueio de ponteiro a qualquer momento pressionando a tecla Esc. O app também pode sair progrmaticamente do bloqueio do ponteiro. Quando o bloqueio de 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();

O código acima é tudo o que você precisa. Quando o navegador bloquear o ponteiro, um balão será exibido, informando ao usuário que seu aplicativo bloqueou o ponteiro e informando que é possível cancelá-lo pressionando a tecla "Esc".

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

Manipulação de eventos

Há dois eventos para os quais o aplicativo precisa adicionar listeners. A primeira é pointerlockchange, que é disparada sempre que ocorre uma mudança no estado de bloqueio do ponteiro. O segundo é mousemove, que é acionado sempre que o mouse é movido.

// 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, verifique 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 for, seu aplicativo bloqueou o ponteiro e, se não estiver, o ponteiro foi desbloqueado pelo usuário ou 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 entregue. Em pseudocódigo:

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

Dentro do callback mousemove, os dados relacionados ao movimento do mouse podem ser extraídos 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;
}

Como detectar erros

Se um erro for gerado pela entrada ou saída 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);

Tela cheia necessária?

Originalmente, o bloqueio do ponteiro estava vinculado à API FullScreen. Um elemento precisa estar no modo de tela cheia para que o cursor possa ser bloqueado. Isso não é mais verdade, e o bloqueio do ponteiro pode ser usado para qualquer elemento do aplicativo em tela cheia ou não.

Exemplo de controles de tiro em primeira pessoa

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

Os controles de tiro em primeira pessoa são criados em torno de quatro mecânicas principais:

  • Como avançar e retroceder ao longo do vetor de aparência atual
  • Como mover para a esquerda e para a direita ao longo do vetor de movimentação atual
  • Girar a guinada da visualização (esquerda e direita)
  • Como 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 up constante. O vetor superior é sempre (0, 1, 0). As quatro mecânicas acima apenas manipulam a posição e o vetor de aparência da câmera de maneiras diferentes.

Movimento

A primeira coisa na apresentação é o movimento. Na demonstração abaixo, o movimento está associado às teclas W, A, S e D padrão. As teclas W e S levam a câmera para frente e para trás. Enquanto as teclas A e D orientam a câmera para a esquerda e para a direita. É muito 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 uso da esquerda e da direita requer uma direção de strafe. A direção do strafe pode ser calculada usando o produto cruzado:

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

Depois de ter a direção da strafe, implementar o movimento da strafe é o mesmo que mover para frente ou para trás.

O próximo passo é girar a visualização.

Guinada

A declinação, ou rotação horizontal da visualização da câmera, é apenas uma rotação em torno do vetor para cima constante. Confira abaixo o código geral para girar o vetor de aparência da câmera em torno de um eixo arbitrário. Ela construa um quatérnio que representa a rotação de deltaAngle radianos em torno de axis e, em seguida, 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);

Faça sugestões

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

Resumo

A API Pointer Lock permite que você controle o cursor do mouse. Ao criar jogos para a Web, seus jogadores vão adorar quando pararem de ficar fragmentados porque moveram o mouse para fora da janela e o jogo parou de receber atualizações. O uso é simples:

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

Demonstrações externas

Referências