--- title: 使用 Gamepad API slug: Web/API/Gamepad_API/Using_the_Gamepad_API tags: - 手柄 - 指南 - 控制器 - 游戏 - 进阶 translation_of: Web/API/Gamepad_API/Using_the_Gamepad_API ---

{{DefaultAPISidebar("Gamepad API")}}{{ SeeCompatTable() }}

HTML5 为丰富的交互式游戏开发引入了许多必要的组件。像 <canvas> 、WebGL、 <audio> 和 <video> 这样的技术,随着 JavaScript 的逐渐成熟,许多以前需要 native code 来实现的功能现在都可以实现了。Gamepad(手柄) API 是开发人员和设计者识别和使用游戏控制板和其他游戏控制器的一种方法。

Gamepad API 引入新的事件在 {{ domxref("Window") }} 对象中,来读取手柄和控制器(以下称“控制器”)的状态。除此之外,API还添加了一个 {{ domxref("Gamepad") }} 对象,你可以用它来查询已连接控制器的状态;还有一个 {{ domxref("navigator.getGamepads()") }} 方法,你可以使用它来获取页面已知的控制器列表。

连接控制器

当一个新的手柄连接到计算机时,焦点页面(当前页面)首先接收一个 {{ event("gamepadconnected") }} 事件。 如果在加载页面时已经连接了手柄,则会在用户按下某个按钮或移动坐标方向(axes)时触发焦点页面的 {{ event("gamepadconnected") }} 事件。

在 Firefox 中,控制器只会暴露给与用户产生交互的可见页面。这有助于防止控制器被用于获取用户的指纹。一旦有一个手柄与页面产生交互,那么其他连接的控制器将自动对页面可见。

你可以这样使用 {{ event("gamepadconnected") }} :

window.addEventListener("gamepadconnected", function(e) {
  console.log("控制器已连接于 %d 位: %s. %d 个按钮, %d 个坐标方向。",
    e.gamepad.index, e.gamepad.id,
    e.gamepad.buttons.length, e.gamepad.axes.length);
});

每个控制器都有一个与之关联的唯一ID,其在事件的 {{domxref("GamepadEvent.gamepad", "gamepad")}} 属性上可用。

断开控制器连接

当控制器断开连接时, 如果页面以前接收过该手柄的数据 (例如 {{ event("gamepadconnected") }}),那么第二个事件 {{ event("gamepaddisconnected") }} 将会分配至焦点页面:

window.addEventListener("gamepaddisconnected", function(e) {
  console.log("控制器已从 %d 位断开: %s",
    e.gamepad.index, e.gamepad.id);
});

即使使用相同类型的多个控制器,控制器的 {{domxref("Gamepad.index", "index")}} 属性都会是唯一的,每一个设备都有一个。index 属性还可充当 {{ domxref("Navigator.getGamepads()") }} 返回 {{jsxref("Array")}} 的索引。

var gamepads = {};

function gamepadHandler(event, connecting) {
  var gamepad = event.gamepad;
  // 注:
  // gamepad === navigator.getGamepads()[gamepad.index]

  if (connecting) {
    gamepads[gamepad.index] = gamepad;
  } else {
    delete gamepads[gamepad.index];
  }
}

window.addEventListener("gamepadconnected", function(e) { gamepadHandler(e, true); }, false);
window.addEventListener("gamepaddisconnected", function(e) { gamepadHandler(e, false); }, false);

上面的示例同时演示了在事件完成后如何保存 gamepad 属性,并在之后使用其查询设备状态。

查询 Gamepad 对象

正如你看到的,上面讨论的 gamepad 事件,包括事件对象上的 gamepad 属性,会返回一个 {{ domxref("Gamepad") }} 对象。因为可能同时连接不止一个控制器,所以我们可以使用它来确定是哪个控制器 (或者说 ID) 触发了事件。我们可以使用 {{ domxref("Gamepad") }} 对象做很多事,比如保留对象的引用并用其查询,以找出哪些按钮和摇杆在什么时候被按下了。相较于在下次触发,现在立即就可以获取控制器的状态对于游戏或其他交互式网页来说是一般是可取的。

开发者执行此类查询时往往涉及将 {{ domxref("Gamepad") }} 对象和一个动画循环 (例如 {{ domxref("Window.requestAnimationFrame","requestAnimationFrame") }})结合在一起,希望根据控制器的状态来对决定当前框架的行为。

{{ domxref("Navigator.getGamepads()") }} 方法返回当前对网页可见的所有设备的数组,{{ domxref("Gamepad") }} 对象 (初始值始终为 null,所以当没有控制器连接的时候将会返回 null )也一样可以用来获取的控制器信息。例如下面将会重写开头的第一个例子:

window.addEventListener("gamepadconnected", function(e) {
  var gp = navigator.getGamepads()[e.gamepad.index];
  console.log("控制器已连接于 %d 位: %s. %d 个按钮, %d 个坐标方向。",
    gp.index, gp.id,
    gp.buttons.length, gp.axes.length);
});

以下是 {{ domxref("Gamepad") }} 对象的属性说明:

注:出于安全原因,Gamepad 对象在 {{ event("gamepadconnected") }} 事件上可用而在 {{ domxref("Window") }} 对象上不可用。一旦我们得到了对它的引用,我们就可以获取其属性以了解有关控制器当前状态的信息。在后台,此对象将会在控制器状态更改时更新。

使用按键信息

让我们看一个简单的示例:显示一个控制器的连接信息 (忽略后续连接的控制器) ,并让您使用控制器右侧的四个操作按钮移动屏幕上一个球。你可以 查看在线演示,并可在 Github 上找到源代码

我们首先声明一些变量:gamepadInfo 用于写入连接信息的段落;ball 是我们希望控制移动的球;start 作为 requestAnimation Frame ID 的初始变量; a 和 b 变量作为球位置动量,并且变量会被用于 {{ domxref("Window.requestAnimationFrame", "requestAnimationFrame()") }} 和 {{ domxref("Window.cancelAnimationFrame", "cancelAnimationFrame()") }} 。(?)

var gamepadInfo = document.getElementById("gamepad-info");
var ball = document.getElementById("ball");
var start;
var a = 0;
var b = 0;

接下来我们使用 {{ event("gamepadconnected") }} 世界来检查控制器是否连接。当有一个控制连接时,我们就使用 {{ domxref("Navigator.getGamepads()") }}[0] 来抓取,输出控制器信息到我们“控制器信息”的 div 里,并开始 gameLoop() 函数来启动球的运动进程。

window.addEventListener("gamepadconnected", function(e) {
  var gp = navigator.getGamepads()[e.gamepad.index];
  gamepadInfo.innerHTML = "控制器已连接于 " + gp.index + " 位:" + gp.id + "。它有 " + gp.buttons.length + " 个按钮和 " + gp.axes.length + " 个坐标方向。";

  gameLoop();
});

现在我们再使用 {{ event("gamepaddisconnected") }} 事件来检查如果控制器断开的情况。如果断开了,我们会停止 {{ domxref("Window.requestAnimationFrame", "requestAnimationFrame()") }} 循环 (见下方) 并重置控制器信息到原来的样子。

window.addEventListener("gamepaddisconnected", function(e) {
  gamepadInfo.innerHTML = "正在等待控制器。";

  cancelRequestAnimationFrame(start);
});

Chrome 在这里有些区别。它没有在变量内不断的更新存储控制器的最后状态,而存储只是当时的一个快照,所以你要在 Chrome 中做到同样的事情的话,就需要不断地轮询,然后在可用的时候只能在代码中使用 {{ domxref("Gamepad") }} 对象来达到目的。我们下面用 {{ domxref("Window.setInterval()") }}实现了7;一旦控制器的可以输出了,游戏循环就会开始,可以使用 {{ domxref("Window.clearInterval()") }} 清除定时循环。请注意在较旧版本的 Chrome 中实现 {{ domxref("Navigator.getGamepads()") }} 需要加上  webkit 前缀。我们尝试对两种前缀版本都进行监测和处理,以向后兼容。

var interval;

if (!('ongamepadconnected' in window)) {
  // 没有控制器事件可用,则开始轮询。
  interval = setInterval(pollGamepads, 500);
}

function pollGamepads() {
  var gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads : []);
  for (var i = 0; i < gamepads.length; i++) {
    var gp = gamepads[i];
    if (gp) {
      gamepadInfo.innerHTML = "控制器已连接于 " + gp.index + " 位:" + gp.id +
        "。它有 " + gp.buttons.length + " 个按钮和 " + gp.axes.length + " 个坐标方向。";
      gameLoop();
      clearInterval(interval);
    }
  }
}

现在看主要的游戏循环。在每次我们所需的四个按钮被按下的时候进行处理。如果被按下了我就会适当地更新动量变量  a 和 b 的值,然后分别用 ab 的值更新球的 {{ cssxref("left") }} 和 {{ cssxref("top") }} 属性。这样就可以在屏幕上移动数的位置了。在当前版本的 Chrome 中 (版本 34) button 的值是存储为数组的两个值,而不是 {{ domxref("GamepadButton") }} 对象。此问题已于开发者版本修复了。

当这些处理好之后,我们使用我们的 requestAnimationFrame() 来请求下一个动画帧,然后运行 gameLoop() 再继续执行。

function buttonPressed(b) {
  if (typeof(b) == "object") {
    return b.pressed;
  }
  return b == 1.0;
}

function gameLoop() {
  var gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads : []);
  if (!gamepads) {
    return;
  }

  var gp = gamepads[0];
  if (buttonPressed(gp.buttons[0])) {
    b--;
  } else if (buttonPressed(gp.buttons[2])) {
    b++;
  }
  if (buttonPressed(gp.buttons[1])) {
    a++;
  } else if (buttonPressed(gp.buttons[3])) {
    a--;
  }

  ball.style.left = a * 2 + "px";
  ball.style.top = b * 2 + "px";

  start = requestAnimationFrame(gameLoop);
}

使用坐标方向(axes)信息

待讨论 (除了一个用 axes[i] 一个用 button[i].value ,其他基本一样,Firefox 与 Chrome均是。)

完整的例子:显示控制器状态

这个例子展示了怎样使用 {{ domxref("Gamepad") }} 对象,还有 {{ event("gamepadconnected") }} 和 {{ event("gamepaddisconnected") }} 事件显示所有已连接到系统的控制器的状态。你可以查看在线演示并且可在Github上看到完整的源代码

var haveEvents = 'ongamepadconnected' in window;
var controllers = {};

function connecthandler(e) {
  addgamepad(e.gamepad);
}

function addgamepad(gamepad) {
  controllers[gamepad.index] = gamepad;

  var d = document.createElement("div");
  d.setAttribute("id", "controller" + gamepad.index);

  var t = document.createElement("h1");
  t.appendChild(document.createTextNode("控制器:" + gamepad.id));
  d.appendChild(t);

  var b = document.createElement("div");
  b.className = "buttons";
  for (var i = 0; i < gamepad.buttons.length; i++) {
    var e = document.createElement("span");
    e.className = "button";
    //e.id = "b" + i;
    e.innerHTML = i;
    b.appendChild(e);
  }

  d.appendChild(b);

  var a = document.createElement("div");
  a.className = "axes";

  for (var i = 0; i < gamepad.axes.length; i++) {
    var p = document.createElement("progress");
    p.className = "axis";
    //p.id = "a" + i;
    p.setAttribute("max", "2");
    p.setAttribute("value", "1");
    p.innerHTML = i;
    a.appendChild(p);
  }

  d.appendChild(a);

  // 见 https://github.com/luser/gamepadtest/blob/master/index.html
  var start = document.getElementById("start");
  if (start) {
    start.style.display = "none";
  }

  document.body.appendChild(d);
  requestAnimationFrame(updateStatus);
}

function disconnecthandler(e) {
  removegamepad(e.gamepad);
}

function removegamepad(gamepad) {
  var d = document.getElementById("controller" + gamepad.index);
  document.body.removeChild(d);
  delete controllers[gamepad.index];
}

function updateStatus() {
  if (!haveEvents) {
    scangamepads();
  }

  var i = 0;
  var j;

  for (j in controllers) {
    var controller = controllers[j];
    var d = document.getElementById("controller" + j);
    var buttons = d.getElementsByClassName("button");

    for (i = 0; i < controller.buttons.length; i++) {
      var b = buttons[i];
      var val = controller.buttons[i];
      var pressed = val == 1.0;
      if (typeof(val) == "object") {
        pressed = val.pressed;
        val = val.value;
      }

      var pct = Math.round(val * 100) + "%";
      b.style.backgroundSize = pct + " " + pct;

      if (pressed) {
        b.className = "button pressed";
      } else {
        b.className = "button";
      }
    }

    var axes = d.getElementsByClassName("axis");
    for (i = 0; i < controller.axes.length; i++) {
      var a = axes[i];
      a.innerHTML = i + ": " + controller.axes[i].toFixed(4);
      a.setAttribute("value", controller.axes[i] + 1);
    }
  }

  requestAnimationFrame(updateStatus);
}

function scangamepads() {
  var gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);
  for (var i = 0; i < gamepads.length; i++) {
    if (gamepads[i]) {
      if (gamepads[i].index in controllers) {
        controllers[gamepads[i].index] = gamepads[i];
      } else {
        addgamepad(gamepads[i]);
      }
    }
  }
}


window.addEventListener("gamepadconnected", connecthandler);
window.addEventListener("gamepaddisconnected", disconnecthandler);

if (!haveEvents) {
  setInterval(scangamepads, 500);
}

规范

规范 状态 备注
{{SpecName("Gamepad", "#gamepad-interface", "Gamepad")}} {{Spec2("Gamepad")}} Initial defintion

浏览器兼容性

{{Compat("api.Gamepad")}}