docs.rodeo

MDN Web Docs mirror

Using the Gamepad API

{{DefaultAPISidebar("Gamepad API")}} 

HTML provides the necessary components for rich, interactive game development. Technologies like <canvas>, WebGL, <audio>, and <video>, along with JavaScript implementations, support tasks that provide similar, if not the same, features as native code. The Gamepad API allows developers and designers to access and use gamepads and other game controllers.

The Gamepad API introduces new events on the {{ domxref("Window") }}  object for reading gamepad and controller (hereby referred to as gamepad) state. In addition to these events, the API also adds a {{ domxref("Gamepad") }}  object, which you can use to query the state of a connected gamepad, and a {{ domxref("navigator.getGamepads()") }}  method which you can use to get a list of gamepads known to the page.

Connecting to a gamepad

When a new gamepad is connected to the computer, the focused page first receives a {{ domxref("Window/gamepadconnected_event", "gamepadconnected") }}  event. If a gamepad is already connected when the page loaded, the {{ domxref("Window/gamepadconnected_event", "gamepadconnected") }}  event is dispatched to the focused page when the user presses a button or moves an axis.

[!NOTE] In Firefox, gamepads are only exposed to a page when the user interacts with one with the page visible. This helps prevent gamepads from being used for fingerprinting the user. Once one gamepad has been interacted with, other gamepads that are connected will automatically be visible.

You can use {{domxref("Window/gamepadconnected_event", "gamepadconnected")}}  like this:

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,
  );
});

Each gamepad has a unique ID associated with it, which is available on the event’s {{domxref("GamepadEvent.gamepad", "gamepad")}}  property.

Disconnecting a gamepad

When a gamepad is disconnected, and if a page has previously received data for that gamepad (e.g. {{ domxref("Window/gamepadconnected_event", "gamepadconnected") }} ), a second event is dispatched to the focused window, {{domxref("Window.gamepaddisconnected_event", "gamepaddisconnected")}} :

window.addEventListener("gamepaddisconnected", (e) => {
  console.log(
    "Gamepad disconnected from index %d: %s",
    e.gamepad.index,
    e.gamepad.id,
  );
});

The gamepad’s {{domxref("Gamepad.index", "index")}}  property will be unique per-device connected to the system, even if multiple controllers of the same type are used. The index property also functions as the index into the {{jsxref("Array")}}  returned by {{ domxref("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,
);

This previous example also demonstrates how the gamepad property can be held after the event has completed — a technique we will use for device state querying later.

Querying the Gamepad object

As you can see, the gamepad events discussed above include a gamepad property on the event object, which returns a {{ domxref("Gamepad") }}  object. We can use this in order to determine which gamepad (i.e., its ID) had caused the event, since multiple gamepads might be connected at once. We can do much more with the {{ domxref("Gamepad") }}  object, including holding a reference to it and querying it to find out which buttons and axes are being pressed at any one time. Doing so is often desirable for games or other interactive web pages that need to know the state of a gamepad now vs. the next time an event fires.

Performing such checks tends to involve using the {{ domxref("Gamepad") }}  object in conjunction with an animation loop (e.g., {{ domxref("Window.requestAnimationFrame","requestAnimationFrame") }} ), where developers want to make decisions for the current frame based on the state of the gamepad or gamepads.

The {{domxref("Navigator.getGamepads()")}}  method returns an array of all devices currently visible to the webpage, as {{ domxref("Gamepad") }}  objects (the first value is always null, so null will be returned if there are no gamepads connected.) This can then be used to get the same information. For example, the first code example above could be rewritten as shown below:

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,
  );
});

The {{ domxref("Gamepad") }}  object’s properties are as follows:

[!NOTE] The Gamepad object is available on the {{ domxref("Window/gamepadconnected_event", "gamepadconnected") }}  event rather than the {{ domxref("Window") }}  object itself, for security reasons. Once we have a reference to it, we can query its properties for information about the current state of the gamepad. Behind the scenes, this object will be updated every time the gamepad’s state changes.

Using button information

Let’s look at an example that displays connection information for one gamepad (it ignores subsequent gamepad connections) and allows you to move a ball around the screen using the four gamepad buttons on the right-hand side of the gamepad. You can view the demo live, and find the source code on GitHub.

To start with, we declare some variables: The gamepadInfo paragraph that the connection info is written into, the ball that we want to move, the start variable that acts as the ID for requestAnimation Frame, the a and b variables that act as position modifiers for moving the ball, and the shorthand variables that will be used for the {{ domxref("Window.requestAnimationFrame", "requestAnimationFrame()") }}  and {{ domxref("Window.cancelAnimationFrame", "cancelAnimationFrame()") }}  cross browser forks.

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

Next we use the {{domxref("Window/gamepadconnected_event", "gamepadconnected")}}  event to check for a gamepad being connected. When one is connected, we grab the gamepad using {{ domxref("Navigator.getGamepads()", "navigator.getGamepads()[0]") }} , print information about the gamepad into our gamepad info div, and fire the gameLoop() function that starts the whole ball movement process up.

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();
});

Now we use the {{domxref("Window/gamepaddisconnected_event", "gamepaddisconnected")}}  event to check if the gamepad is disconnected again. If so, we stop the {{DOMxRef("Window.requestAnimationFrame", "requestAnimationFrame()")}}  loop (see below) and revert the gamepad information back to what it was originally.

window.addEventListener("gamepaddisconnected", (e) => {
  gamepadInfo.textContent = "Waiting for gamepad.";

  cancelAnimationFrame(start);
});

Now on to the main game loop. In each execution of the loop we check if one of four buttons is being pressed; if so, we update the values of the a and b movement variables appropriately, then update the {{ cssxref("left") }}  and {{ cssxref("top") }}  properties, changing their values to the current values of a and b respectively. This has the effect of moving the ball around the screen.

After all this is done, we use our requestAnimationFrame() to request the next animation frame, running gameLoop() again.

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);
}

Complete example: Displaying gamepad state

This example shows how to use the {{domxref("Gamepad")}}  object, as well as the {{domxref("Window/gamepadconnected_event", "gamepadconnected")}}  and {{domxref("Window/gamepaddisconnected_event", "gamepaddisconnected")}}  events to display the state of all gamepads connected to the system. The example is based on a Gamepad demo, which has the source code available on 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);
}

Specifications

{{Specifications}} 

Browser compatibility

{{Compat}} 

In this article

View on MDN