Quando colleghi un controller, premi i pulsanti, muovi le levette, premi i grilletti... e come sviluppatore, non vedi nulla di tutto ciò. Il browser lo rileva, certo, ma a meno che tu non registri i numeri nella console, è invisibile. Questo è il mal di testa con l'API del Gamepad. È in circolazione da anni ed è in realtà piuttosto potente. Puoi leggere pulsanti, levette, grilletti, i lavori. Ma la maggior parte delle persone non lo tocca. Perché? Perché non c'è feedback. Nessun pannello negli strumenti per sviluppatori. Non esiste un modo chiaro per sapere se il controller sta facendo quello che pensi. Sembra di volare alla cieca. Ciò mi ha infastidito abbastanza da creare un piccolo strumento: Gamepad Cascade Debugger. Invece di fissare l'output della console, ottieni una visualizzazione live e interattiva del controller. Premi qualcosa e reagisce sullo schermo. E con CSS Cascade Layers, gli stili rimangono organizzati, quindi è più semplice eseguire il debug. In questo post ti mostrerò perché il debug dei controller è così complicato, come i CSS aiutano a ripulirlo e come puoi creare un debugger visivo riutilizzabile per i tuoi progetti.
Anche se riesci a registrarli tutti, ti ritroverai rapidamente con spam illeggibile sulla console. Ad esempio: [0,0,1,0,0,0.5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]
Puoi dire quale pulsante è stato premuto? Forse, ma solo dopo aver affaticato gli occhi e aver perso qualche input. Quindi no, il debug non è facile quando si tratta di leggere gli input. Problema 3: mancanza di struttura Anche se metti insieme un visualizzatore rapido, gli stili possono diventare rapidamente confusi. Gli stati predefinito, attivo e di debug possono sovrapporsi e, senza una struttura chiara, il tuo CSS diventa fragile e difficile da estendere. I CSS Cascade Layers possono aiutare. Raggruppano gli stili in "livelli" ordinati per priorità, quindi smetti di combattere la specificità e di indovinare: "Perché il mio stile di debug non viene visualizzato?" Invece, mantieni preoccupazioni separate:
Base: l'aspetto iniziale standard del controller. Attivo: evidenziazioni per i pulsanti premuti e le levette spostate. Debug: sovrapposizioni per gli sviluppatori (ad esempio letture numeriche, guide e così via).
Se dovessimo definire i livelli nei CSS in base a questo, avremmo: /* priorità dalla più bassa alla più alta */ @layer base, attivo, debug;
@strato base { /* ... */ }
@livello attivo { /* ... */ }
@livello debug { /* ... */ }
Poiché ogni livello si accumula in modo prevedibile, sai sempre quali regole vincono. Questa prevedibilità rende il debug non solo più semplice, ma effettivamente gestibile. Abbiamo trattato il problema (input invisibile e disordinato) e l'approccio (un debugger visivo creato con Cascade Layers). Ora esamineremo il processo passo passo per creare il debugger. Il concetto di debugger Il modo più semplice per rendere visibile l'input nascosto è semplicemente disegnarlo sullo schermo. Questo è ciò che fa questo debugger. Pulsanti, grilletti e joystick hanno tutti un aspetto visivo.
Premi A: un cerchio si illumina. Muovi il bastone: il cerchio scivola intorno. Premi un grilletto a metà: una barra si riempie a metà.
Ora non stai fissando gli 0 e gli 1, ma in realtà guardi il controller reagire dal vivo. Naturalmente, una volta che si iniziano ad accumulare stati come predefinito, premuto, informazioni di debug e forse anche una modalità di registrazione, il CSS inizia a diventare più grande e complesso. È qui che i livelli a cascata tornano utili. Ecco un esempio ridotto: @strato base { .pulsante { sfondo: #222; raggio del bordo: 50%; larghezza: 40px; altezza: 40px; } }
@livello attivo { .pulsante.premuto { sfondo: #0f0; /* verde brillante */ } }
@livello debug { .pulsante::dopo { contenuto: attr(valore-dati); dimensione carattere: 12px; colore: #fff; } }
L'ordine dei livelli è importante: base → attivo → debug.
base disegna il controller. attivo gestisce gli stati premuti. il debug lancia sugli overlay.
Separarlo in questo modo significa che non stai combattendo strane guerre di specificità. Ogni strato ha il suo posto e sai sempre cosa vince. Costruirlo Mettiamo prima qualcosa sullo schermo. Non è necessario che abbia un bell'aspetto, deve solo esistere in modo da avere qualcosa su cui lavorare.
Debugger a cascata del gamepad
Sono letteralmente solo scatole. Non è ancora entusiasmante, ma ci fornisce spunti da utilizzare in seguito con CSS e JavaScript. Ok, sto usando i livelli a cascata qui perché mantiene le cose organizzate una volta aggiunti più stati. Ecco un passaggio approssimativo:
/* =================================== IMPOSTAZIONE STRATI CASCATA L'ordine è importante: base → attivo → debug =================================== */
/* Definisce in anticipo l'ordine dei livelli */ @layer base, attivo, debug;
/* Livello 1: stili di base - aspetto predefinito */ @strato base { .pulsante { sfondo: #333; raggio del bordo: 50%; larghezza: 70px; altezza: 70px; display: flessibile; giustifica-contenuto: centro; allineare gli elementi: centro; }
.pausa { larghezza: 20px; altezza: 70px; sfondo: #333; visualizzazione: blocco in linea; } }
/* Livello 2: stati attivi: gestisce i pulsanti premuti */ @livello attivo { .pulsante.attivo { sfondo: #0f0; /* Verde brillante quando premuto */ trasformazione: scala(1.1); /* Ingrandisce leggermente il pulsante */ }
.pausa.attivo { sfondo: #0f0; trasformazione: scalaY(1.1); /* Si allunga verticalmente quando viene premuto */ } }
/* Livello 3: overlay di debug - informazioni sullo sviluppatore */ @livello debug { .pulsante::dopo { contenuto: attr(valore-dati); /* Mostra il valore numerico */ dimensione carattere: 12px; colore: #fff; } }
La bellezza di questo approccio è che ogni livello ha uno scopo chiaro. Il livello di base non può mai sovrascrivere attivo e attivo non può mai sovrascrivere il debug, indipendentemente dalla specificità. Ciò elimina le guerre sulle specificità CSS che di solito affliggono gli strumenti di debug. Ora sembra che alcuni ammassi siano posizionati su uno sfondo scuro. Onestamente, non è poi così male.
Aggiunta di JavaScript Tempo di JavaScript. È qui che il controller fa effettivamente qualcosa. Lo costruiremo passo dopo passo. Passaggio 1: impostare la gestione dello stato Innanzitutto, abbiamo bisogno di variabili per tracciare lo stato del debugger: // =================================== // GESTIONE DELLO STATO // ===================================
lascia correre = falso; // Tiene traccia se il debugger è attivo lascia rafId; // Memorizza l'ID requestAnimationFrame per l'annullamento
Queste variabili controllano il ciclo di animazione che legge continuamente l'input del gamepad. Passaggio 2: prendi i riferimenti DOM Successivamente, otteniamo riferimenti a tutti gli elementi HTML che aggiorneremo: // =================================== // RIFERIMENTI ELEMENTO 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");
Memorizzare questi riferimenti in anticipo è più efficiente che interrogare ripetutamente il DOM. Passaggio 3: aggiungi il fallback della tastiera Per i test senza controller fisico, mapperemo i tasti della tastiera sui pulsanti: // =================================== // FALLBACK DELLA TASTIERA (per testare senza controller) // ===================================
const keyMap = { "a": btnA, "b": btnB, "x": btnX, "p": [pausa1, pausa2] // Il tasto 'p' controlla entrambe le barre di pausa };
Questo ci consente di testare l'interfaccia utente premendo i tasti su una tastiera. Passaggio 4: crea il ciclo di aggiornamento principale Ecco dove avviene la magia. Questa funzione funziona continuamente e legge lo stato del gamepad: // =================================== // LOOP DI AGGIORNAMENTO DEL GAMEPAD PRINCIPALE // ===================================
funzione aggiornamentoGamepad() { // Ottieni tutti i gamepad collegati const gamepad = navigator.getGamepads(); if (!gamepad) ritorna;
// Usa il primo gamepad connesso const gp = gamepad[0];
se (gp) { // Aggiorna gli stati del pulsante attivando la classe "attiva". btnA.classList.toggle("attivo", gp.buttons[0].pressed); btnB.classList.toggle("attivo", gp.buttons[1].pressed); btnX.classList.toggle("attivo", gp.buttons[2].pressed);
// Gestisci il pulsante di pausa (pulsante indice 9 sulla maggior parte dei controller) const pausePressed = gp.buttons[9].pressed; pause1.classList.toggle("attivo", pausePressed); pause2.classList.toggle("attivo", pausePressed);
// Crea un elenco dei pulsanti attualmente premuti per la visualizzazione dello stato lasciato premuto = []; gp.buttons.forEach((btn, i) => { se (btn.premuto)premuto.push("Pulsante " + i); });
// Aggiorna il testo dello stato se viene premuto un pulsante if (lunghezza.premuto > 0) { status.textContent = "Premuto: " + premuto.join(", "); } }
// Continua il ciclo se il debugger è in esecuzione se (in corsa) { rafId = requestAnimationFrame(updateGamepad); } }
Il metodo classList.toggle() aggiunge o rimuove la classe attiva in base alla pressione del pulsante, che attiva i nostri stili di livello CSS. Passaggio 5: gestire gli eventi della tastiera Questi ascoltatori di eventi fanno funzionare il fallback della tastiera: // =================================== // GESTORI DI EVENTI DA TASTIERA // ===================================
document.addEventListener("keydown", (e) => { if (keyMap[e.key]) { // Gestisce elementi singoli o multipli if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("active")); } altrimenti { keyMap[e.key].classList.add("attivo"); } status.textContent = "Tasto premuto: " + e.key.toUpperCase(); } });
document.addEventListener("keyup", (e) => { if (keyMap[e.key]) { // Rimuove lo stato attivo quando viene rilasciata la chiave if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("active")); } altrimenti { keyMap[e.key].classList.remove("attivo"); } status.textContent = "Chiave rilasciata: " + e.key.toUpperCase(); } });
Passaggio 6: aggiungi il controllo di avvio/arresto Infine, abbiamo bisogno di un modo per attivare e disattivare il debugger: // =================================== // ATTIVA/DISATTIVA IL DEBUGGER // ===================================
document.getElementById("toggle").addEventListener("click", () => { correndo = !correndo; // Inverti lo stato di esecuzione
se (in corsa) { status.textContent = "Debugger in esecuzione..."; aggiornamentoGamepad(); // Avvia il ciclo di aggiornamento } altrimenti { status.textContent = "Debugger inattivo"; cancelAnimationFrame(rafId); // Interrompe il ciclo } });
Quindi sì, premi un pulsante e si illumina. Spingi il bastone e si muove. Questo è tutto. Ancora una cosa: valori grezzi. A volte vuoi solo vedere i numeri, non le luci.
In questa fase dovresti vedere:
Un semplice controller su schermo, Pulsanti che reagiscono quando interagisci con loro e Una lettura di debug opzionale che mostra gli indici dei pulsanti premuti.
Per rendere il tutto meno astratto, ecco una rapida demo del controller sullo schermo che reagisce in tempo reale:
Ora, premendo Avvia registrazione si registra tutto finché non si preme Interrompi registrazione. 2. Esportazione dei dati in CSV/JSON Una volta che avremo un registro, vorremo salvarlo.
Passaggio 1: crea l'assistente per il download Innanzitutto, abbiamo bisogno di una funzione di supporto che gestisca i download dei file nel browser: // =================================== // AIUTO PER IL DOWNLOAD DEI FILE // ===================================
function downloadFile(nome file, contenuto, tipo = "testo/semplice") { // Crea un BLOB dal contenuto const blob = new Blob([contenuto], { tipo }); const url = URL.createObjectURL(blob);
// Crea un collegamento per il download temporaneo e fai clic su di esso const a = document.createElement("a"); a.href = URL; a.download = nome file; a.clic();
// Pulisci l'URL dell'oggetto dopo il download setTimeout(() => URL.revokeObjectURL(url), 100); }
Questa funzione funziona creando un BLOB (oggetto binario di grandi dimensioni) dai tuoi dati, generando un URL temporaneo per esso e facendo clic a livello di codice su un collegamento di download. La pulizia garantisce che non ci siano perdite di memoria. Passaggio 2: gestire l'esportazione JSON JSON è perfetto per preservare la struttura completa dei dati:
// =================================== // ESPORTA COME JSON // ===================================
document.getElementById("export-json").addEventListener("click", () => { // Controlla se c'è qualcosa da esportare if (!fotogrammi.lunghezza) { console.warn("Nessuna registrazione disponibile per l'esportazione."); ritorno; }
// Crea un payload con metadati e frame carico utile costante = { creatoAt: new Date().toISOString(), cornici };
// Scarica come JSON formattato scaricaFile( "gamepad-log.json", JSON.stringify(carico utile, null, 2), "applicazione/json" ); });
Il formato JSON mantiene tutto strutturato e facilmente analizzabile, rendendolo ideale per il caricamento negli strumenti di sviluppo o la condivisione con i compagni di squadra. Passaggio 3: gestire l'esportazione CSV Per le esportazioni CSV, dobbiamo appiattire i dati gerarchici in righe e colonne:
//=================================== // ESPORTA COME CSV // ===================================
document.getElementById("export-csv").addEventListener("click", () => { // Controlla se c'è qualcosa da esportare if (!fotogrammi.lunghezza) { console.warn("Nessuna registrazione disponibile per l'esportazione."); ritorno; }
// Crea una riga di intestazione CSV (colonne per timestamp, tutti i pulsanti, tutti gli assi) const headerButtons = frames[0].buttons.map((_, i) => btn${i}); const headerAxes = frames[0].axes.map((_, i) => asse${i}); const header = ["t", ...headerButtons, ...headerAxes].join(",") + "\n";
// Crea righe di dati CSV const righe = frames.map(f => { const btnVals = f.pulsanti.map(b => b.valore); return [f.t, ...btnVals, ...f.axes].join(","); }).join("\n");
// Scarica come CSV downloadFile("gamepad-log.csv", intestazione + righe, "testo/csv"); });
CSV è eccezionale per l'analisi dei dati perché si apre direttamente in Excel o Fogli Google, consentendoti di creare grafici, filtrare dati o modelli spot visivamente. Ora che i pulsanti di esportazione sono attivi, vedrai due nuove opzioni sul pannello: Esporta JSON ed Esporta CSV. JSON è utile se vuoi reinserire il registro grezzo nei tuoi strumenti di sviluppo o curiosare nella struttura. CSV, d'altra parte, si apre direttamente in Excel o Fogli Google in modo da poter creare grafici, filtrare o confrontare gli input. La figura seguente mostra come appare il pannello con questi controlli aggiuntivi.
3. Sistema di istantanee A volte non è necessaria una registrazione completa, ma solo un rapido "screenshot" degli stati di input. È qui che aiuta il pulsante Scatta istantanea.
E il JavaScript:
// =================================== // SCATTI UN'ISTANTANEA // ===================================
document.getElementById("istantanea").addEventListener("click", () => { // Ottieni tutti i gamepad collegati const pad = navigator.getGamepads(); const activePads = [];
// Esegui il loop e acquisisci lo stato di ciascun gamepad collegato per (cost mo di pad) { se (!gp) continua; // Salta gli slot vuoti
activePads.push({ id: gp.id, // Nome/modello del controller timestamp: performance.now(), pulsanti: gp.buttons.map(b => ({ premuto: b.premuto, valore: b.valore })), assi: [...gp.axes] }); }
// Controlla se sono stati trovati dei gamepad if (!activePads.length) { console.warn("Nessun gamepad collegato per l'istantanea."); alert("Nessun controller rilevato!"); ritorno; }
// Registra e avvisa l'utente console.log("Istantanea:", activePads); alert(Istantanea scattata! Controller ${activePads.length} catturati.); });
Le istantanee congelano lo stato esatto del controller in un determinato momento. 4. Riproduzione dell'input fantasma Ora passiamo alla parte divertente: riproduzione dell'input fantasma. Questo prende un registro e lo riproduce visivamente come se un giocatore fantasma stesse utilizzando il controller.
JavaScript per la riproduzione: // =================================== // REPLAY FANTASMA // ===================================
document.getElementById("replay").addEventListener("click", () => { // Assicurati di avere una registrazione da riprodurre if (!fotogrammi.lunghezza) { alert("Nessuna registrazione da riprodurre!"); ritorno; }
console.log("Avvio riproduzione fantasma...");
// Temporizzazione della traccia per la riproduzione sincronizzata lascia che startTime = performance.now(); lascia che frameIndex = 0;
// Riproduce il ciclo dell'animazione passo della funzione() { const ora = performance.now(); const trascorso = ora - startTime;
// Elabora tutti i frame che dovrebbero essersi verificati a questo punto while (frameIndex < frames.length && frames[frameIndex].t <= trascorso) { const frame = frame[Indiceframe];
// Aggiorna l'interfaccia utente con gli stati dei pulsanti registrati btnA.classList.toggle("attivo", frame.buttons[0].pressed); btnB.classList.toggle("attivo", frame.buttons[1].pressed); btnX.classList.toggle("attivo", frame.buttons[2].pressed);
// Aggiorna la visualizzazione dello stato lasciato premuto = []; frame.buttons.forEach((btn, i) => { if (btn.premuto) premuto.push("Pulsante " + i); }); if (lunghezza.premuto > 0) { status.textContent = "Ghost: " +press.join(", "); }
frameIndice++; }
// Continua il ciclo se ci sono più frame if (indiceframe < frame.lunghezza) { requestAnimationFrame(passaggio); } altrimenti { console.log("Riproducifinito."); status.textContent = "Replay completato"; } }
// Avvia la riproduzione passo(); });
Per rendere il debug un po' più pratico, ho aggiunto un replay fantasma. Dopo aver registrato una sessione, puoi premere Replay e guardare l'interfaccia utente recitarla, quasi come se un giocatore fantasma stesse utilizzando il pad. A questo scopo nel pannello viene visualizzato un nuovo pulsante Replay Ghost.
Premi Registra, gioca un po' con il controller, fermati, quindi riproduci. L'interfaccia utente riecheggia tutto ciò che hai fatto, come un fantasma che segue i tuoi input. Perché preoccuparsi di questi extra?
La registrazione/esportazione consente ai tester di mostrare esattamente cosa è successo. Le istantanee congelano un momento nel tempo, molto utili quando stai inseguendo strani bug. La riproduzione fantasma è ottima per tutorial, controlli di accessibilità o semplicemente per confrontare le configurazioni dei controlli fianco a fianco.
A questo punto, non è più solo una bella demo, ma qualcosa che potresti effettivamente mettere in pratica. Casi d'uso nel mondo reale Ora abbiamo questo debugger che può fare molto. Mostra input in tempo reale, registra i registri, li esporta e persino riproduce elementi. Ma la vera domanda è: a chi importa davvero? A chi è utile? Sviluppatori di giochi I controller fanno parte del lavoro, ma eseguirne il debug? Di solito un dolore. Immagina di testare una combinazione di gioco di combattimento, come ↓ → + pugno. Invece di pregare, lo hai premuto due volte allo stesso modo, lo registri una volta e lo riproduci. Fatto. Oppure scambia i log JSON con un compagno di squadra per verificare se il tuo codice multiplayer reagisce allo stesso modo sulla sua macchina. È enorme. Professionisti dell'accessibilità Questo mi sta a cuore. Non tutti giocano con un controller “standard”. I controller adattivi a volte emettono segnali strani. Con questo strumento puoi vedere esattamente cosa sta succedendo. Insegnanti, ricercatori, chiunque. Possono acquisire registri, confrontarli o riprodurre gli input fianco a fianco. All'improvviso, le cose invisibili diventano evidenti. Test di garanzia della qualità I tester di solito scrivono note come "Ho schiacciato i pulsanti qui e si è rotto". Non molto utile. Adesso? Possono acquisire le stampe esatte, esportare il registro e inviarlo. Nessuna supposizione. Educatori Se stai realizzando tutorial o video su YouTube, il replay fantasma è d'oro. Puoi letteralmente dire "Ecco cosa ho fatto con il controller", mentre l'interfaccia utente mostra che ciò sta accadendo. Rende le spiegazioni molto più chiare. Oltre i giochi E sì, non si tratta solo di giochi. Le persone hanno utilizzato controller per robot, progetti artistici e interfacce di accessibilità. Ogni volta lo stesso problema: cosa vede effettivamente il browser? Con questo, non devi indovinare. Conclusione Il debug dell'input di un controller è sempre stato come volare alla cieca. A differenza del DOM o del CSS, non esiste un ispettore integrato per i gamepad; sono solo numeri grezzi nella console, facilmente persi nel rumore. Con poche centinaia di righe di HTML, CSS e JavaScript, abbiamo creato qualcosa di diverso:
Un debugger visivo che rende visibili gli input invisibili. Un sistema CSS a più livelli che mantiene l'interfaccia utente pulita e debuggabile. Una serie di miglioramenti (registrazione, esportazione, istantanee, riproduzione fantasma) che lo elevano da demo a strumento di sviluppo.
Questo progetto mostra quanto lontano puoi arrivare unendo la potenza della piattaforma Web con un po' di creatività nei CSS Cascade Layers. Lo strumento che ho appena spiegato nella sua interezza è open source. Puoi clonare il repository GitHub e provarlo tu stesso. Ma ancora più importante, puoi renderlo tuo. Aggiungi i tuoi livelli. Costruisci la tua logica di replay. Integralo con il tuo prototipo di gioco. O addirittura usarlo in modi che non avevo immaginato. Per l'insegnamento, l'accessibilità o l'analisi dei dati. Alla fine, non si tratta solo di eseguire il debug dei gamepad. Si tratta di far luce sugli input nascosti e di dare agli sviluppatori la sicurezza di lavorare con hardware che il web ancora non abbraccia completamente. Quindi collega il controller, apri l'editor e inizia a sperimentare. Potresti rimanere sorpreso da ciò che il tuo browser e il tuo CSS possono veramente realizzare.