使用游戏手柄 API
HTML 提供了丰富、交互式游戏开发所需的必要组件。<canvas>
、WebGL、<audio>
和 <video>
等技术,以及 JavaScript 实现,支持提供与原生代码类似(如果不是相同)功能的任务。游戏手柄 API 允许开发人员和设计师访问和使用游戏手柄和其他游戏控制器。
游戏手柄 API 在 Window
对象上引入了用于读取游戏手柄和控制器(以下简称游戏手柄)状态的新事件。除了这些事件之外,API 还添加了 Gamepad
对象,可用于查询连接的游戏手柄的状态,以及 navigator.getGamepads()
方法,可用于获取页面已知的 gamepads 列表。
连接到游戏手柄
当新的游戏手柄连接到计算机时,焦点页面首先会收到 gamepadconnected
事件。如果页面加载时游戏手柄已连接,则当用户按下按钮或移动轴时,gamepadconnected
事件将被分派到焦点页面。
注意:在 Firefox 中,游戏手柄只有在用户与页面可见时与之交互时才会向页面公开。这有助于防止游戏手柄用于指纹识别 用户。一旦与一个游戏手柄交互,已连接的其他游戏手柄将自动可见。
您可以像这样使用 gamepadconnected
window.addEventListener("gamepadconnected", (e) => {
console.log(
"Gamepad connected at index %d: %s. %d buttons, %d axes.",
e.gamepad.index,
e.gamepad.id,
e.gamepad.buttons.length,
e.gamepad.axes.length,
);
});
每个游戏手柄都有一个与其关联的唯一 ID,它在事件的 gamepad
属性上可用。
断开游戏手柄连接
当游戏手柄断开连接时,如果页面之前已收到该游戏手柄的数据(例如 gamepadconnected
),则会向焦点窗口分派第二个事件,gamepaddisconnected
window.addEventListener("gamepaddisconnected", (e) => {
console.log(
"Gamepad disconnected from index %d: %s",
e.gamepad.index,
e.gamepad.id,
);
});
游戏手柄的 index
属性对于连接到系统的每个设备都是唯一的,即使使用了相同类型的多个控制器。index
属性也充当 Array
中的索引,该数组由 Navigator.getGamepads()
返回。
const gamepads = {};
function gamepadHandler(event, connected) {
const gamepad = event.gamepad;
// Note:
// gamepad === navigator.getGamepads()[gamepad.index]
if (connected) {
gamepads[gamepad.index] = gamepad;
} else {
delete gamepads[gamepad.index];
}
}
window.addEventListener(
"gamepadconnected",
(e) => {
gamepadHandler(e, true);
},
false,
);
window.addEventListener(
"gamepaddisconnected",
(e) => {
gamepadHandler(e, false);
},
false,
);
前面的示例还演示了事件完成后如何保留 gamepad
属性——这是一种我们将在稍后用于设备状态查询的技术。
查询 Gamepad 对象
如您所见,上面讨论的gamepads 事件在事件对象上包含一个 gamepad
属性,该属性返回一个 Gamepad
对象。我们可以使用它来确定哪个游戏手柄(即其 ID)导致了该事件,因为可能同时连接了多个游戏手柄。我们还可以使用 Gamepad
对象做更多的事情,包括对它进行引用并查询它以找出任何时候都在按下哪些按钮和轴。对于需要了解游戏手柄当前状态(而不是下次事件触发时)的游戏或其他交互式网页来说,这样做通常是可取的。
执行此类检查往往涉及将 Gamepad
对象与动画循环(例如 requestAnimationFrame
)结合使用,开发人员希望根据游戏手柄的状态做出当前帧的决策。
Navigator.getGamepads()
方法返回一个数组,其中包含当前对网页可见的所有设备,作为 Gamepad
对象(第一个值始终为 null
,因此如果没有任何游戏手柄连接,则会返回 null
)。然后可以使用它来获取相同的信息。例如,上面的第一个代码示例可以改写如下
window.addEventListener("gamepadconnected", (e) => {
const gp = navigator.getGamepads()[e.gamepad.index];
console.log(
"Gamepad connected at index %d: %s. %d buttons, %d axes.",
gp.index,
gp.id,
gp.buttons.length,
gp.axes.length,
);
});
Gamepad
对象的属性如下
id
:包含有关控制器的某些信息的字符串。这没有严格指定,但在 Firefox 中,它将包含由连字符 (-
) 分隔的三个信息:两个 4 位十六进制字符串,包含控制器的 USB 供应商和产品 ID,以及驱动程序提供的控制器名称。此信息旨在让您找到设备上控件的映射,并向用户显示有用的反馈。index
:一个整数,对于当前连接到系统的每个 gamepads 都是唯一的。这可用于区分多个控制器。请注意,断开设备连接然后连接新设备可能会重复使用之前的索引。mapping
:一个字符串,指示浏览器是否已将设备上的控件重新映射到已知布局。目前,只有一种受支持的已知布局——标准 gamepads。如果浏览器能够将设备上的控件映射到该布局,则mapping
属性将设置为字符串standard
。connected
:一个布尔值,指示 gamepads 是否仍然连接到系统。如果是,则值为True
;否则为False
。buttons
:一个GamepadButton
对象数组,表示设备上存在的按钮。每个GamepadButton
都具有一个pressed
属性和一个value
属性pressed
属性是一个布尔值,指示按钮当前是按下 (true
) 还是未按下 (false
)。value
属性是一个浮点值,用于启用表示模拟按钮,例如许多现代 gamepads 上的触发器。这些值被规范化为 0.0..1.0 范围,其中 0.0 表示未按下的按钮,1.0 表示完全按下的按钮。
axes
:一个数组,表示设备上存在的带轴的控件(例如模拟摇杆)。数组中的每个条目都是 -1.0 - 1.0 范围内的浮点值,表示轴位置从最低值 (-1.0) 到最高值 (1.0)。timestamp
:这将返回一个DOMHighResTimeStamp
,表示上次更新此 gamepads 的数据的时刻,允许开发人员确定axes
和button
数据是否已从硬件更新。该值必须相对于PerformanceTiming
接口的navigationStart
属性。这些值是单调递增的,这意味着它们可以进行比较以确定更新的顺序,因为较新的值将始终大于或等于较旧的值。请注意,Firefox 当前不支持此属性。
注意:出于安全原因,Gamepad 对象在 gamepadconnected
事件上可用,而不是在 Window
对象本身。一旦我们获得了对它的引用,我们就可以查询它的属性以获取有关 gamepads 当前状态的信息。在幕后,每当 gamepads 状态发生变化时,此对象都会更新。
使用按钮信息
让我们来看一个简单的示例,它显示了一个 gamepads 的连接信息(它会忽略后续的 gamepads 连接),并允许您使用 gamepads 右侧的四个 gamepads 按钮在屏幕上移动一个球。您可以查看实时演示,以及在 GitHub 上找到源代码。
首先,我们声明一些变量:gamepadInfo
段落,其中写入连接信息,ball
,我们要移动的球,start
变量,充当 requestAnimation Frame
的 ID,a
和 b
变量,充当移动球的位置修饰符,以及将用于 requestAnimationFrame()
和 cancelAnimationFrame()
跨浏览器分支的简写变量。
const gamepadInfo = document.getElementById("gamepad-info");
const ball = document.getElementById("ball");
let start;
let a = 0;
let b = 0;
接下来,我们使用 gamepadconnected
事件来检查 gamepads 是否连接。当一个 gamepads 连接时,我们使用 navigator.getGamepads()[0]
获取 gamepads,将有关 gamepads 的信息打印到我们的 gamepads 信息 div
中,并触发启动整个球移动过程的 gameLoop()
函数。
window.addEventListener("gamepadconnected", (e) => {
const gp = navigator.getGamepads()[e.gamepad.index];
gamepadInfo.textContent = `Gamepad connected at index ${gp.index}: ${gp.id}. It has ${gp.buttons.length} buttons and ${gp.axes.length} axes.`;
gameLoop();
});
现在,我们使用 gamepaddisconnected
事件来检查 gamepads 是否再次断开连接。如果是,我们停止 requestAnimationFrame()
循环(见下文)并将 gamepads 信息恢复到最初状态。
window.addEventListener("gamepaddisconnected", (e) => {
gamepadInfo.textContent = "Waiting for gamepad.";
cancelAnimationFrame(start);
});
现在进入主游戏循环。在每次循环执行时,我们检查是否有四个按钮中的一个被按下;如果是,我们相应地更新a
和b
移动变量的值,然后更新left
和top
属性,将其值分别更改为a
和b
的当前值。这将使球在屏幕上移动。
完成所有这些操作后,我们使用requestAnimationFrame()
请求下一个动画帧,再次运行gameLoop()
。
function gameLoop() {
const gamepads = navigator.getGamepads();
if (!gamepads) {
return;
}
const gp = gamepads[0];
if (gp.buttons[0].pressed) {
b--;
}
if (gp.buttons[2].pressed) {
b++;
}
if (gp.buttons[1].pressed) {
a++;
}
if (gp.buttons[3].pressed) {
a--;
}
ball.style.left = `${a * 2}px`;
ball.style.top = `${b * 2}px`;
start = requestAnimationFrame(gameLoop);
}
完整示例:显示游戏手柄状态
此示例展示了如何使用Gamepad
对象,以及gamepadconnected
和gamepaddisconnected
事件来显示连接到系统的游戏手柄的状态。此示例基于Gamepad 演示,该演示的源代码可在 GitHub 上获得。
let loopstarted = false;
window.addEventListener("gamepadconnected", (evt) => {
addgamepad(evt.gamepad);
});
window.addEventListener("gamepaddisconnected", (evt) => {
removegamepad(evt.gamepad);
});
function addgamepad(gamepad) {
const d = document.createElement("div");
d.setAttribute("id", `controller${gamepad.index}`);
const t = document.createElement("h1");
t.textContent = `gamepad: ${gamepad.id}`;
d.append(t);
const b = document.createElement("ul");
b.className = "buttons";
gamepad.buttons.forEach((button, i) => {
const e = document.createElement("li");
e.className = "button";
e.textContent = `Button ${i}`;
b.append(e);
});
d.append(b);
const a = document.createElement("div");
a.className = "axes";
gamepad.axes.forEach((axis, i) => {
const p = document.createElement("progress");
p.className = "axis";
p.setAttribute("max", "2");
p.setAttribute("value", "1");
p.textContent = i;
a.append(p);
});
d.appendChild(a);
// See https://github.com/luser/gamepadtest/blob/master/index.html
const start = document.querySelector("#start");
if (start) {
start.style.display = "none";
}
document.body.append(d);
if (!loopstarted) {
requestAnimationFrame(updateStatus);
loopstarted = true;
}
}
function removegamepad(gamepad) {
document.querySelector(`#controller${gamepad.index}`).remove();
}
function updateStatus() {
for (const gamepad of navigator.getGamepads()) {
if (!gamepad) continue;
const d = document.getElementById(`controller${gamepad.index}`);
const buttonElements = d.getElementsByClassName("button");
for (const [i, button] of gamepad.buttons.entries()) {
const el = buttonElements[i];
const pct = `${Math.round(button.value * 100)}%`;
el.style.backgroundSize = `${pct} ${pct}`;
if (button.pressed) {
el.textContent = `Button ${i} [PRESSED]`;
el.style.color = "#42f593";
el.className = "button pressed";
} else {
el.textContent = `Button ${i}`;
el.style.color = "#2e2d33";
el.className = "button";
}
}
const axisElements = d.getElementsByClassName("axis");
for (const [i, axis] of gamepad.axes.entries()) {
const el = axisElements[i];
el.textContent = `${i}: ${axis.toFixed(4)}`;
el.setAttribute("value", axis + 1);
}
}
requestAnimationFrame(updateStatus);
}
规范
规范 |
---|
游戏手柄 # gamepad-interface |
游戏手柄扩展 # partial-gamepad-interface |
浏览器兼容性
BCD 表仅在浏览器中加载