Comment utiliser une manette en JavaScript ?

Vous programmez un jeu pour navigateur web avec JavaScript et vous voulez ajouter un contrôle à la manette ? Rien de plus simple !

Article publié le 16/12/2024, dernière mise à jour le 16/12/2024

Les navigateurs modernes intègrent une API pour gérer les contrôleurs de jeu (les manettes) pour que vous puissiez utiliser ces dernières dans vos applications web !

L’API est très simple à utiliser, mais il faut bien comprendre son fonctionnement pour ne pas faire d’erreurs.

La documentation officielle de cette API est disponible chez Mozilla !

Détection de la manette

La première étape est de détecter si une manette est déjà connectée à la machine, et de la stocker dans une variable.

navigator.getGamepads() permet de lister toutes les manettes connectées !

Ici, si au moins une manette est connectée, alors on l’utilise comme contrôleur par défaut :

let mainGamepad = null;

function tryLoadDefaultGamepad(){
    const gamepads = navigator.getGamepads();
    if(gamepads.length > 0){
        mainGamepad = gamepads[0];
    } else {
        mainGamepad = null;
        console.log("[DEBUG] Gamepad : 0 connected");
    }
}

function init(){
    tryLoadDefaultGamepad();
}

init();

Attention : Firefox et Chrome ne détecte une manette que lorsqu’il y a une première interaction de la manette avec la page (appui sur un bouton ou joystick par exemple) !

Le code ci-dessus ne détectera donc aucune manette au chargement de la page.

Pour que notre détection soit complète, il va falloir écouter les évènements gamepadconnected et gamepaddisconnected et agir en conséquence :

window.addEventListener("gamepadconnected", (e) => {
  const index = e.gamepad.index;
  const gamepads = navigator.getGamepads();

  // chaque nouvelle manette branchée deviendra la principale
  mainGamepad = gamepads[index];
  console.log(`[DEBUG] Gamepad : Id ${e.gamepad.index} connected`);
});

window.addEventListener("gamepaddisconnected", (e) => {
  if(e.gamepad.index === mainGamepad.index){
    console.log(`[DEBUG] Gamepad : Id ${e.gamepad.index} disconnected`);
    tryLoadDefaultGamepad();
  }
});

Avec le code ci-dessus au complet, votre page est désormais capable de détecter une manette !

Maintenant voyons comment utiliser notre manette !

Gérer les boutons et joysticks

Comme il existe beaucoup de contrôleurs de jeu différents, avec plus ou moins de boutons, des joysticks, etc… Il est en général recommandé de créer une interface (ou un objet) qui va représenter les contrôles disponible dans notre jeu.

Ici on a choisi de n’ajouter que deux actions par simplicité, ce qui rend compatibles des manettes à deux boutons ou plus.

let controls = {
  top: false,
  left: false,
  bottom: false,
  right: false,
  action1: false,
  action2: false
};

Une fois que l’on a la représentation de nos contrôles, il va falloir relier ces contrôles aux interactions avec notre manette. Pour cela nous allons utiliser les objets buttons et axes fournis par l’API pour chaque manette :

const joystickSensitivity = 0.5;

function updateControlsValues(){
    if (mainGamepad) {
        const buttons = mainGamepad.buttons;
        const axes = mainGamepad.axes;

        // La sensibilité permet d'éviter les problèmes de "drift" des joysticks
        controls.top = axes[1] < -joystickSensitivity || buttons[12].pressed;
        controls.bottom = axes[1] > joystickSensitivity || buttons[13].pressed;
        controls.left = axes[0] < -joystickSensitivity || buttons[14].pressed;
        controls.right = axes[0] > joystickSensitivity || buttons[15].pressed;
        controls.action1 = buttons[0].pressed;
        controls.action2 = buttons[1].pressed;
    }
}

Vous l’aurez compris, l’objet axes représente les axes (horizontal et vertical) de notre joystick

Et pour le reste :

  • buttons[0] représente le bouton A
  • buttons[1] représente le bouton B
  • buttons[12] à [15] représentent la croix directionnelle

Mais maintenant qu’il est possible de mettre à jour l’état des contrôles de notre jeu, en fonction de la manette, il va falloir faire cette mise à jour régulièrement.

On va donc mettre en place une boucle infinie, qui sera déclenchée dans notre fonction init() et appelée à interval régulier grâce à la fonction requestAnimationFrame :

function gameLoop(){
  updateControlsValues();
  console.log("[DEBUG] Controls :", controls);
  requestAnimationFrame(gameLoop);
}

function init(){
    tryLoadDefaultGamepad();
    gameLoop();
}

init();

Pourquoi faire une boucle ?

On peut se demander pourquoi utiliser une boucle pour vérifier l’état de la manette à interval régulier, plutôt que d’écouter un évènement lorsqu’un bouton est appuyé.

Et la question est légitime, surtout en JavaScript ou tout est basé sur des évènements.

Mais voilà, la raison est simplement que : l’API ne fourni aucun évènement lors d’un appui sur un bouton, ou lors du mouvement d’un joystick.

Cela peut paraitre bizarre, mais cela vient d’une contrainte technique avec l’électronique de la manette. Les joysticks ne sont pas des capteurs numériques, mais analogique.

Cela signifie que le moindre changement dans la position du joystick vient modifier la valeur.

Et des micro-mouvements, il y en a des dizaines par secondes ! Si JavaScript devait envoyer un évènement à chaque changement de valeur du joystick, les performances de la page web seraient grandement impactées.

Le code complet

Voici l’intégralité du code de l’article que vous pouvez copier-coller à souhait !

Le programme est strictement similaire, mais l’organisation a été légèrement améliorée.

let mainGamepad = null;
const joystickSensitivity = 0.5;
let controls = {
	  top: false,
	  left: false,
	  bottom: false,
	  right: false,
	  action1: false,
	  action2: false
};

window.addEventListener("gamepadconnected", (e) => {
	  const index = e.gamepad.index;
	  const gamepads = navigator.getGamepads();
	  // chaque nouvelle manette branchée deviendra la principale
	  mainGamepad = gamepads[index];
	  console.log(`[DEBUG] Gamepad : Id ${e.gamepad.index} connected`);
});

window.addEventListener("gamepaddisconnected", (e) => {
	  if(e.gamepad.index === mainGamepad.index){
		    console.log(`[DEBUG] Gamepad : Id ${e.gamepad.index} disconnected`);
		    tryLoadDefaultGamepad();
	  }
});

function tryLoadDefaultGamepad(){
    const gamepads = navigator.getGamepads();
    if(gamepads.length > 0){
        mainGamepad = gamepads[0];
    } else {
        mainGamepad = null;
        console.log("[DEBUG] Gamepad : 0 connected");
    }
}

function updateControlsValues(){
    if (mainGamepad) {
        const buttons = mainGamepad.buttons;
        const axes = mainGamepad.axes;
        // Sensitivity threshold for joysticks
        controls.top = axes[1] < -joystickSensitivity || buttons[12].pressed;
        controls.bottom = axes[1] > joystickSensitivity || buttons[13].pressed;
        controls.left = axes[0] < -joystickSensitivity || buttons[14].pressed;
        controls.right = axes[0] > joystickSensitivity || buttons[15].pressed;
        controls.action1 = buttons[0].pressed;
        controls.action2 = buttons[1].pressed;
    }
}

function gameLoop(){
	  updateControlsValues();
	  console.log("[DEBUG] Controls :", controls);
	  requestAnimationFrame(gameLoop);
}

function init(){
    tryLoadDefaultGamepad();
    gameLoop();
}

init();

Vous avez terminé l'article ?

Commentaires (0)

pour laisser un commentaire

Aucun commentaire pour l'instant