指针锁定和第一人称射击游戏控件

John McCutchan
John McCutchan

简介

Pointer Lock API 有助于在浏览器游戏中正确实现第一人称射击游戏控制。如果没有相对鼠标移动,玩家的光标可能会(例如)到达屏幕右边缘,而任何进一步向右的移动都会被忽略 - 视图不会继续向右平移,玩家将无法追击坏人并用机枪扫射他们。玩家会遭到击杀,并感到沮丧。使用指针锁定后,就不会出现这种不理想的行为。

Pointer Lock API 允许您的应用执行以下操作:

  • 获取对原始鼠标数据(包括相对鼠标移动)的访问权限
  • 将所有鼠标事件路由到特定元素

启用指针锁定后,鼠标光标会被隐藏,您可以根据需要选择绘制应用专属指针,也可以让鼠标指针保持隐藏状态,以便用户可以使用鼠标移动框架。相对鼠标移动是指鼠标指针位置相对于上一帧的变化量,与绝对位置无关。例如,如果鼠标指针从 (640, 480) 移动到 (520, 490),则相对移动为 (-120, 10)。请参阅下文中的互动示例,其中显示了原始鼠标位置增量。

本教程涵盖两个主题:激活和处理指针锁定事件的基础知识,以及实现第一人称射击游戏控制方案。没错,读完本文后,您将了解如何使用指针锁定功能,并为自己的浏览器游戏实现 Quake 风格的控制!

浏览器兼容性

Browser Support

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

Source

指针锁定机制

功能检测

如需确定用户的浏览器是否支持指针锁定,您需要检查文档对象中是否存在 pointerLockElement 或供应商前缀版本。在代码中:

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

目前,只有 Firefox 和 Chrome 支持指针锁定。Opera 和 IE 尚不支持。

正在激活

激活指针锁定分为两个步骤。首先,您的应用会请求为特定元素启用指针锁定,在用户授予权限后,系统会立即触发 pointerlockchange 事件。用户可以随时按 Esc 键取消指针锁定。您的应用也可以通过编程方式退出指针锁定。取消指针锁定时,系统会触发 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();

上述代码就是所需的全部内容。当浏览器锁定指针时,系统会弹出一个气泡,告知用户您的应用已锁定指针,并指示用户可以通过按“Esc”键取消锁定。

Chrome 中的指针锁定信息栏。
Chrome 中的指针锁定信息栏。

事件处理

您的应用必须为两个事件添加监听器。第一个是 pointerlockchange,每当指针锁定状态发生变化时,就会触发该事件。第二个是 mousemove,它会在鼠标移动时触发。

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

在 pointerlockchange 回调中,您必须检查指针是刚刚被锁定还是刚刚被解锁。确定是否已启用指针锁定很简单:检查 document.pointerLockElement 是否等于请求指针锁定的元素。如果为 true,则表示您的应用已成功锁定指针;如果为 false,则表示指针已被用户或您自己的代码解锁。

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

启用指针锁定后,clientX、clientY、screenX 和 screenY 会保持不变。movementX 和 movementY 会更新为指针自上次事件传递以来移动的像素数。在伪代码中:

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

在 mousemove 回调中,可以从事件的 movementX 和 movementY 字段中提取相对鼠标移动数据。

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

捕获错误

如果进入或退出指针锁定状态时出现错误,系统会触发 pointerlockerror 事件。此事件没有附加任何数据。

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

是否必须全屏?

最初,指针锁定与 FullScreen API 相关联。这意味着,元素必须处于全屏模式,才能将指针锁定到该元素。现在情况已发生变化,无论是否全屏,指针锁定都可用于应用中的任何元素。

第一人称射击游戏控件示例

现在,我们已启用指针锁定并接收事件,接下来看一个实际示例。您是否曾想了解《Quake》中的控制机制是如何运作的?系好安全带,因为我将用代码来解释它们!

第一人称射击游戏控件围绕四个核心机制构建:

  • 沿当前观察矢量向前和向后移动
  • 沿当前扫射矢量左右移动
  • 旋转视图偏航角(左右)
  • 旋转视图俯仰角(上下)

实现此控制方案的游戏只需要三个数据:镜头位置、镜头观察矢量和恒定向上矢量。向上向量始终为 (0, 1, 0)。上述四种机制只是以不同的方式操纵相机位置和相机视角矢量。

移动

首先是移动。在下面的演示中,移动操作映射到标准的 W、A、S 和 D 键。W 键和 S 键可控制摄像头向前和向后移动。A 键和 D 键则用于控制摄像头向左和向右移动。向前和向后移动相机很简单:

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

左右扫射需要一个扫射方向。可以使用叉积计算扫射方向:

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

确定扫射方向后,实现扫射移动与向前或向后移动相同。

接下来是旋转视图。

偏航

偏航角或相机视图的水平旋转只是围绕恒定向上矢量的旋转。以下是用于围绕任意轴旋转相机视线向量的通用代码。其工作原理是构造一个四元数,表示围绕 axis 旋转 deltaAngle 弧度,然后使用该四元数旋转相机观察向量:

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

推介

实现相机视图的俯仰角或垂直旋转与此类似,但您需要围绕扫射矢量应用旋转,而不是围绕向上矢量应用旋转。第一步是计算扫射向量,然后围绕该轴旋转相机视线向量。

摘要

Pointer Lock API 可让您控制鼠标光标。如果您正在制作网页游戏,那么当玩家因为兴奋地将鼠标移出窗口而导致游戏停止接收鼠标更新,从而导致他们被击杀时,他们会非常喜欢这个功能。用法很简单:

  • 添加 pointerlockchange 事件监听器以跟踪指针锁定状态
  • 请求锁定特定元素的指针
  • 添加 mousemove 事件监听器以获取更新

外部演示

参考