Utiliser l'API Gamepad
======================

HTML5 a introduit de nombreuses briques technologiques qui permettent le développement de jeux interactifs. Les fonctionnalités offertes par `<canvas>`, WebGL, `<audio>`, et `<video>`, ainsi que les API JavaScript correspondantes, ont suffisamment gagné en maturité pour réaliser des tâches qui nécessitaient auparavant du code natif. L'API *Gamepad* est un outil qui permet d'accéder et d'utiliser les manettes et autres contrôleurs de jeux.

L'API *Gamepad* introduit de nouveaux évènements sur l'objet `Window` qui permettent de lire l'état de la manette. En plus de ces évènements, l'API ajoute également un objet `Gamepad`, qui permet de connaître l'état d'une manette connectée et une méthode `navigator.getGamepads()` qu'on peut utiliser pour obtenir la liste des manettes connues sur la page.

Connecter une manette
---------------------

Lorsqu'une nouvelle manette est connectée à l'ordinateur, la page qui a le focus reçoit d'abord un évènement `gamepadconnected`. Si une manette est déjà connectée lorsque la page est chargée, l'évènement `gamepadconnected` est émis sur la page lorsque la personne appuie sur un bouton ou déplace un axe.

**Note :** Dans Firefox, les manettes sont uniquement exposées à la page après qu'il y a eu une interaction de la personne avec la page. Cela permet d'éviter à ce que les manettes soient utilisées pour créer une empreinte, de faciliter le pistage. Une fois qu'une manette a interagi avec la page, les autres manettes connectées seront automatiquement visibles.

On peut utiliser `gamepadconnected` comme ceci :


```js
window.addEventListener("gamepadconnected", function (e) {
  console.log(
    "Manette connectée à l'indice %d : %s. %d boutons, %d axes.",
    e.gamepad.index,
    e.gamepad.id,
    e.gamepad.buttons.length,
    e.gamepad.axes.length,
  );
});
```


Chaque manette dispose d'un identifiant unique qui lui est associé et qui est disponible via la propriété `gamepad` (en-US) de l'évènement.

Déconnecter une manette
-----------------------

Lorsqu'une manette est déconnectée et si la page avait déjà reçu des données pour cette manette (par exemple avec `gamepadconnected`), un deuxième évènement est envoyé sur la fenêtre, `gamepaddisconnected` :


```js
window.addEventListener("gamepaddisconnected", function (e) {
  console.log(
    "Manette déconnectée à l'indice %d : %s",
    e.gamepad.index,
    e.gamepad.id,
  );
});
```


La propriété `index` (en-US) de l'objet porté par `gamepad` sera unique pour chaque appareil connecté au système, même si plusieurs manettes du même type sont utilisées. La propriété `index` fonctionne également comme l'indice qui peut être utilisé pour parcourir le tableau (`Array`) renvoyé par la méthode `Navigator.getGamepads()`.


```js
let gamepads = {};

function gamepadHandler(event, connecting) {
  let gamepad = event.gamepad;
  // Note :
  // 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,
);
```


L'exemple qui précède illustre également comment la propriété `gamepad` peut être retenue après la fin de l'évènement. Nous utiliserons cette technique plus tard pour faire des requêtes sur l'état de l'appareil.

Utiliser l'objet `Gamepad`
--------------------------

Comme vous pouvez le voir, les évènements `gamepad` présentés ci-avant incluent une propriété `gamepad` rattachée à l'objet de l'évènement. Cette propriété fournit un objet `Gamepad`. On peut utiliser cet objet afin de déterminer la manette qui a causé l'évènement (avec son identifiant), car plusieurs manettes pourraient être connectées simultanément. On peut faire bien plus avec cet objet `Gamepad`, y compris garder une référence vers celui-ci et l'utiliser pour déterminer les boutons et axes utilisés à tout moment. Une telle utilisation est souvent nécessaire pour les jeux ou les pages interactives lorsqu'il faut connaître l'état de la manette à l'instant T et l'état dans lequel elle sera au moment du prochain évènement.

Généralement, ces opérations sont effectuées en utilisant un objet `Gamepad` avec une boucle d'animation (par exemple avec `requestAnimationFrame`), où on peut développer la logique du jeu afin de choisir quoi faire pour la *frame* courante selon l'état de la (ou des) manette(s).

La méthode `Navigator.getGamepads()` renvoie un tableau de l'ensemble des appareils qui sont actuellement visibles de la page web sous la forme d'objets `Gamepad` (la première valeur vaut toujours `null`, et c'est `null` qui est renvoyé s'il n'y a pas de manettes connectées). On peut l'utiliser pour obtenir les mêmes informations. Ainsi, le premier exemple de code ci-avant pourrait être réécrit de la façon suivante :


```js
window.addEventListener("gamepadconnected", function (e) {
  var gp = navigator.getGamepads()[e.gamepad.index];
  console.log(
    "Manette connectée à l'indice %d : %s. %d boutons, %d axes.",
    gp.index,
    gp.id,
    gp.buttons.length,
    gp.axes.length,
  );
});
```


Les propriétés d'un objet `Gamepad` sont :

`id`

Une chaîne de caractères contenant des informations sur la manette. Le format n'est pas spécifié de façon stricte. Pour Firefox, ce sera trois informations séparées par des tirets (`-`) : deux chaînes de caractères avec 4 chiffres hexadécimaux indiquant l'éditeur du pilote USB et l'identifiant produit de la manette puis le nom de la manette fourni par le pilote. Ces informations doivent permettre de trouver la correspondance des touches de l'appareil et de fournir des retours pertinents à la personne qui utilise la manette.

`index`

Un entier, unique pour chaque manette actuellement connectée au système. Elle peut être utilisée afin de distinguer une manette parmi plusieurs. On notera que déconnecter un appareil puis en reconnecter un nouveau pourra réutiliser un des indices précédemment utilisé.

`mapping`

Une chaîne de caractères qui indique si le navigateur a adapté les contrôles de l'appareil sur une disposition connue. Il existe actuellement une seule disposition prise en charge, la manette standard. Si le navigateur est capable de faire correspondre les contrôles de l'appareil avec cette disposition, la propriété `mapping` vaudra la chaîne de caractères `standard`.

`connected`

Un booléen qui indique si la manette est toujours connectée au système (`true` si c'est le cas, `false` sinon).

`buttons`

Un tableau d'objets `GamepadButton` (en-US) représentant les boutons présents sur l'appareil. Chaque objet `GamepadButton` (en-US) aura deux propriétés, `pressed` et `value` :

`pressed`

Un booléen qui indique si le bouton est actuellement enfoncé/appuyé (`true`) ou relâché (`false`).

`value`

Un nombre flottant utilisée pour représenter la valeur des boutons analogiques comme les gâchettes. Les valeurs sont normalisées sur l'intervalle `[0.0, 1.0]`, avec `0.0` qui représente un bouton sur lequel il n'y a aucune pression et `1.0` qui représente un bouton complètement appuyé/enfoncé.


`axes`

Un tableau qui représente les contrôles où des axes sont présents sur l'appareil (par exemple les joysticks analogiques). Chaque élément du tableau est une valeur flottante sur l'intervalle `[-1.0, 1.0]` qui représente la position sur un axe, de la valeur la plus faible (`-1.0`), à la valeur la plus haute (`1.0`).

`timestamp`

: Un objet `DOMHighResTimeStamp` indiquant le dernier instant auquel les données des manettes ont été mises à jour. Cela permet de déterminer si les données fournies par `axes` et `button` ont été mises à jour par le matériel. Cette valeur doit être relative à l'attribut `navigationStart` de l'interface `PerformanceTiming`. Les valeurs augmentent de façon monotone, ce qui signifie qu'on peut les comparer pour déterminer l'ordre des mises à jour (les valeurs les plus récentes seront toujours supérieures aux valeurs antérieures). Cette propriété n'est actuellement pas prise en charge par Firefox.


**Note :** L'objet `Gamepad` est disponible sur l'évènement `gamepadconnected` plutôt que sur l'objet `Window` pour des raisons de sécurité. Une fois qu'une référence est obtenue, on peut consulter ses propriétés pour obtenir des informations à propos de la manette. Sous le capot, cet objet est mis à jour à chaque fois que l'état de la manette change.

### Utiliser les informations des boutons

Prenons un exemple simple, qui affiche les informations de connexion d'une seule manette (on ignore ici les connexions des manettes qui auraient lieu après) et qui permet de déplacer une balle sur l'écran à l'aide des quatre boutons situés sur le côté droit de la manette. Vous pouvez voir la démonstration en direct, ainsi que le code source correspondant sur GitHub.

Pour commencer, on déclare quelques variables :

* `gamepadInfo` qui correspond au paragraphe dans lequel on écrit les informations de connexion
* `ball` qui est la balle qu'on souhaite déplacer
* `start` qui est la variable utilisée comme identifiant pour `requestAnimationFrame()`
* `a` et `b` qui serviront de modificateurs de position pour la balle

On a également (dans le code, mais pas ici par souci de concision), des variables pour faciliter l'accès à `requestAnimationFrame()` et `cancelAnimationFrame()` dans les différents navigateurs.


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


Ensuite, on utilise l'évènement `gamepadconnected` pour vérifier si une manette est connectée. Lorsqu'une manette est connectée, on récupère l'objet correspondant avec `Navigator.getGamepads()[0]` et on affiche les informations dans l'élément `div` correspondant puis on déclenche la fonction `gameLoop()` qui démarre la procédure pour le mouvement de la balle.


```js
window.addEventListener("gamepadconnected", function (e) {
  let gp = navigator.getGamepads()[e.gamepad.index];
  gamepadInfo.innerHTML =
    "Manette connectée à l'indice" +
    gp.index +
    " : " +
    gp.id +
    ". Elle a " +
    gp.buttons.length +
    " boutons et " +
    gp.axes.length +
    " axes.";

  gameLoop();
});
```


On gère également l'évènement `gamepaddisconnected` pour vérifier si la manette a été déconnectée. Si c'est le cas, on arrête la boucle `requestAnimationFrame()` (voir après) et on réinitialise les informations affichées sur la manette.


```js
window.addEventListener("gamepaddisconnected", function (e) {
  gamepadInfo.innerHTML = "En attente d'une manette.";

  cancelRequestAnimationFrame(start);
});
```


Pour Chrome, les choses sont différentes. Le navigateur ne stocke qu'un instantané de l'état de la manette et il faut donc continuellement rafraîchir l'état connu avant d'utiliser l'objet `Gamepad` lorsque celui-ci devient disponible. Dans le code qui suit, on fait cela avec `setInterval()` (en-US). Une fois que l'objet est disponible, les informations sont affichées, la boucle de jeu commence et l'intervalle de répétition pour l'état de la manette est arrêté avec `clearInterval`. Pour les versions les plus anciennes de Chrome, `Navigator.getGamepads()` est implémentée avec un préfixe `webkit`. Pour une rétro-compatibilité, on essaie ici de détecter et de gérer les deux cas (avec et sans préfixe).


```js
let interval;

if (!("ongamepadconnected" in window)) {
  // Les évènements gamepad ne sont pas disponibles
  // On boucle pour connaître l'état à la place
  interval = setInterval(pollGamepads, 500);
}

function pollGamepads() {
  let gamepads = navigator.getGamepads
    ? navigator.getGamepads()
    : navigator.webkitGetGamepads
      ? navigator.webkitGetGamepads
      : [];
  for (let i = 0; i < gamepads.length; i++) {
    let gp = gamepads[i];
    if (gp) {
      gamepadInfo.innerHTML =
        "Manette connectée à l'indice " +
        gp.index +
        " : " +
        gp.id +
        ". Elle a " +
        gp.buttons.length +
        " boutons et " +
        gp.axes.length +
        " axes.";
      gameLoop();
      clearInterval(interval);
    }
  }
}
```


Voyons maintenant la boucle de jeu principale. Pour chaque exécution de la boucle, on regarde si un des quatre boutons est pressé. Si c'est le cas, on met à jour les variables de mouvement `a` et `b` de façon correspondante. Ensuite, on met à jour les propriétés CSS `left` et `top` avec les valeurs respectives de `a` et `b`. En pratique, cela aura pour effet de déplacer la balle sur l'écran.

Une fois que tout ça est fait, on utilise `requestAnimationFrame()` pour passer à la *frame* suivante, où on exécute à nouveau `gameLoop()`.


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

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

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

Exemple complet : afficher l'état de la manette
-----------------------------------------------

Cet exemple illustre comment utiliser l'objet `Gamepad` ainsi que les évènements `gamepadconnected` et `gamepaddisconnected` afin d'afficher l'état de l'ensemble des manettes connectées au système. Vous pouvez voir la démonstration fonctionner et consulter le code source complet sur GitHub.


```js
let haveEvents = "ongamepadconnected" in window;
let controllers = {};

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

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

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

  let t = document.createElement("h1");
  t.appendChild(document.createTextNode("gamepad: " + gamepad.id));
  d.appendChild(t);

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

  d.appendChild(b);

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

  for (let i = 0; i < gamepad.axes.length; i++) {
    let 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);

  // Voir https://github.com/luser/gamepadtest/blob/master/index.html
  let 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) {
  let d = document.getElementById("controller" + gamepad.index);
  document.body.removeChild(d);
  delete controllers[gamepad.index];
}

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

  let i = 0;
  let j;

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

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

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

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

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

  requestAnimationFrame(updateStatus);
}

function scangamepads() {
  let gamepads = navigator.getGamepads
    ? navigator.getGamepads()
    : navigator.webkitGetGamepads
      ? navigator.webkitGetGamepads()
      : [];
  for (let 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);
}
```

Spécifications
--------------

| Specification |
| --- |
| Gamepad # gamepad-interface |
| Gamepad Extensions # partial-gamepad-interface |

Compatibilité des navigateurs
-----------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.