桌面游戏手柄控件

现在我们来看如何添加一些额外的东西——通过 Gamepad API 支持游戏手柄控件。它为您的网页游戏带来了类似主机的体验。

Gamepad API 使您能够将游戏手柄连接到计算机,并通过 JavaScript 代码直接检测按下的按钮,这得益于浏览器实现了此功能。API 暴露了您将游戏逻辑连接起来并成功控制用户界面和游戏玩法所需的所有信息。

API 状态、浏览器和硬件支持

Gamepad API 仍处于工作草案状态,但浏览器支持已经相当好——根据 caniuse.com 的数据,全球覆盖率约为 63%。支持的设备列表也相当广泛——大多数流行的游戏手柄(例如 XBox 360 或 PS3)都应该适合 Web 实现。

纯 JavaScript 方法

首先,让我们考虑在我们的 小控件演示 中实现纯 JavaScript 游戏手柄控件,以了解其工作原理。首先,我们需要一个事件监听器来监听新设备的连接

js
window.addEventListener("gamepadconnected", gamepadHandler);

这只执行一次,所以我们可以创建一些稍后需要用来存储控制器信息和已按下按钮的变量

js
let controller = {};
let buttonsPressed = [];
function gamepadHandler(e) {
  controller = e.gamepad;
  output.textContent = `Gamepad: ${controller.id}`;
}

gamepadHandler 函数中的第二行会在设备连接时显示在屏幕上

Gamepad connected message under the Captain Rogers game - wireless XBox 360 controller.

我们还可以显示设备的 id——在上面的例子中,我们使用的是 XBox 360 无线控制器。

要更新游戏手柄当前按下按钮的状态,我们需要一个函数,它会在每一帧都执行此操作

js
function gamepadUpdateHandler() {
  buttonsPressed = [];
  if (controller.buttons) {
    for (const [i, button] of controller.buttons.entries()) {
      if (button.pressed) {
        buttonsPressed.push(i);
      }
    }
  }
}

我们首先重置 buttonsPressed 数组,以便为存储我们将在当前帧中写入的最新信息做好准备。然后,如果按钮可用,我们循环遍历它们;如果 pressed 属性设置为 true,那么我们将其添加到 buttonsPressed 数组中以供后续处理。接下来,我们将考虑 gamepadButtonPressedHandler() 函数

js
function gamepadButtonPressedHandler(button) {
  return buttonsPressed.includes(button);
}

该函数以按钮索引作为参数;它会检查 buttonsPressed 是否包含我们正在查找的按钮,如果包含则返回 true。这会检查按钮是否被按下。

接下来,在 draw() 函数中,我们做了两件事——执行 gamepadUpdateHandler() 函数以在每一帧获取当前按下按钮的状态,并使用 gamepadButtonPressedHandler() 函数来检查我们感兴趣的按钮是否被按下,如果被按下则执行某些操作

js
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 方法

让我们继续将 Gamepad API 的最终实现应用到我们使用 Phaser 创建的 Captain Rogers: Battle at Andromeda 游戏中。不过,这也是纯 JavaScript 代码,因此可以用于任何其他项目,无论使用什么框架。

首先,我们将创建一个小库来处理输入。这是 GamepadAPI 对象,其中包含有用的变量和函数

js
const GamepadAPI = {
  active: false,
  controller: {},
  connect(event) {},
  disconnect(event) {},
  update() {},
  buttons: {
    layout: [],
    cache: [],
    status: [],
    pressed(button, state) {},
  },
  axes: {
    status: [],
  },
};

controller 变量存储有关已连接游戏手柄的信息,还有一个 active 布尔变量,我们可以用它来知道控制器是否已连接。connect()disconnect() 函数绑定到以下事件

js
window.addEventListener("gamepadconnected", GamepadAPI.connect);
window.addEventListener("gamepaddisconnected", GamepadAPI.disconnect);

它们分别在游戏手柄连接和断开时触发。下一个函数是 update(),它会更新有关按下按钮和摇杆的信息。

buttons 变量包含给定控制器的 layout(例如,哪些按钮在哪里,因为 XBox 360 的布局可能与通用控制器不同),cache 包含上一帧按钮的信息,status 包含当前帧的信息。

pressed() 函数获取输入数据并将相关信息设置在我们的对象中,而 axes 属性存储一个包含表示摇杆按下程度的浮点值的数组,在 xy 方向上,用 (-1, 1) 范围内的浮点数表示。

游戏手柄连接后,控制器信息会存储在对象中

js
const GamepadAPI = {
  // …
  connect(event) {
    GamepadAPI.controller = event.gamepad;
    GamepadAPI.active = true;
  },
  // …
};

disconnect 函数会从对象中移除信息

js
const GamepadAPI = {
  // …
  disconnect(event) {
    delete GamepadAPI.controller;
    GamepadAPI.active = false;
  },
};

update() 函数在游戏的更新循环的每一帧执行,因此它包含有关按下按钮的最新信息

js
const GamepadAPI = {
  // …
  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 (const ax of c.axes) {
        axes.push(ax.toFixed(2));
      }
    }
    GamepadAPI.axes.status = axes;
    GamepadAPI.buttons.status = pressed;
    return pressed;
  },
  // …
};

上面的函数会清除按钮缓存,并将上一帧的按钮状态复制到缓存中。接下来,清除按钮状态并添加新信息。摇杆信息也是如此——循环遍历摇杆会将值添加到数组中。接收到的值会分配给相应的对象,并返回按下信息以供调试。

button.pressed() 函数会检测实际的按钮按下情况

js
const GamepadAPI = {
  // …
  buttons: {
    // …
    pressed(button, hold) {
      let newPress = false;
      if (GamepadAPI.buttons.status.includes(button)) {
        newPress = true;
      }
      if (!hold && GamepadAPI.buttons.cache.includes(button)) {
        newPress = false;
      }
      return newPress;
    },
    // …
  },
  // …
};

它会检查我们正在寻找的按钮是否被按下,如果按下,则将相应的布尔变量设置为 true。如果我们想检查按钮是否尚未被按住(也就是说,这是一次新的按下),那么检查上一帧的缓存状态就可以做到——如果按钮已经被按下,那么我们就忽略新的按下并将其设置为 false

实现

现在我们知道了 GamepadAPI 对象的样子以及它包含哪些变量和函数,那么让我们来学习一下所有这些在游戏中是如何实际使用的。为了指示游戏手柄控制器已激活,我们可以向用户显示一些自定义文本在游戏的主菜单屏幕上。

textGamepad 对象保存着表示已连接游戏手柄的文本,默认情况下是隐藏的。这是我们在 create() 函数中准备的代码,该函数在新状态创建时执行一次

js
function create() {
  // …
  const message = "Gamepad connected! Press Y for controls";
  const textGamepad = this.add.text(0, 0, message);
  textGamepad.visible = false;
}

update() 函数中,该函数每帧执行一次,我们可以等到控制器真正连接后,然后显示相应的文本。然后我们可以通过使用 Gamepad.update() 方法来跟踪已按下按钮的信息,并根据给定信息做出反应

js
function 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 按钮,帮助信息就会显示出来;当释放该按钮时,帮助信息就会消失。

Gamepad info with all the available keys described and explained.

屏幕上的说明

当游戏启动时,会显示一些介绍性文本,显示可用控件——我们已经检测到游戏是在桌面还是移动设备上启动,然后为设备显示相应的消息,但我们可以做得更多,以允许使用游戏手柄

js
function 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";
  }
}

在桌面模式下,我们可以检查控制器是否处于活动状态并显示游戏手柄控件——如果不是,则显示键盘控件。

游戏玩法控制

我们可以通过为主玩家提供主要和辅助游戏手柄移动控件来提供更多灵活性

js
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 按钮——当它被按住时,会生成一个新的子弹,其余的都由游戏处理

js
if (GamepadAPI.buttons.pressed("A", "hold")) {
  this.spawnBullet();
}

显示所有控件的屏幕与主菜单中的外观完全相同

js
this.screenGamepadHelp.visible = GamepadAPI.buttons.pressed("Y", "hold");

如果按下 B 按钮,游戏将被暂停

js
if (gamepadAPI.buttonPressed("B")) {
  this.managePause();
}

暂停和游戏结束状态

我们已经学会了如何控制游戏的整个生命周期:暂停游戏、重新开始游戏或返回主菜单。它在移动设备和桌面上都能流畅运行,并且添加游戏手柄控件同样简单——在 update() 函数中,我们检查当前状态是否为“暂停”——如果是,则启用相关操作

js
if (GamepadAPI.buttons.pressed("Start")) {
  this.managePause();
}

if (GamepadAPI.buttons.pressed("Back")) {
  this.stateBack();
}

类似地,当“游戏结束”状态处于活动状态时,我们可以允许用户重新开始游戏而不是继续游戏

js
if (GamepadAPI.buttons.pressed("Start")) {
  this.stateRestart();
}
if (GamepadAPI.buttons.pressed("Back")) {
  this.stateBack();
}

当游戏结束屏幕可见时,Start 按钮会重新开始游戏,而 Back 按钮会帮助我们返回主菜单。当游戏暂停时也是如此:Start 按钮会取消暂停游戏,Back 按钮会返回,就像之前一样。

总结

就是这样!我们已成功在游戏中实现了游戏手柄控件——尝试连接任何流行的控制器,如 XBox 360 控制器,亲身体验用游戏手柄躲避小行星并射击外星人有多有趣。

现在,我们可以继续探索新的、甚至更不寻常的 HTML 游戏控制方式,例如对着笔记本电脑挥手或对着麦克风大喊。