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

John McCutchan
John McCutchan

简介

Pointer Lock API 有助于在浏览器游戏中正确实现第一人称射击游戏控件。例如,如果没有相对鼠标移动,玩家的光标可能会撞到屏幕右侧,而向右的任何进一步移动都会被忽略 - 视图不会继续向右平移,玩家也无法追捕坏人并用机关枪扫射他们。玩家会变得紧张和沮丧。使用指针锁定功能,这种次优行为就不会发生。

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

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

启用指针锁定的副作用是,系统会隐藏鼠标光标,以便您根据需要选择绘制特定于应用的指针,或让鼠标指针保持隐藏状态,以便用户使用鼠标移动框架。相对鼠标移动是指鼠标指针位置相对于上一帧的增量(无论绝对位置如何)。例如,如果鼠标指针从 (640, 480) 移动到 (520, 490),则相对移动为 (-120, 10)。请参阅下方显示原始鼠标位置增量值的交互式示例。

本教程将介绍以下两个主题:启用和处理指针锁定事件的要点,以及实现第一人称射击游戏控制方案。没错,阅读完本文后,您将了解如何使用指针锁定功能,以及如何为您自己的浏览器游戏实现 Quake 风格的控件!

浏览器兼容性

浏览器支持

  • Chrome:37.
  • Edge:13.
  • Firefox:50.
  • Safari: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 中的控件是如何运作的?请系好安全带,我将通过代码来解释这些概念!

第一人称射击游戏控制机制以四大核心机制为基础:

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

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

推介

实现相机视图的俯仰或垂直旋转的方式类似,但您需要围绕平移矢量旋转,而不是围绕向上矢量旋转。第一步是计算 strafing 矢量,然后围绕该轴旋转相机视线矢量。

摘要

您可以使用 Pointer Lock API 控制鼠标光标。如果您在制作 Web 游戏,当玩家因为兴奋地将鼠标移出窗口而导致游戏停止接收鼠标更新,而不再被击杀时,他们会非常开心。使用方法很简单:

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

外部演示

参考