桌面游戏手柄控制
现在我们将看看如何添加一些额外的功能——通过游戏手柄 API 支持游戏手柄控制。它为您的 Web 游戏带来了类似控制台的体验。
游戏手柄 API 使您能够将游戏手柄连接到您的计算机,并通过 JavaScript 代码直接检测按下的按钮,这得益于浏览器实现了此功能。一个 API 公开了您连接游戏逻辑并成功控制用户界面和游戏玩法所需的所有信息。
API 状态、浏览器和硬件支持
该 游戏手柄 API 仍处于工作草案状态,尽管浏览器支持已经相当不错——根据 caniuse.com 的数据,全球覆盖率约为 63%。支持的设备列表也非常广泛——大多数流行的游戏手柄(例如 Xbox 360 或 PS3)都适合 Web 实现。
纯 JavaScript 方法
让我们考虑一下如何在我们的 小型控制演示 中实现纯 JavaScript 游戏手柄控制,以了解其工作原理。首先,我们需要一个事件侦听器来侦听新设备的连接
window.addEventListener("gamepadconnected", gamepadHandler);
它只执行一次,因此我们可以创建一些稍后需要用于存储控制器信息和已按下按钮的变量
let controller = {};
let buttonsPressed = [];
function gamepadHandler(e) {
controller = e.gamepad;
output.textContent = `Gamepad: ${controller.id}`;
}
gamepadHandler
函数中的第二行在设备连接时显示在屏幕上。
我们还可以显示设备的 id
——在上面的示例中,我们使用的是 Xbox 360 无线控制器。
要更新游戏手柄当前按下按钮的状态,我们需要一个函数在每一帧中执行此操作
function gamepadUpdateHandler() {
buttonsPressed = [];
if (controller.buttons) {
for (let b = 0; b < controller.buttons.length; b++) {
if (controller.buttons[b].pressed) {
buttonsPressed.push(b);
}
}
}
}
我们首先重置 buttonsPressed
数组,使其准备好存储我们将从当前帧写入的最新信息。然后,如果按钮可用,我们遍历它们;如果 pressed
属性设置为 true
,则将其添加到 buttonsPressed
数组以供以后处理。接下来,我们将考虑 gamepadButtonPressedHandler()
函数
function gamepadButtonPressedHandler(button) {
let press = false;
for (let i = 0; i < buttonsPressed.length; i++) {
if (buttonsPressed[i] === button) {
press = true;
}
}
return press;
}
该函数以按钮作为参数;在循环中,它检查给定按钮的编号是否在 buttonsPressed
数组中当前按下的按钮中。如果是,则该函数返回 true
;否则返回 false
。
接下来,在 draw()
函数中,我们执行两件事——执行 gamepadUpdateHandler()
函数以获取每一帧中按下按钮的当前状态,并使用 gamepadButtonPressedHandler()
函数检查我们感兴趣的按钮是否已按下,如果已按下则执行某些操作
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// …
gamepadUpdateHandler();
if (gamepadButtonPressedHandler(0)) {
playerY -= 5;
} else if (gamepadButtonPressedHandler(1)) {
playerY += 5;
}
if (gamepadButtonPressedHandler(2)) {
playerX -= 5;
} else if (gamepadButtonPressedHandler(3)) {
playerX += 5;
}
if (gamepadButtonPressedHandler(11)) {
alert("BOOM!");
}
// …
ctx.drawImage(img, playerX, playerY);
requestAnimationFrame(draw);
}
在这种情况下,我们正在检查四个方向键按钮 (0-3) 和 A 按钮 (11)。
注意:请记住,不同的设备可能具有不同的按键映射,例如,无线 Xbox 360 上的方向键右按钮的索引为 3,但在其他设备上可能不同。
您还可以创建一个辅助函数,为列出的按钮分配正确的名称,例如,而不是检查 gamepadButtonPressedHandler(3)
是否已按下,您可以进行更具描述性的检查:gamepadButtonPressedHandler('DPad-Right')
。
您可以看到一个 实时演示——尝试连接您的游戏手柄并按下按钮。
Phaser 方法
让我们继续在使用 Phaser 创建的 Captain Rogers: Battle at Andromeda 游戏中进行最终的游戏手柄 API 实现。但是,这也是纯 JavaScript 代码,因此无论使用什么框架,都可以将其用于任何其他项目。
首先,我们将创建一个小型库来负责处理输入。这是包含有用变量和函数的 GamepadAPI
对象
const GamepadAPI = {
active: false,
controller: {},
connect(event) {},
disconnect(event) {},
update() {},
buttons: {
layout: [],
cache: [],
status: [],
pressed(button, state) {},
},
axes: {
status: [],
},
};
controller
变量存储有关已连接游戏手柄的信息,并且有一个 active
布尔变量,我们可以使用它来了解控制器是否已连接。connect()
和 disconnect()
函数绑定到以下事件
window.addEventListener("gamepadconnected", GamepadAPI.connect);
window.addEventListener("gamepaddisconnected", GamepadAPI.disconnect);
当游戏手柄分别连接和断开连接时,它们会被触发。下一个函数是 update()
,它更新有关已按下按钮和轴的信息。
buttons
变量包含给定控制器的 layout
(例如哪些按钮在哪里,因为 Xbox 360 布局可能与通用布局不同),cache
包含前一帧的按钮信息,以及 status
包含当前帧的信息。
pressed()
函数获取输入数据并在我们的对象中设置有关它的信息,并且 axes
属性存储包含表示轴在 x
和 y
方向上按下程度的值的数组,由 (-1, 1)
范围内的浮点数表示。
游戏手柄连接后,有关控制器的信息将存储在对象中
connect(event) {
GamepadAPI.controller = event.gamepad;
GamepadAPI.active = true;
},
disconnect
函数从对象中删除信息
disconnect(event) {
delete GamepadAPI.controller;
GamepadAPI.active = false;
},
update()
函数在每一帧的游戏更新循环中执行,因此它包含有关已按下按钮的最新信息
update() {
GamepadAPI.buttons.cache = [];
for (let k = 0; k < GamepadAPI.buttons.status.length; k++) {
GamepadAPI.buttons.cache[k] = GamepadAPI.buttons.status[k];
}
GamepadAPI.buttons.status = [];
const c = GamepadAPI.controller || {};
const pressed = [];
if (c.buttons) {
for (let b = 0; b < c.buttons.length; b++) {
if (c.buttons[b].pressed) {
pressed.push(GamepadAPI.buttons.layout[b]);
}
}
}
const axes = [];
if (c.axes) {
for (let a = 0; a < c.axes.length; a++) {
axes.push(c.axes[a].toFixed(2));
}
}
GamepadAPI.axes.status = axes;
GamepadAPI.buttons.status = pressed;
return pressed;
},
上面的函数清除按钮缓存,并将它们从前一帧的状态复制到缓存中。接下来,按钮状态被清除并添加新信息。轴信息也是如此——遍历轴将值添加到数组中。接收到的值被分配给相应的对象并返回已按下的信息以进行调试。
button.pressed()
函数检测实际的按钮按下
pressed(button, hold) {
let newPress = false;
for (let i = 0; i < GamepadAPI.buttons.status.length; i++) {
if (GamepadAPI.buttons.status[i] === button) {
newPress = true;
if (!hold) {
for (let j = 0; j < GamepadAPI.buttons.cache.length; j++) {
if (GamepadAPI.buttons.cache[j] === button) {
newPress = false;
}
}
}
}
}
return newPress;
},
它遍历已按下的按钮,如果我们正在查找的按钮已按下,则相应的布尔变量将设置为 true
。如果我们想检查按钮是否尚未按住(因此这是一个新的按下),则遍历前一帧的缓存状态即可完成此操作——如果按钮已按下,则我们忽略新的按下并将其设置为 false
。
实施
现在我们知道了 GamepadAPI
对象的外观以及它包含的变量和函数,因此让我们了解所有这些如何在游戏中实际使用。为了指示游戏手柄控制器处于活动状态,我们可以向用户显示游戏主菜单屏幕上的一些自定义文本。
textGamepad
对象包含表示已连接游戏手柄的文本,默认情况下处于隐藏状态。这是我们在 create()
函数中准备的代码,该函数在创建新状态时执行一次
create() {
// …
const message = 'Gamepad connected! Press Y for controls';
const textGamepad = this.add.text(0, 0, message);
textGamepad.visible = false;
}
在 update()
函数中,该函数在每一帧中执行,我们可以等待控制器实际连接,以便显示正确的文本。然后,我们可以使用 Gamepad.update()
方法跟踪有关已按下按钮的信息,并对给定信息做出反应
update() {
// …
if (GamepadAPI.active) {
this.textGamepad.visible = true;
GamepadAPI.update();
if (GamepadAPI.buttons.pressed('Start')) {
// start the game
}
if (GamepadAPI.buttons.pressed('X')) {
// turn on/off the sounds
}
this.screenGamepadHelp.visible = GamepadAPI.buttons.pressed('Y', 'hold');
}
}
按下 Start
按钮时,将调用相关函数以开始游戏,并且使用相同的方法打开和关闭音频。有一个选项连接到显示 screenGamepadHelp
,它包含一张解释所有按钮控制的图片——如果按下并按住 Y
按钮,则帮助将可见;当释放时,帮助将消失。
屏幕上的说明
游戏开始时,会显示一些介绍性文本,向您显示可用的控件——我们已经在检测游戏是在桌面还是移动设备上启动,然后显示设备的相关消息,但我们可以更进一步,以允许游戏手柄的存在
create() {
// …
if (this.game.device.desktop) {
if (GamepadAPI.active) {
moveText = 'DPad or left Stick\nto move';
shootText = 'A to shoot,\nY for controls';
} else {
moveText = 'Arrow keys\nor WASD to move';
shootText = 'X or Space\nto shoot';
}
} else {
moveText = 'Tap and hold to move';
shootText = 'Tap to shoot';
}
}
在桌面上,我们可以检查控制器是否处于活动状态并显示游戏手柄控件——如果不是,则显示键盘控件。
游戏控制
我们可以通过为玩家提供主要和备用游戏手柄移动控件来提供更大的灵活性
if (GamepadAPI.buttons.pressed("DPad-Up", "hold")) {
// move player up
} else if (GamepadAPI.buttons.pressed("DPad-Down", "hold")) {
// move player down
}
if (GamepadAPI.buttons.pressed("DPad-Left", "hold")) {
// move player left
}
if (GamepadAPI.buttons.pressed("DPad-Right", "hold")) {
// move player right
}
if (GamepadAPI.axes.status) {
if (GamepadAPI.axes.status[0] > 0.5) {
// move player up
} else if (GamepadAPI.axes.status[0] < -0.5) {
// move player down
}
if (GamepadAPI.axes.status[1] > 0.5) {
// move player left
} else if (GamepadAPI.axes.status[1] < -0.5) {
// move player right
}
}
现在,用户可以通过使用DPad
按钮或左侧摇杆轴来移动屏幕上的飞船。
你有没有注意到轴的当前值是相对于0.5
进行评估的?这是因为轴具有浮点数,而按钮是布尔值。当达到某个阈值时,我们可以假设输入是用户有意进行的,并可以相应地采取行动。
对于射击控制,我们使用了A
按钮——当按住它时,会生成一颗新的子弹,其他所有操作都由游戏处理。
if (GamepadAPI.buttons.pressed("A", "hold")) {
this.spawnBullet();
}
显示所有控件的屏幕与主菜单中的屏幕完全相同。
this.screenGamepadHelp.visible = GamepadAPI.buttons.pressed("Y", "hold");
如果按下B
按钮,游戏就会暂停。
if (gamepadAPI.buttonPressed("B")) {
this.managePause();
}
暂停和游戏结束状态
我们已经学习了如何控制游戏的整个生命周期:暂停游戏、重新开始游戏或返回主菜单。它在移动设备和桌面设备上运行流畅,添加游戏手柄控制也同样简单——在update()
函数中,我们检查当前状态是否为paused
——如果是,则启用相关操作。
if (GamepadAPI.buttons.pressed("Start")) {
this.managePause();
}
if (GamepadAPI.buttons.pressed("Back")) {
this.stateBack();
}
类似地,当gameover
状态处于活动状态时,我们可以允许用户重新开始游戏,而不是继续游戏。
if (GamepadAPI.buttons.pressed("Start")) {
this.stateRestart();
}
if (GamepadAPI.buttons.pressed("Back")) {
this.stateBack();
}
当游戏结束屏幕可见时,Start
按钮重新开始游戏,而Back
按钮帮助我们返回主菜单。游戏暂停时也是如此:Start
按钮取消暂停游戏,Back
按钮返回上一步,就像之前一样。