Lorsque vous branchez un contrôleur, vous écrasez des boutons, déplacez les sticks, appuyez sur les gâchettes… et en tant que développeur, vous ne voyez rien de tout cela. Le navigateur le récupère, bien sûr, mais à moins que vous n’enregistriez des numéros dans la console, il est invisible. C’est le casse-tête de l’API Gamepad. Cela existe depuis des années et c’est en fait assez puissant. Vous pouvez lire les boutons, les bâtons, les déclencheurs, les travaux. Mais la plupart des gens n’y touchent pas. Pourquoi? Parce qu'il n'y a pas de retour. Aucun panneau dans les outils de développement. Il n’existe aucun moyen clair de savoir si le contrôleur fait ce que vous pensez. C'est comme voler à l'aveugle. Cela m'a suffisamment dérangé pour créer un petit outil : Gamepad Cascade Debugger. Au lieu de regarder la sortie de la console, vous obtenez une vue interactive en direct du contrôleur. Appuyez sur quelque chose et il réagit sur l'écran. Et avec CSS Cascade Layers, les styles restent organisés, il est donc plus propre à déboguer. Dans cet article, je vais vous montrer pourquoi le débogage des contrôleurs est si pénible, comment CSS aide à les nettoyer et comment vous pouvez créer un débogueur visuel réutilisable pour vos propres projets.
Même si vous parvenez à tous les enregistrer, vous vous retrouverez rapidement avec du spam de console illisible. Par exemple : [0,0,1,0,0,0,5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]
Pouvez-vous dire sur quel bouton vous avez appuyé ? Peut-être, mais seulement après avoir fatigué vos yeux et manqué quelques entrées. Donc non, le débogage n’est pas facile lorsqu’il s’agit de lire des entrées. Problème 3 : manque de structure Même si vous créez un visualiseur rapide, les styles peuvent rapidement devenir compliqués. Les états par défaut, actif et de débogage peuvent se chevaucher, et sans structure claire, votre CSS devient fragile et difficile à étendre. Les couches CSS Cascade peuvent vous aider. Ils regroupent les styles en « calques » classés par priorité, de sorte que vous arrêtez de lutter contre la spécificité et de deviner : « Pourquoi mon style de débogage ne s'affiche-t-il pas ? Au lieu de cela, vous entretenez des préoccupations distinctes :
Base : L’apparence initiale standard du contrôleur. Actif : mise en surbrillance des boutons enfoncés et des sticks déplacés. Débogage : superpositions pour les développeurs (par exemple, affichages numériques, guides, etc.).
Si nous devions définir des calques en CSS selon cela, nous aurions : /* priorité la plus basse à la plus élevée */ @layer base, actif, débogage ;
@couche de base { /* ... */ }
@couche active { /* ... */ }
débogage @layer { /* ... */ }
Étant donné que chaque couche s'empile de manière prévisible, vous savez toujours quelles règles gagnent. Cette prévisibilité rend le débogage non seulement plus facile, mais réellement gérable. Nous avons abordé le problème (entrée invisible et désordonnée) et l'approche (un débogueur visuel construit avec Cascade Layers). Nous allons maintenant parcourir le processus étape par étape pour créer le débogueur. Le concept du débogueur Le moyen le plus simple de rendre visible une entrée masquée est de simplement la dessiner sur l’écran. C'est ce que fait ce débogueur. Les boutons, les déclencheurs et les joysticks reçoivent tous un visuel.
Appuyez sur A : un cercle s'allume. Poussez le bâton : le cercle glisse. Appuyez sur une gâchette à mi-chemin : une barre se remplit à mi-chemin.
Désormais, vous ne regardez plus les 0 et les 1, mais vous regardez réellement le contrôleur réagir en direct. Bien sûr, une fois que vous commencez à empiler des états comme par défaut, enfoncé, des informations de débogage, peut-être même un mode d'enregistrement, le CSS commence à devenir plus grand et plus complexe. C’est là que les couches en cascade s’avèrent utiles. Voici un exemple simplifié : @couche de base { .bouton { arrière-plan : #222 ; rayon de bordure : 50 % ; largeur : 40px ; hauteur : 40px ; } }
@couche active { .bouton.appuyé { arrière-plan : #0f0 ; /* vert vif */ } }
débogage @layer { .button::après { contenu : attr (valeur de données); taille de police : 12 px ; couleur : #fff ; } }
L'ordre des couches est important : base → actif → débogage.
la base dessine le contrôleur. les poignées actives sont enfoncées. le débogage lance les superpositions.
Le diviser ainsi signifie que vous ne menez pas d’étranges guerres de spécificité. Chaque couche a sa place et vous savez toujours ce qui gagne. Le construire Mettons d'abord quelque chose à l'écran. Il n’est pas nécessaire que cela soit beau, il suffit d’exister pour que nous ayons quelque chose avec lequel travailler.
Débogueur en cascade pour manette de jeu
Ce ne sont littéralement que des boîtes. Pas encore passionnant, mais cela nous donne des poignées à saisir plus tard avec CSS et JavaScript. D'accord, j'utilise des couches en cascade ici car cela permet de garder les choses organisées une fois que vous ajoutez plus d'états. Voici une passe approximative :
/* =================================== CONFIGURATION DES COUCHES EN CASCADE L'ordre compte : base → actif → débogage =================================== */
/* Définir l'ordre des couches à l'avance */ @layer base, actif, débogage ;
/* Couche 1 : Styles de base - apparence par défaut */ @couche de base { .bouton { arrière-plan : #333 ; rayon de bordure : 50 % ; largeur : 70px ; hauteur : 70px ; affichage : flexible ; justifier-contenu : centre ; align-items : centre ; }
.pause { largeur : 20 px ; hauteur : 70px ; arrière-plan : #333 ; affichage : bloc en ligne ; } }
/* Couche 2 : États actifs - gère les boutons enfoncés */ @couche active { .bouton.actif { arrière-plan : #0f0 ; /* Vert vif lorsque vous appuyez dessus */ transformation : échelle (1.1); /* Agrandit légèrement le bouton */ }
.pause.active { arrière-plan : #0f0 ; transformation : scaleY(1.1); /* S'étire verticalement lorsqu'on appuie dessus */ } }
/* Couche 3 : superpositions de débogage - informations sur le développeur */ débogage @layer { .button::après { contenu : attr (valeur de données); /* Affiche la valeur numérique */ taille de police : 12 px ; couleur : #fff ; } }
La beauté de cette approche réside dans le fait que chaque couche a un objectif clair. La couche de base ne peut jamais remplacer l'actif, et l'actif ne peut jamais remplacer le débogage, quelle que soit la spécificité. Cela élimine les guerres de spécificité CSS qui affectent généralement les outils de débogage. Il semble maintenant que certains clusters se trouvent sur un fond sombre. Honnêtement, pas trop mal.
Ajout du JavaScript Heure JavaScript. C'est là que le contrôleur fait réellement quelque chose. Nous allons construire cela étape par étape. Étape 1 : configurer la gestion de l'état Tout d’abord, nous avons besoin de variables pour suivre l’état du débogueur : // =================================== // GESTION DE L'ÉTAT // ===================================
laisser courir = faux ; // Suit si le débogueur est actif laissez rafid; // Stocke l'ID requestAnimationFrame pour l'annulation
Ces variables contrôlent la boucle d'animation qui lit en permanence les entrées de la manette de jeu. Étape 2 : Récupérez les références DOM Ensuite, nous obtenons des références à tous les éléments HTML que nous allons mettre à jour : // =================================== // RÉFÉRENCES DES ÉLÉMENTS DOM // ===================================
const btnA = document.getElementById("btn-a"); const btnB = document.getElementById("btn-b"); const btnX = document.getElementById("btn-x"); const pause1 = document.getElementById("pause1"); const pause2 = document.getElementById("pause2"); const status = document.getElementById("status");
Stocker ces références à l'avance est plus efficace que d'interroger le DOM à plusieurs reprises. Étape 3 : Ajouter un clavier de secours Pour les tests sans contrôleur physique, nous mapperons les touches du clavier aux boutons : // =================================== // CLAVIER FALLBACK (pour tester sans contrôleur) // ===================================
const keyMap = { "a": btnA, "b": btnB, "x": btnX, "p": [pause1, pause2] // La touche 'p' contrôle les deux barres de pause } ;
Cela nous permet de tester l'interface utilisateur en appuyant sur les touches d'un clavier. Étape 4 : Créer la boucle de mise à jour principale C’est ici que la magie opère. Cette fonction s'exécute en continu et lit l'état de la manette : // =================================== // BOUCLE DE MISE À JOUR PRINCIPALE DU GAMEPAD // ===================================
fonction updateGamepad() { // Récupère toutes les manettes connectées const gamepads = navigator.getGamepads(); si (!gamepads) revient ;
// Utiliser la première manette connectée const gp = manettes de jeu[0];
si (gp) { // Mettre à jour les états des boutons en basculant la classe "active" btnA.classList.toggle("active", gp.buttons[0].pressed); btnB.classList.toggle("active", gp.buttons[1].pressed); btnX.classList.toggle("active", gp.buttons[2].pressed);
// Gérer le bouton pause (bouton index 9 sur la plupart des contrôleurs) const pausePressed = gp.buttons[9].pressed; pause1.classList.toggle("active", pausePressed); pause2.classList.toggle("active", pausePressed);
// Construire une liste des boutons actuellement enfoncés pour l'affichage de l'état laisser appuyé = []; gp.buttons.forEach((btn, i) => { si (btn.pressé)pressé.push("Bouton " + i); });
// Mettre à jour le texte d'état si un bouton est enfoncé si (pressed.length > 0) { status.textContent = "Pressé : " + pressé.join(", "); } }
// Continue la boucle si le débogueur est en cours d'exécution si (en cours d'exécution) { rafId = requestAnimationFrame(updateGamepad); } }
La méthode classList.toggle() ajoute ou supprime la classe active selon que le bouton est enfoncé, ce qui déclenche nos styles de calque CSS. Étape 5 : Gérer les événements du clavier Ces écouteurs d'événements font fonctionner le clavier de secours : // =================================== // Gestionnaires d'événements de clavier // ===================================
document.addEventListener("keydown", (e) => { si (keyMap[e.key]) { // Gère un ou plusieurs éléments if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("active")); } autre { keyMap[e.key].classList.add("active"); } status.textContent = "Touche enfoncée : " + e.key.toUpperCase(); } });
document.addEventListener("keyup", (e) => { si (keyMap[e.key]) { // Supprime l'état actif lorsque la clé est relâchée if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("active")); } autre { keyMap[e.key].classList.remove("active"); } status.textContent = "Clé libérée : " + e.key.toUpperCase(); } });
Étape 6 : ajouter un contrôle de démarrage/arrêt Enfin, nous avons besoin d'un moyen d'activer et de désactiver le débogueur : // =================================== // ACTIVER/DÉSACTIVER LE DÉBUGEUR // ===================================
document.getElementById("toggle").addEventListener("click", () => { en cours d'exécution = !en cours d'exécution; // Retourne l'état d'exécution
si (en cours d'exécution) { status.textContent = "Débogueur en cours d'exécution..."; updateGamepad(); // Démarre la boucle de mise à jour } autre { status.textContent = "Débogueur inactif" ; CancelAnimationFrame(rafId); // Arrête la boucle } });
Alors oui, appuyez sur un bouton et il brille. Poussez le bâton et il bouge. C'est ça. Encore une chose : les valeurs brutes. Parfois, vous voulez juste voir des chiffres, pas des lumières.
A ce stade, vous devriez voir :
Un simple contrôleur à l'écran, Des boutons qui réagissent lorsque vous interagissez avec eux, et Une lecture de débogage facultative affichant les indices des boutons enfoncés.
Pour rendre cela moins abstrait, voici une démonstration rapide du contrôleur à l’écran réagissant en temps réel :
Maintenant, appuyer sur Démarrer l’enregistrement enregistre tout jusqu’à ce que vous appuyiez sur Arrêter l’enregistrement. 2. Exportation de données vers CSV/JSON Une fois que nous aurons un journal, nous voudrons le sauvegarder.
Étape 1 : Créer l'assistant de téléchargement Tout d’abord, nous avons besoin d’une fonction d’assistance qui gère les téléchargements de fichiers dans le navigateur : // =================================== // AIDE AU TÉLÉCHARGEMENT DE FICHIER // ===================================
function downloadFile(nom de fichier, contenu, type = "text/plain") { // Crée un blob à partir du contenu const blob = new Blob([contenu], { type }); const url = URL.createObjectURL(blob);
// Créez un lien de téléchargement temporaire et cliquez dessus const a = document.createElement("a"); a.href = url ; a.download = nom de fichier ; a.cliquez();
// Nettoie l'URL de l'objet après le téléchargement setTimeout(() => URL.revokeObjectURL(url), 100); }
Cette fonction fonctionne en créant un Blob (grand objet binaire) à partir de vos données, en générant une URL temporaire pour celui-ci et en cliquant par programme sur un lien de téléchargement. Le nettoyage garantit que nous ne perdons pas de mémoire. Étape 2 : Gérer l'exportation JSON JSON est parfait pour préserver la structure complète des données :
// =================================== // EXPORTATION SOUS JSON // ===================================
document.getElementById("export-json").addEventListener("click", () => { // Vérifie s'il y a quelque chose à exporter si (!frames.length) { console.warn("Aucun enregistrement disponible pour l'exportation."); retour; }
// Créer une charge utile avec des métadonnées et des frames charge utile const = { crééÀ : new Date().toISOString(), cadres } ;
// Télécharger au format JSON télécharger le fichier ( "gamepad-log.json", JSON.stringify(charge utile, null, 2), "application/json" ); });
Le format JSON garde tout structuré et facilement analysable, ce qui le rend idéal pour le rechargement dans les outils de développement ou le partage avec des coéquipiers. Étape 3 : Gérer l'exportation CSV Pour les exportations CSV, nous devons aplatir les données hiérarchiques en lignes et colonnes :
//=================================== // EXPORTER EN CSV // ===================================
document.getElementById("export-csv").addEventListener("click", () => { // Vérifie s'il y a quelque chose à exporter si (!frames.length) { console.warn("Aucun enregistrement disponible pour l'exportation."); retour; }
// Construire la ligne d'en-tête CSV (colonnes pour l'horodatage, tous les boutons, tous les axes) const headerButtons = frames[0].buttons.map((_, i) => btn${i}); const headerAxes = frames[0].axes.map((_, i) => axis${i}); const header = ["t", ...headerButtons, ...headerAxes].join(",") + "\n";
// Créer des lignes de données CSV const lignes = frames.map(f => { const btnVals = f.buttons.map(b => b.value); return [f.t, ...btnVals, ...f.axes].join(","); }).join("\n");
// Télécharger au format CSV downloadFile("gamepad-log.csv", en-tête + lignes, "text/csv"); });
Le CSV est idéal pour l'analyse des données car il s'ouvre directement dans Excel ou Google Sheets, vous permettant de créer des graphiques, de filtrer des données ou de repérer visuellement des modèles. Maintenant que les boutons d'exportation sont activés, vous verrez deux nouvelles options sur le panneau : Exporter JSON et Exporter CSV. JSON est idéal si vous souhaitez réintégrer le journal brut dans vos outils de développement ou parcourir la structure. CSV, en revanche, s'ouvre directement dans Excel ou Google Sheets afin que vous puissiez tracer, filtrer ou comparer les entrées. La figure suivante montre à quoi ressemble le panneau avec ces commandes supplémentaires.
3. Système d'instantanés Parfois, vous n’avez pas besoin d’un enregistrement complet, juste d’une rapide « capture d’écran » des états d’entrée. C’est là qu’un bouton Prendre un instantané est utile.
Et le JavaScript :
// =================================== // PRENDRE UN INSTANTANÉ // ===================================
document.getElementById("instantané").addEventListener("clic", () => { // Récupère toutes les manettes connectées const pads = navigator.getGamepads(); const activePads = [];
// Parcourez et capturez l'état de chaque manette de jeu connectée pour (const gp de pads) { si (!gp) continue ; // Ignorer les emplacements vides
activePads.push({ id : gp.id, // Nom/modèle du contrôleur horodatage : performance.now(), boutons : gp.buttons.map(b => ({ pressé: b.pressé, valeur : b.value })), axes : [...gp.axes] }); }
// Vérifiez si des manettes de jeu ont été trouvées si (!activePads.length) { console.warn("Aucune manette de jeu connectée pour l'instantané."); alert("Aucun contrôleur détecté !"); retour; }
// Enregistrez et informez l'utilisateur console.log("Instantané :", activePads); alert(Instantané pris ! Contrôleur(s) ${activePads.length} capturé(s).); });
Les instantanés gèlent l’état exact de votre contrôleur à un moment donné. 4. Relecture d'entrée fantôme Passons maintenant au plus amusant : la relecture des entrées fantômes. Cela prend un journal et le lit visuellement comme si un joueur fantôme utilisait le contrôleur.
JavaScript pour la relecture : // =================================== // RÉCUPÉRATION FANTÔME // ===================================
document.getElementById("replay").addEventListener("click", () => { // Assurez-vous que nous avons un enregistrement à rejouer si (!frames.length) { alert("Aucun enregistrement à rejouer !"); retour; }
console.log("Démarrage de la relecture fantôme...");
// Synchronisation de la piste pour la lecture synchronisée laissez startTime = performance.now(); laissez frameIndex = 0;
// Rejouer la boucle d'animation fonction étape() { const maintenant = performance.now(); const écoulé = maintenant - startTime ;
// Traite toutes les images qui auraient dû se produire maintenant while (frameIndex < frames.length && frames[frameIndex].t <= écoulé) { const frame = frames[frameIndex];
// Mise à jour de l'interface utilisateur avec les états des boutons enregistrés btnA.classList.toggle("active", frame.buttons[0].pressed); btnB.classList.toggle("active", frame.buttons[1].pressed); btnX.classList.toggle("active", frame.buttons[2].pressed);
// Affichage de l'état de la mise à jour laisser appuyé = []; frame.buttons.forEach((btn, i) => { if (btn.pressed) pressé.push("Bouton " + i); }); si (pressed.length > 0) { status.textContent = "Ghost: " + pressé.join(", "); }
frameIndex++; }
// Continue la boucle s'il y a plus de frames si (frameIndex < frames.length) { requestAnimationFrame(étape); } autre { console.log("Rejouerterminé."); status.textContent = "Relecture terminée"; } }
// Démarre la relecture étape(); });
Pour rendre le débogage un peu plus pratique, j'ai ajouté une rediffusion fantôme. Une fois que vous avez enregistré une session, vous pouvez appuyer sur replay et regarder l’interface utilisateur la jouer, presque comme si un joueur fantôme exécutait le pad. Un nouveau bouton Replay Ghost apparaît dans le panneau à cet effet.
Appuyez sur Enregistrer, jouez un peu avec le contrôleur, arrêtez, puis rejouez. L'interface utilisateur fait écho à tout ce que vous avez fait, comme un fantôme suivant vos entrées. Pourquoi s'embêter avec ces extras ?
L'enregistrement/l'exportation permet aux testeurs de montrer facilement ce qui s'est passé. Les instantanés figent un instant dans le temps, ce qui est très utile lorsque vous recherchez des bugs étranges. Ghost Replay est idéal pour les didacticiels, les contrôles d'accessibilité ou simplement pour comparer les configurations de contrôle côte à côte.
À ce stade, il ne s’agit plus seulement d’une démo soignée, mais de quelque chose que vous pouvez réellement mettre en œuvre. Cas d'utilisation réels Nous disposons désormais de ce débogueur qui peut faire beaucoup de choses. Il affiche les entrées en direct, enregistre les journaux, les exporte et même rejoue des éléments. Mais la vraie question est : qui s’en soucie réellement ? À qui est-ce utile ? Développeurs de jeux Les contrôleurs font partie du travail, mais les déboguer ? Généralement une douleur. Imaginez que vous testez un combo de jeu de combat, comme ↓ → + coup de poing. Au lieu de prier, vous appuyez deux fois sur la même manière, vous l'enregistrez une fois et vous le rejouez. Terminé. Ou vous échangez les journaux JSON avec un coéquipier pour vérifier si votre code multijoueur réagit de la même manière sur sa machine. C'est énorme. Praticiens de l'accessibilité Celui-ci me tient à cœur. Tout le monde ne joue pas avec une manette « standard ». Les contrôleurs adaptatifs émettent parfois des signaux étranges. Avec cet outil, vous pouvez voir exactement ce qui se passe. Enseignants, chercheurs, peu importe. Ils peuvent récupérer les journaux, les comparer ou relire les entrées côte à côte. Soudain, des choses invisibles deviennent évidentes. Tests d'assurance qualité Les testeurs écrivent généralement des notes telles que « J'ai écrasé des boutons ici et c'est cassé ». Pas très utile. Maintenant ? Ils peuvent capturer les presses exactes, exporter le journal et l'envoyer. Aucune supposition. Éducateurs Si vous créez des didacticiels ou des vidéos YouTube, la relecture fantôme est de l'or. Vous pouvez littéralement dire : « Voici ce que j’ai fait avec le contrôleur », tandis que l’interface utilisateur montre que cela se produit. Rend les explications beaucoup plus claires. Au-delà des jeux Et oui, il ne s’agit pas seulement de jeux. Les gens ont utilisé des contrôleurs pour des robots, des projets artistiques et des interfaces d'accessibilité. Même problème à chaque fois : que voit réellement le navigateur ? Avec ça, vous n’avez pas à deviner. Conclusion Déboguer une entrée de contrôleur a toujours eu l’impression de voler à l’aveugle. Contrairement au DOM ou au CSS, il n’existe pas d’inspecteur intégré pour les manettes de jeu ; ce ne sont que des chiffres bruts dans la console, facilement perdus dans le bruit. Avec quelques centaines de lignes de HTML, CSS et JavaScript, nous avons construit quelque chose de différent :
Un débogueur visuel qui rend visibles les entrées invisibles. Un système CSS en couches qui maintient l'interface utilisateur propre et déboguable. Un ensemble d'améliorations (enregistrement, exportation, instantanés, relecture fantôme) qui l'élèvent de démo à outil de développement.
Ce projet montre jusqu'où vous pouvez aller en mélangeant la puissance de la plateforme Web avec un peu de créativité dans les couches CSS Cascade. L'outil que je viens d'expliquer dans son intégralité est open-source. Vous pouvez cloner le dépôt GitHub et l'essayer par vous-même. Mais plus important encore, vous pouvez vous l’approprier. Ajoutez vos propres calques. Construisez votre propre logique de relecture. Intégrez-le à votre prototype de jeu. Ou même l’utiliser d’une manière que je n’ai pas imaginée. Pour l’enseignement, l’accessibilité ou l’analyse de données. En fin de compte, il ne s’agit pas seulement de déboguer les manettes de jeu. Il s’agit de mettre en lumière les entrées cachées et de donner aux développeurs la confiance nécessaire pour travailler avec du matériel que le Web n’intègre pas encore pleinement. Alors, branchez votre contrôleur, ouvrez votre éditeur et commencez à expérimenter. Vous pourriez être surpris de voir ce que votre navigateur et votre CSS peuvent réellement accomplir.