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 boutonA
buttons[1]
représente le boutonB
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();
Aucun commentaire pour l'instant