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

约翰·麦库钦
John McCutchan

简介

Pointer Lock API 有助于在浏览器游戏中正确实现第一人称射击游戏控件。例如,如果不进行相对的鼠标移动,玩家的光标可能会触及屏幕的右侧边缘,之后往右移动的幅度会受到限制,因为视图无法继续向右平移,玩家也无法追踪坏人并用机枪扫视他们。玩家会感到沮丧并感到沮丧。使用指针锁定时,不会发生这种不理想行为。

利用 Pointer Lock API,您的应用可以执行以下操作:

  • 获取原始鼠标数据,包括相对鼠标移动
  • 将所有鼠标事件路由到特定元素

启用指针锁定的附带作用是,鼠标光标会隐藏起来,这样您就可以选择绘制特定于应用的指针(如果需要的话),或者隐藏鼠标指针以便用户可以使用鼠标移动帧。相对鼠标移动是指鼠标指针位置相对于上一帧的增量,与绝对位置无关。例如,如果鼠标指针从 (640, 480) 移动到 (520, 490),则相对移动为 (-120, 10)。请参阅下文,了解显示原始鼠标位置增量的交互式示例。

本教程涵盖两个主题:激活和处理指针锁定事件的基本要点,以及实现第一人称射击游戏控件方案。没错,读完这篇文章后,你会了解如何使用指针锁定,以及如何在你自己的浏览器游戏中实现 Quake 式控件!

浏览器兼容性

浏览器支持

  • 37
  • 13
  • 50
  • 10.1

来源

指针锁定机制

特征检测

如需确定用户的浏览器是否支持指针锁定,您需要检查文档对象中是否有 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 是否等于请求了指针锁定的元素。如果已锁定,则表示您的应用已成功锁定指针,如果没有,则表示用户或您自己的代码已解锁指针。

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

启用指针锁定后,clientXclientYscreenXscreenY 保持不变。movementXmovementY 会更新为自上次传递事件以来指针本应移动的像素数。在伪代码中:

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

mousemove 回调内,可以从事件的 movementXmovementY 字段提取相对鼠标移动数据。

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 中的控件是如何运作的?别担心,我准备用代码来解释一下!

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

  • 沿当前 Look 矢量向前和向后移动
  • 沿当前扫视矢量左右移动
  • 旋转视图偏摆(左右)
  • 旋转视图倾斜度(上下)

实现此控制方案的游戏只需要三个数据:摄像头位置、摄像头视角向量和常数向上矢量。向上矢量始终为 (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);

确定纸条的方向后,执行纸条移动与向前或向后移动相同。

接下来是旋转视图。

偏摆

镜头视图的偏摆或水平旋转只是围绕固定的向上矢量旋转。以下是用于围绕任意轴旋转镜头外观矢量的通用代码。其工作原理是,构建一个表示 deltaAngle 弧度绕 axis 旋转的四元数,然后使用四元数旋转镜头外观矢量:

// 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 事件监听器以获取更新

外部演示

参考编号