Bloqueo del puntero y controles de disparos en primera persona

Introducción

La API de Pointer Lock ayuda a implementar correctamente controles de disparos en primera persona en un juego de navegador. Sin el movimiento relativo del mouse, el cursor del jugador podría, por ejemplo, tocar el borde derecho de la pantalla y cualquier otro movimiento hacia la derecha se descontaría: la vista no seguiría desplazando el dedo hacia la derecha y el jugador no podría perseguir a los malos ni aplastarlos con su ametralladora. El jugador se desgastará y se frustrará. Con el bloqueo del puntero, no puede ocurrir este comportamiento subóptimo.

La API de Pointer Lock permite que tu aplicación haga lo siguiente:

  • Obtén acceso a datos sin procesar del mouse, incluidos los movimientos relativos del mouse.
  • Enrutar todos los eventos del mouse a un elemento específico

Como efecto secundario de habilitar el bloqueo del puntero, el cursor del mouse está oculto y te permite elegir dibujar un puntero específico de la aplicación si lo deseas, o dejar el puntero del mouse oculto para que el usuario pueda mover el marco con el mouse. El movimiento relativo del mouse es el delta de la posición del puntero del mouse respecto del fotograma anterior, independientemente de la posición absoluta. Por ejemplo, si el puntero del mouse se movió de (640, 480) a (520, 490), el movimiento relativo fue (-120, 10). A continuación, puedes ver un ejemplo interactivo que muestra los deltas de posición del mouse sin procesar.

En este instructivo, se abarcan dos temas: los aspectos básicos de la activación y el procesamiento de eventos de bloqueo del puntero y la implementación del esquema de control de disparos en primera persona. Así es. Cuando termines de leer este artículo, sabrás cómo usar el bloqueo del puntero e implementar controles estilo Quake para tu propio juego de navegador.

Compatibilidad del navegador

Navegadores compatibles

  • 37
  • 13
  • 50
  • 10.1

Origen

Mecánica de bloqueo del puntero

Detección de atributos

Para determinar si el navegador del usuario admite el bloqueo del puntero, debes verificar si pointerLockElement o una versión con el prefijo del proveedor en el objeto del documento. En el código:

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

Actualmente, el bloqueo del puntero solo está disponible en Firefox y Chrome. Opera e IE todavía no lo admiten.

Activando

La activación del bloqueo del puntero es un proceso de dos pasos. Primero, tu aplicación solicita que se habilite el bloqueo del puntero para un elemento específico y, inmediatamente después de que el usuario otorgue el permiso, se activa un evento pointerlockchange. El usuario puede cancelar el bloqueo del puntero en cualquier momento presionando la tecla Escape. Tu aplicación también puede salir de forma programática del bloqueo del puntero. Cuando se cancela el bloqueo del puntero, se activa un evento pointerlockchange.

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

El código anterior es todo lo que se necesita. Cuando el navegador bloquea el puntero, aparecerá un cuadro emergente que le informará al usuario que tu aplicación bloqueó el puntero y le indicará que puede cancelarlo si presiona la tecla "Esc".

Barra de información de bloqueo del puntero en Chrome
Barra de información de bloqueo de puntero en Chrome

Manejo de eventos

Existen dos eventos para los que tu aplicación debe agregar objetos de escucha. El primero es pointerlockchange, que se activa cada vez que se produce un cambio en el estado de bloqueo del puntero. El segundo es mousemove, que se activa cada vez que se mueve el mouse.

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

Dentro de la devolución de llamada pointerlockchange, debes verificar si el puntero se acaba de bloquear o desbloquear. Determinar si el bloqueo del puntero estaba habilitado es sencillo: comprueba si document.pointerLockElement es igual al elemento para el que se solicitó el bloqueo del puntero. Si es así, tu aplicación bloqueó correctamente el puntero y, si no lo está, el usuario o tu propio código desbloqueó el puntero.

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

Cuando se habilita el bloqueo del puntero, clientX, clientY, screenX y screenY permanecen constantes. movementX y movementY se actualizan con la cantidad de píxeles que el puntero se habría movido desde que se entregó el último evento. En seudocódigo:

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

Dentro de la devolución de llamada mousemove, los datos de movimiento relativo del mouse se pueden extraer de los campos movementX y movementY del evento.

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

Detecta errores

Si se genera un error al ingresar o salir del puntero, se activa el evento pointerlockerror. No hay datos adjuntos a este evento.

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

¿Usar el modo de pantalla completa?

Originalmente, el bloqueo del puntero estaba vinculado a la API de FullScreen. Esto significa que un elemento debe estar en modo de pantalla completa antes de poder bloquear el puntero. Esto ya no es verdadero y el bloqueo del puntero se puede usar para cualquier elemento en la pantalla completa de tu app o no.

Ejemplo de controles de disparos en primera persona

Ahora que el bloqueo del puntero está habilitado y la recepción de eventos, es hora de ver un ejemplo práctico. ¿Alguna vez quisiste saber cómo funcionan los controles de Quake? ¡Ponte en acción, porque voy a explicarlos con un código!

Los controles de los juegos de disparos en primera persona se basan en cuatro mecanismos principales:

  • Avanzar y retroceder a lo largo del vector de vista actual
  • Moverse hacia la izquierda y la derecha a lo largo del vector de strafe actual
  • Cómo rotar la guiñada de la vista (izquierda y derecha)
  • Cómo rotar la inclinación de la vista (hacia arriba y hacia abajo)

Un juego que implementa este esquema de control necesita solo tres datos: posición de la cámara, vector de apariencia de cámara y vector arriba constante. El vector arriba es siempre (0, 1, 0). Las cuatro mecánicas anteriores solo manipulan la posición y el vector de apariencia de la cámara de diferentes maneras.

Movimiento

Lo primero que hay que hacer es el movimiento. En la siguiente demostración, el movimiento se asigna a las teclas W, A, S y D estándar. Las teclas W y S dirigen la cámara hacia delante y hacia atrás. Mientras que las teclas A y D dirigen la cámara hacia la izquierda y la derecha. Mover la cámara hacia delante y hacia atrás es sencillo:

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

El despliegue hacia la izquierda y la derecha requiere una dirección de strafe. La dirección de la Strafe se puede calcular usando el producto cruzado:

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

Una vez que tengas la dirección de la traga, implementar este movimiento es lo mismo que moverte hacia adelante o hacia atrás.

A continuación, gira la vista.

Guiñada

La guiñada, o la rotación horizontal de la vista de la cámara, es solo una rotación alrededor del vector de dirección hacia arriba constante. A continuación, se muestra el código general para rotar el vector de apariencia de la cámara alrededor de un eje arbitrario. Funciona construyendo un cuaternión que representa la rotación de radianes de deltaAngle alrededor de axis y, luego, usa ese cuaternión para rotar el vector de apariencia de la cámara:

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

Sugiere

La implementación de la inclinación o la rotación vertical de la vista de la cámara es similar, pero en lugar de una rotación alrededor del vector de arriba, se aplica una rotación alrededor del vector de Strafe. El primer paso es calcular el vector de Strafe y, luego, rotar el vector de apariencia de la cámara alrededor de ese eje.

Resumen

La API de Pointer Lock te permite controlar el cursor del mouse. Si creas juegos web, a tus jugadores les encantará cuando dejen de desestabilizarse porque sacaron el mouse de la ventana con entusiasmo y el juego dejó de recibir actualizaciones. El uso es sencillo:

  • Agrega el objeto de escucha de eventos pointerlockchange para hacer un seguimiento del estado del bloqueo del puntero.
  • Solicita el bloqueo del puntero para un elemento específico
  • Agrega el objeto de escucha de eventos mousemove para obtener actualizaciones

Demostraciones externas

Referencias