Når du tilslutter en controller, maser du knapper, flytter pindene, trækker udløserne ... og som udvikler ser du intet af det. Browseren samler det op, men medmindre du logger numre i konsollen, er det usynligt. Det er hovedpinen med Gamepad API. Det har eksisteret i årevis, og det er faktisk ret kraftfuldt. Du kan læse knapper, pinde, triggere, værkerne. Men de fleste mennesker rører det ikke. Hvorfor? For der er ingen feedback. Intet panel i udviklerværktøjer. Ingen klar måde at vide, om controlleren overhovedet gør, hvad du tror. Det føles som at flyve blind. Det forstyrrede mig nok til at bygge et lille værktøj: Gamepad Cascade Debugger. I stedet for at stirre på konsollens output får du en levende, interaktiv visning af controlleren. Tryk på noget, og det reagerer på skærmen. Og med CSS Cascade Layers forbliver stilene organiseret, så det er renere at fejlsøge. I dette indlæg vil jeg vise dig, hvorfor fejlfinding af controllere er så smertefuldt, hvordan CSS hjælper med at rydde op i det, og hvordan du kan bygge en genanvendelig visuel debugger til dine egne projekter.
Selvom du er i stand til at logge dem alle, ender du hurtigt med ulæselig konsolspam. For eksempel: [0,0,1,0,0,0,5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]
Kan du se hvilken knap der blev trykket på? Måske, men først efter at have anstrengt dine øjne og mangler nogle få input. Så nej, fejlretning kommer ikke let, når det kommer til at læse input. Opgave 3: Mangel på struktur Selvom du sammensætter en hurtig visualizer, kan stilarter hurtigt blive rodet. Standard-, aktiv- og fejlretningstilstande kan overlappe hinanden, og uden en klar struktur bliver din CSS skør og svær at udvide. CSS Cascade Layers kan hjælpe. De grupperer stilarter i "lag", der er ordnet efter prioritet, så du holder op med at bekæmpe specificitet og gætte, "Hvorfor vises min debug-stil ikke?" I stedet opretholder du separate bekymringer:
Base: Controllerens standard, oprindelige udseende. Aktiv: Højdepunkter for trykte knapper og flyttede pinde. Fejlretning: Overlejringer til udviklere (f.eks. numeriske udlæsninger, vejledninger og så videre).
Hvis vi skulle definere lag i CSS i henhold til dette, ville vi have: /* laveste til højeste prioritet */ @lagbase, aktiv, debug;
@layer base { /* ... */ }
@lag aktiv { /* ... */ }
@layer debug { /* ... */ }
Fordi hvert lag stables forudsigeligt, ved du altid, hvilke regler der vinder. Den forudsigelighed gør fejlfinding ikke bare nemmere, men faktisk overskuelig. Vi har dækket problemet (usynligt, rodet input) og tilgangen (en visuel debugger bygget med Cascade Layers). Nu vil vi gennemgå den trinvise proces for at bygge fejlretningen. Debugger-konceptet Den nemmeste måde at synliggøre skjult input på er blot at tegne det på skærmen. Det er, hvad denne debugger gør. Knapper, triggere og joysticks får alle et visuelt billede.
Tryk på A: En cirkel lyser. Skub stokken: Cirklen glider rundt. Træk en aftrækker halvvejs: En stang fyldes halvt.
Nu stirrer du ikke på 0'er og 1'er, men ser faktisk controlleren reagere live. Selvfølgelig, når du begynder at samle tilstande som standard, trykket, fejlfindingsoplysninger, måske endda en optagetilstand, begynder CSS'en at blive større og mere kompleks. Det er her kaskadelag kommer til nytte. Her er et afklebet eksempel: @layer base { .button { baggrund: #222; grænse-radius: 50%; bredde: 40px; højde: 40px; } }
@lag aktiv { .button.pressed { baggrund: #0f0; /* lysegrøn */ } }
@layer debug { .button::efter { indhold: attr(data-værdi); skriftstørrelse: 12px; farve: #fff; } }
Lagrækkefølgen har betydning: base → aktiv → debug.
base trækker controlleren. aktive håndterer pressede tilstande. debug kaster på overlejringer.
At bryde det op på denne måde betyder, at du ikke kæmper underlige specificitetskrige. Hvert lag har sin plads, og du ved altid, hvad der vinder. Bygger det ud Lad os først få noget på skærmen. Det behøver ikke at se godt ud - det skal bare eksistere, så vi har noget at arbejde med.
Gamepad Cascade Debugger
Det er bogstaveligt talt bare kasser. Ikke spændende endnu, men det giver os håndtag til at gribe senere med CSS og JavaScript. Okay, jeg bruger kaskadelag her, fordi det holder tingene organiseret, når du tilføjer flere tilstande. Her er en grov aflevering:
/* =================================== CASCADE LAG OPSÆTNING Ordren har betydning: base → aktiv → debug ===================================== */
/* Definer lagrækkefølge på forhånd */ @lagbase, aktiv, debug;
/* Layer 1: Base styles - standard udseende */ @layer base { .button { baggrund: #333; grænse-radius: 50%; bredde: 70px; højde: 70px; display: flex; retfærdiggøre-indhold: center; align-items: center; }
.pause { bredde: 20px; højde: 70px; baggrund: #333; display: inline-blok; } }
/* Lag 2: Aktive tilstande - håndterer trykkede knapper */ @lag aktiv { .button.active { baggrund: #0f0; /* Lys grøn, når der trykkes på */ transform: skala(1.1); /* Forstørrer knappen lidt */ }
.pause.active { baggrund: #0f0; transform: skalaY(1.1); /* Strækker sig lodret, når der trykkes på */ } }
/* Lag 3: Debug overlays - udvikler info */ @layer debug { .button::efter { indhold: attr(data-værdi); /* Viser den numeriske værdi */ skriftstørrelse: 12px; farve: #fff; } }
Det smukke ved denne tilgang er, at hvert lag har et klart formål. Basislaget kan aldrig tilsidesætte aktiv, og aktiv kan aldrig tilsidesætte fejlretning, uanset specificitet. Dette eliminerer CSS-specificitetskrigene, der normalt plager fejlfindingsværktøjer. Nu ser det ud til, at nogle klynger sidder på en mørk baggrund. Helt ærligt, ikke så dårligt.
Tilføjelse af JavaScript JavaScript tid. Det er her, controlleren rent faktisk gør noget. Vi bygger dette trin for trin. Trin 1: Konfigurer State Management For det første har vi brug for variabler til at spore debuggerens tilstand: // =================================== // STATENS LEDELSE // ===================================
lad løbe = falsk; // Sporer om debuggeren er aktiv lade rafId; // Gemmer requestAnimationFrame ID til annullering
Disse variabler styrer animationsløkken, der kontinuerligt læser gamepad-input. Trin 2: Få fat i DOM-referencer Dernæst får vi referencer til alle de HTML-elementer, vi vil opdatere: // =================================== // DOM ELEMENT REFERENCER // ===================================
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");
Det er mere effektivt at gemme disse referencer på forhånd end at forespørge på DOM gentagne gange. Trin 3: Tilføj tastaturtilbagegang For at teste uden en fysisk controller, kortlægger vi tastaturtaster til knapper: // =================================== // KEYBOARD FALLBACK (til test uden en controller) // ===================================
const keyMap = { "a": btnA, "b": btnB, "x": btnX, "p": [pause1, pause2] // 'p'-tasten styrer begge pausebjælker };
Dette lader os teste brugergrænsefladen ved at trykke på taster på et tastatur. Trin 4: Opret hovedopdateringsløkken Det er her magien sker. Denne funktion kører kontinuerligt og læser gamepad-tilstand: // =================================== // HOVED GAMEPAD OPDATERING LOOP // ===================================
function updateGamepad() { // Få alle tilsluttede gamepads const gamepads = navigator.getGamepads(); hvis (!gamepads) vender tilbage;
// Brug den første tilsluttede gamepad const gp = gamepads[0];
if (gp) { // Opdater knaptilstande ved at skifte til "aktiv" klasse btnA.classList.toggle("aktiv", gp.knapper[0].trykt); btnB.classList.toggle("aktiv", gp.knapper[1].trykt); btnX.classList.toggle("aktiv", gp.knapper[2].trykt);
// Håndter pauseknap (knapindeks 9 på de fleste controllere) const pausePressed = gp.buttons[9].pressed; pause1.classList.toggle("aktiv", pausePressed); pause2.classList.toggle("aktiv", pausePressed);
// Opbyg en liste over aktuelt trykket knapper til statusvisning lad trykket = []; gp.buttons.forEach((btn, i) => { if (btn.pressed)pressed.push("Knap " + i); });
// Opdater statustekst, hvis der trykkes på nogen knapper if (trykt.længde > 0) { status.textContent = "Tryktet: " + presset.join(", "); } }
// Fortsæt løkken, hvis debugger kører if (løber) { rafId = requestAnimationFrame(updateGamepad); } }
ClassList.toggle()-metoden tilføjer eller fjerner den aktive klasse baseret på, om der trykkes på knappen, hvilket udløser vores CSS-lagstile. Trin 5: Håndter tastaturbegivenheder Disse begivenhedslyttere får tastaturet til at fungere: // =================================== // KEYBOARD EVENT HÅNDTERE // ===================================
document.addEventListener("keydown", (e) => { if (keyMap[e.key]) { // Håndter enkelte eller flere elementer if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("active")); } andet { keyMap[e.key].classList.add("aktiv"); } status.textContent = "Trykt på tast: " + e.key.toUpperCase(); } });
document.addEventListener("keyup", (e) => { if (keyMap[e.key]) { // Fjern aktiv tilstand, når nøglen slippes if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("active")); } andet { keyMap[e.key].classList.remove("aktiv"); } status.textContent = "Nøgle frigivet: " + e.key.toUpperCase(); } });
Trin 6: Tilføj Start/Stop-kontrol Endelig har vi brug for en måde at slå fejlfinderen til og fra: // =================================== // SKIFT DEBUGGER TIL/FRA // ===================================
document.getElementById("toggle").addEventListener("klik", () => { løbende = !løbende; // Vend køretilstanden
if (løber) { status.textContent = "Debugger kører..."; updateGamepad(); // Start opdateringssløjfen } andet { status.textContent = "Debugger inaktiv"; cancelAnimationFrame(rafId); // Stop løkken } });
Så ja, tryk på en knap og den lyser. Skub pinden og den bevæger sig. Det er det. En ting mere: rå værdier. Nogle gange vil man bare se tal, ikke lys.
På dette stadium skal du se:
En simpel controller på skærmen, Knapper, der reagerer, mens du interagerer med dem, og En valgfri debug-udlæsning, der viser indekser for trykket på knapper.
For at gøre dette mindre abstrakt er her en hurtig demo af skærmcontrolleren, der reagerer i realtid:
Når du trykker på Start optagelse, logges alt, indtil du trykker på Stop optagelse. 2. Eksport af data til CSV/JSON Når vi har en log, vil vi gerne gemme den.
Trin 1: Opret Download-hjælperen For det første har vi brug for en hjælpefunktion, der håndterer fildownloads i browseren: // =================================== // FIL DOWNLOAD HJÆLP // ===================================
function downloadFile(filnavn, indhold, type = "tekst/almindelig") { // Opret en klat fra indholdet const blob = new Blob([indhold], { type }); const url = URL.createObjectURL(blob);
// Opret et midlertidigt downloadlink, og klik på det const a = document.createElement("a"); a.href = url; a.download = filnavn; a.klik();
// Ryd op i objektets URL efter download setTimeout(() => URL.revokeObjectURL(url), 100); }
Denne funktion fungerer ved at oprette en Blob (binært stort objekt) ud fra dine data, generere en midlertidig URL til det, og programmatisk klikke på et downloadlink. Oprydningen sikrer, at vi ikke lækker hukommelse. Trin 2: Håndter JSON-eksport JSON er perfekt til at bevare den komplette datastruktur:
// =================================== // EKSPORTER SOM JSON // ===================================
document.getElementById("export-json").addEventListener("click", () => { // Tjek om der er noget at eksportere if (!frames.length) { console.warn("Ingen optagelse tilgængelig at eksportere."); returnere; }
// Opret en nyttelast med metadata og frames const nyttelast = { oprettet på: ny dato().toISOString(), rammer };
// Download som formateret JSON download fil( "gamepad-log.json", JSON.stringify(nyttelast, null, 2), "applikation/json" ); });
JSON-formatet holder alt struktureret og let parseligt, hvilket gør det ideelt til at indlæse tilbage i udviklerværktøjer eller dele med holdkammerater. Trin 3: Håndter CSV-eksport For CSV-eksporter skal vi udjævne de hierarkiske data i rækker og kolonner:
//=================================== // EKSPORTER SOM CSV // ===================================
document.getElementById("export-csv").addEventListener("click", () => { // Tjek om der er noget at eksportere if (!frames.length) { console.warn("Ingen optagelse tilgængelig at eksportere."); returnere; }
// Byg CSV-overskriftsrække (kolonner for tidsstempel, alle knapper, alle akser) const headerButtons = frames[0].buttons.map((_, i) => btn${i}); const headerAxes = frames[0].axes.map((_, i) => akse${i}); const header = ["t", ...headerButtons, ...headerAxes].join(",") + "\n";
// Byg CSV-datarækker const rows = frames.map(f => { const btnVals = f.buttons.map(b => b.value); return [f.t, ...btnVals, ...f.axes].join(","); }).join("\n");
// Download som CSV downloadFile("gamepad-log.csv", overskrift + rækker, "tekst/csv"); });
CSV er genialt til dataanalyse, fordi det åbnes direkte i Excel eller Google Sheets, så du kan oprette diagrammer, filtrere data eller se mønstre visuelt. Nu hvor eksportknapperne er i, vil du se to nye muligheder på panelet: Eksporter JSON og Eksporter CSV. JSON er rart, hvis du vil smide den rå log tilbage i dine dev-værktøjer eller rode rundt i strukturen. CSV åbner på den anden side direkte ind i Excel eller Google Sheets, så du kan kortlægge, filtrere eller sammenligne input. Følgende figur viser, hvordan panelet ser ud med de ekstra kontroller.
3. Snapshot System Nogle gange har du ikke brug for en fuld optagelse, bare et hurtigt "skærmbillede" af inputtilstande. Det er her, en Take Snapshot-knap hjælper.
Og JavaScript:
// =================================== // TAG SNAPSHOT // ===================================
document.getElementById("snapshot").addEventListener("click", () => { // Få alle tilsluttede gamepads const pads = navigator.getGamepads(); const activePads = [];
// Gå igennem og indfang tilstanden for hver tilsluttet gamepad for (konst gp af puder) { hvis (!gp) fortsætter; // Spring over tomme pladser
activePads.push({ id: gp.id, // Controllernavn/model tidsstempel: performance.now(), knapper: gp.buttons.map(b => ({ trykket: b. trykket, værdi: b.værdi })), akser: [...gp.akser] }); }
// Tjek om der blev fundet gamepads if (!activePads.length) { console.warn("Ingen gamepads tilsluttet til snapshot."); alert("Ingen controller fundet!"); returnere; }
// Log og underret bruger console.log("Snapshot:", activePads); alert(Snapshot taget! Fanget ${activePads.length} controller(s).); });
Snapshots fryser den nøjagtige tilstand af din controller på et tidspunkt. 4. Ghost Input Replay Nu til det sjove: gentagelse af spøgelsesinput. Dette tager en log og afspiller den visuelt, som om en fantomafspiller brugte controlleren.
JavaScript til afspilning: // =================================== // SPØGELSE AFSPIL // ===================================
document.getElementById("replay").addEventListener("click", () => { // Sørg for, at vi har en optagelse, der skal afspilles if (!frames.length) { alert("Ingen optagelse at afspille!"); returnere; }
console.log("Starter spøgelsesafspilning...");
// Sporetiming til synkroniseret afspilning lad startTime = performance.now(); lad frameIndex = 0;
// Afspil animationsløkke igen funktionstrin() { const nu = ydeevne.nu(); const elapsed = nu - startTime;
// Behandle alle frames, der skulle være opstået nu while (frameIndex < frames.length && frames[frameIndex].t <= elapsed) { const frame = frames[frameIndex];
// Opdater UI med de optagede knaptilstande btnA.classList.toggle("active", frame.buttons[0].pressed); btnB.classList.toggle("active", frame.buttons[1].pressed); btnX.classList.toggle("active", frame.buttons[2].pressed);
// Opdater statusvisning lad trykket = []; frame.buttons.forEach((btn, i) => { if (btn.pressed) pressed.push("Knap " + i); }); if (trykt.længde > 0) { status.textContent = "Ghost: " + pressed.join(", "); }
frameIndex++; }
// Fortsæt loop, hvis der er flere rammer if (frameIndex < frames.length) { requestAnimationFrame(trin); } andet { console.log("Genafspilfærdig."); status.textContent = "Genafspilning fuldført"; } }
// Start afspilningen trin(); });
For at gøre fejlsøgningen lidt mere praktisk tilføjede jeg en spøgelsesreplay. Når du har optaget en session, kan du trykke på afspilning og se brugergrænsefladen udspille den, næsten som en fantomafspiller kører puden. En ny Replay Ghost-knap dukker op i panelet til dette.
Tryk på Optag, rode lidt med controlleren, stop og afspil derefter. Brugergrænsefladen afspejler bare alt, hvad du gjorde, som et spøgelse, der følger dine input. Hvorfor bøvle med disse ekstramateriale?
Optagelse/eksport gør det nemt for testere at vise præcis, hvad der skete. Snapshots fryser et øjeblik i tiden, super nyttigt, når du jagter mærkelige fejl. Ghost replay er fantastisk til selvstudier, tilgængelighedstjek eller bare sammenligning af kontrolopsætninger side om side.
På dette tidspunkt er det ikke bare en pæn demo længere, men noget, du faktisk kunne sætte i gang. Real-World Use Cases Nu har vi denne debugger, der kan gøre meget. Det viser live input, optager logfiler, eksporterer dem og afspiller endda ting. Men det virkelige spørgsmål er: hvem bekymrer sig egentlig? Hvem er dette nyttigt for? Spiludviklere Controllere er en del af jobbet, men fejlfinder du dem? Normalt en smerte. Forestil dig, at du tester en kampspilskombination, som ↓ → + punch. I stedet for at bede trykkede du på den på samme måde to gange, du optager den en gang og afspiller den igen. Færdig. Eller du bytter JSON-logfiler med en holdkammerat for at tjekke, om din multiplayer-kode reagerer på samme måde på deres maskine. Det er kæmpestort. Tilgængelighedspraktikere Denne ligger mit hjerte nært. Ikke alle spiller med en "standard" controller. Adaptive controllere smider nogle gange mærkelige signaler ud. Med dette værktøj kan du se præcis, hvad der sker. Lærere, forskere, hvem som helst. De kan gribe logfiler, sammenligne dem eller afspille input side om side. Pludselig bliver usynlige ting tydelige. Kvalitetssikringstest Testere skriver normalt noter som "Jeg mosede knapper her, og den gik i stykker." Ikke særlig hjælpsom. Nu? De kan fange de nøjagtige presser, eksportere loggen og sende den af sted. Ingen gæt. Pædagoger Hvis du laver tutorials eller YouTube-videoer, er spøgelsesafspilning guld. Du kan bogstaveligt talt sige: "Her er, hvad jeg gjorde med controlleren", mens brugergrænsefladen viser, at det sker. Gør forklaringer langt klarere. Ud over spil Og ja, det her handler ikke kun om spil. Folk har brugt controllere til robotter, kunstprojekter og tilgængelighedsgrænseflader. Samme problem hver gang: hvad ser browseren egentlig? Med dette behøver du ikke at gætte. Konklusion Fejlretning af en controller-input har altid føltes som at flyve i blinde. I modsætning til DOM eller CSS er der ingen indbygget inspektør til gamepads; det er bare rå numre i konsollen, let tabt i støjen. Med et par hundrede linjer HTML, CSS og JavaScript byggede vi noget andet:
En visuel debugger, der gør usynlige input synlige. Et lagdelt CSS-system, der holder brugergrænsefladen ren og fejlfindbar. Et sæt forbedringer (optagelse, eksport, snapshots, spøgelsesafspilning), der løfter det fra demo til udviklerværktøj.
Dette projekt viser, hvor langt du kan nå ved at blande webplatformens kraft med lidt kreativitet i CSS Cascade Layers. Værktøjet, jeg lige har forklaret i sin helhed, er open source. Du kan klone GitHub-repoen og prøve det selv. Men endnu vigtigere, du kan gøre det til dit eget. Tilføj dine egne lag. Byg din egen replay-logik. Integrer det med din spilprototype. Eller endda bruge det på måder, jeg ikke har forestillet mig. Til undervisning, tilgængelighed eller dataanalyse. I slutningen af dagen handler dette ikke kun om fejlfinding af gamepads. Det handler om at kaste lys over skjulte input og give udviklere selvtilliden til at arbejde med hardware, som nettet stadig ikke fuldt ud omfavner. Så tilslut din controller, åbn din editor, og begynd at eksperimentere. Du kan blive overrasket over, hvad din browser og din CSS virkelig kan udrette.