Når du kobler til en kontroller, knuser du knapper, flytter pinnene, trekker avtrekkerne ... og som utvikler ser du ingenting av det. Nettleseren plukker det opp, men med mindre du logger tall i konsollen, er det usynlig. Det er hodepinen med Gamepad API. Det har eksistert i årevis, og det er faktisk ganske kraftig. Du kan lese knapper, pinner, triggere, verkene. Men de fleste rører det ikke. Hvorfor? For det er ingen tilbakemelding. Ingen panel i utviklerverktøy. Ingen klar måte å vite om kontrolleren i det hele tatt gjør det du tror. Det føles som å fly blind. Det forstyrret meg nok til å bygge et lite verktøy: Gamepad Cascade Debugger. I stedet for å stirre på konsollens utgang, får du en levende, interaktiv visning av kontrolleren. Trykk på noe og det reagerer på skjermen. Og med CSS Cascade Layers forblir stilene organisert, så det er renere å feilsøke. I dette innlegget skal jeg vise deg hvorfor det er så vondt å feilsøke kontrollere, hvordan CSS hjelper til med å rydde opp, og hvordan du kan bygge en gjenbrukbar visuell debugger for dine egne prosjekter.
Selv om du er i stand til å logge dem alle, vil du raskt ende opp med uleselig konsollspam. 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 fortelle hvilken knapp som ble trykket på? Kanskje, men bare etter å ha anstrengt øynene og savnet noen få innganger. Så nei, feilsøking kommer ikke lett når det gjelder å lese inndata. Oppgave 3: Mangel på struktur Selv om du setter sammen en rask visualisering, kan stiler fort bli rotete. Standard-, aktiv- og feilsøkingstilstander kan overlappe, og uten en klar struktur blir CSS-en din sprø og vanskelig å utvide. CSS Cascade Layers kan hjelpe. De grupperer stiler i "lag" som er sortert etter prioritet, så du slutter å kjempe mot spesifisitet og gjette: "Hvorfor vises ikke feilsøkingsstilen min?" I stedet opprettholder du separate bekymringer:
Base: Kontrollerens standard, første utseende. Aktiv: Høydepunkter for trykte knapper og bevegede pinner. Feilsøking: Overlegg for utviklere (f.eks. numeriske avlesninger, veiledninger og så videre).
Hvis vi skulle definere lag i CSS i henhold til dette, ville vi ha: /* laveste til høyeste prioritet */ @lagsbase, aktiv, feilsøking;
@layer base { /* ... */ }
@lag aktivt { /* ... */ }
@layer debug { /* ... */ }
Fordi hvert lag stabler forutsigbart, vet du alltid hvilke regler som vinner. Den forutsigbarheten gjør feilsøking ikke bare enklere, men faktisk håndterlig. Vi har dekket problemet (usynlig, rotete input) og tilnærmingen (en visuell debugger bygget med Cascade Layers). Nå skal vi gå gjennom trinn-for-trinn-prosessen for å bygge feilsøkeren. Debugger-konseptet Den enkleste måten å gjøre skjulte input synlige på er å bare tegne den på skjermen. Det er det denne feilsøkeren gjør. Knapper, utløsere og styrespaker får alle et visuelt bilde.
Trykk A: En sirkel lyser opp. Dytt pinnen: Sirkelen glir rundt. Trekk en avtrekker halvveis: En stolpe fylles halvveis.
Nå stirrer du ikke på 0-er og 1-ere, men ser faktisk kontrolleren reagere live. Selvfølgelig, når du begynner å samle på tilstander som standard, trykket, feilsøkingsinformasjon, kanskje til og med en opptaksmodus, begynner CSS å bli større og mer kompleks. Det er der kaskadelag kommer godt med. Her er et nedstrippet eksempel: @layer base { .button { bakgrunn: #222; kantradius: 50 %; bredde: 40px; høyde: 40px; } }
@lag aktivt { .button.pressed { bakgrunn: #0f0; /* lys grønn */ } }
@layer debug { .button::etter { innhold: attr(data-verdi); skriftstørrelse: 12px; farge: #fff; } }
Lagrekkefølgen er viktig: base → aktiv → feilsøking.
base trekker kontrolleren. aktive håndterer trykket tilstander. feilsøkingskast på overlegg.
Å bryte det opp slik betyr at du ikke kjemper rare spesifisitetskriger. Hvert lag har sin plass, og du vet alltid hva som vinner. Bygger det ut La oss få noe på skjermen først. Det trenger ikke å se bra ut – det trenger bare å eksistere så vi har noe å jobbe med.
Gamepad Cascade Debugger
Det er bokstavelig talt bare bokser. Ikke spennende ennå, men det gir oss håndtak å ta tak i senere med CSS og JavaScript. Ok, jeg bruker kaskadelag her fordi det holder ting organisert når du legger til flere tilstander. Her er en grov pasning:
/* =================================== CASCADE LAG OPPSETT Bestilling er viktig: base → aktiv → feilsøking ===================================== */
/* Definer lagrekkefølge på forhånd */ @lagsbase, aktiv, feilsøking;
/* Lag 1: Grunnstiler - standardutseende */ @layer base { .button { bakgrunn: #333; kantradius: 50 %; bredde: 70px; høyde: 70px; display: flex; rettferdiggjøre-innhold: senter; align-items: center; }
.pause { bredde: 20px; høyde: 70px; bakgrunn: #333; display: inline-blokk; } }
/* Lag 2: Aktive tilstander - håndterer trykte knapper */ @lag aktivt { .button.active { bakgrunn: #0f0; /* Lys grønn når du trykker på */ transform: skala(1.1); /* Forstørrer knappen litt */ }
.pause.active { bakgrunn: #0f0; transform: skalaY(1.1); /* Strekkes vertikalt når den trykkes */ } }
/* Lag 3: Feilsøkingsoverlegg – utviklerinformasjon */ @layer debug { .button::etter { innhold: attr(data-verdi); /* Viser den numeriske verdien */ skriftstørrelse: 12px; farge: #fff; } }
Det fine med denne tilnærmingen er at hvert lag har en klar hensikt. Grunnlaget kan aldri overstyre aktiv, og aktiv kan aldri overstyre feilsøking, uavhengig av spesifisitet. Dette eliminerer CSS-spesifisitetskrigene som vanligvis plager feilsøkingsverktøy. Nå ser det ut til at noen klynger sitter på en mørk bakgrunn. Ærlig talt, ikke så verst.
Legger til JavaScript JavaScript-tid. Det er her kontrolleren faktisk gjør noe. Vi bygger dette trinn for trinn. Trinn 1: Konfigurer State Management Først trenger vi variabler for å spore feilsøkerens tilstand: // =================================== // STATSLEDELSE // ===================================
la løpe = falsk; // Sporer om feilsøkeren er aktiv la rafId; // Lagrer requestAnimationFrame ID for kansellering
Disse variablene kontrollerer animasjonssløyfen som kontinuerlig leser gamepad-inndata. Trinn 2: Ta tak i DOM-referanser Deretter får vi referanser til alle HTML-elementene vi skal oppdatere: // =================================== // DOM ELEMENT REFERANSER // ===================================
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");
Å lagre disse referansene på forhånd er mer effektivt enn å spørre DOM gjentatte ganger. Trinn 3: Legg til tastaturreserve For testing uten en fysisk kontroller, tilordner vi tastaturtaster til knapper: // =================================== // KEYBOARD FALLBACK (for testing uten kontroller) // ===================================
const keyMap = { "a": btnA, "b": btnB, "x": btnX, "p": [pause1, pause2] // 'p'-tasten kontrollerer begge pauselinjene };
Dette lar oss teste brukergrensesnittet ved å trykke på tastene på et tastatur. Trinn 4: Lag hovedoppdateringssløyfen Det er her magien skjer. Denne funksjonen kjører kontinuerlig og leser gamepad-status: // =================================== // HOVED GAMEPAD OPPDATERINGSLOOP // ===================================
function updateGamepad() { // Få alle tilkoblede gamepads const gamepads = navigator.getGamepads(); hvis (!gamepads) returnerer;
// Bruk den første tilkoblede gamepaden const gp = gamepads[0];
if (gp) { // Oppdater knappens tilstander ved å veksle mellom "aktiv" klasse btnA.classList.toggle("active", gp.buttons[0].pressed); btnB.classList.toggle("aktiv", gp.knapper[1].trykket); btnX.classList.toggle("aktiv", gp.knapper[2].trykket);
// Håndter pauseknapp (knappindeks 9 på de fleste kontrollere) const pausePressed = gp.buttons[9].pressed; pause1.classList.toggle("aktiv", pauseTrykt); pause2.classList.toggle("aktiv", pausePressed);
// Bygg en liste over knapper som er trykket for øyeblikket for statusvisning la trykket = []; gp.buttons.forEach((btn, i) => { if (btn.pressed)pressed.push("Knapp " + i); });
// Oppdater statustekst hvis noen knapper trykkes if (trykket.lengde > 0) { status.textContent = "Tryktet: " + presset.join(", "); } }
// Fortsett løkken hvis debugger kjører if (løper) { rafId = requestAnimationFrame(updateGamepad); } }
ClassList.toggle()-metoden legger til eller fjerner den aktive klassen basert på om knappen trykkes, noe som utløser CSS-lagstilene våre. Trinn 5: Håndter tastaturhendelser Disse hendelseslyttere får tastaturet til å fungere: // =================================== // KEYBOARD EVENTS HANDLER // ===================================
document.addEventListener("keydown", (e) => { if (keyMap[e.key]) { // Håndter enkelt eller flere elementer if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("active")); } annet { keyMap[e.key].classList.add("aktiv"); } status.textContent = "Tast trykket: " + e.key.toUpperCase(); } });
document.addEventListener("keyup", (e) => { if (keyMap[e.key]) { // Fjern aktiv tilstand når nøkkelen slippes if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("active")); } annet { keyMap[e.key].classList.remove("aktiv"); } status.textContent = "Nøkkel frigitt: " + e.key.toUpperCase(); } });
Trinn 6: Legg til start/stopp-kontroll Til slutt trenger vi en måte å slå feilsøkeren på og av: // =================================== // SLÅ DEBUGGER PÅ/AV // ===================================
document.getElementById("toggle").addEventListener("klikk", () => { løping = !løper; // Vend kjøretilstanden
if (løper) { status.textContent = "Debugger kjører..."; updateGamepad(); // Start oppdateringssløyfen } annet { status.textContent = "Debugger inaktiv"; cancelAnimationFrame(rafId); // Stopp sløyfen } });
Så ja, trykk på en knapp og den lyser. Skyv pinnen og den beveger seg. Det er det. En ting til: råverdier. Noen ganger vil du bare se tall, ikke lys.
På dette stadiet bør du se:
En enkel kontroller på skjermen, Knapper som reagerer når du samhandler med dem, og En valgfri feilsøkingsavlesning som viser indekser for trykte knapper.
For å gjøre dette mindre abstrakt, her er en rask demo av kontrolleren på skjermen som reagerer i sanntid:
Nå, ved å trykke på Start opptak logger du alt til du trykker på Stopp opptak. 2. Eksportere data til CSV/JSON Når vi har en logg, vil vi lagre den.
Trinn 1: Lag nedlastingshjelperen Først trenger vi en hjelpefunksjon som håndterer filnedlastinger i nettleseren: // =================================== // LAST NED HJELPEREN // ===================================
function downloadFile(filnavn, innhold, type = "tekst/vanlig") { // Lag en blob fra innholdet const blob = new Blob([innhold], { type }); const url = URL.createObjectURL(blob);
// Opprett en midlertidig nedlastingslenke og klikk på den const a = document.createElement("a"); a.href = url; a.nedlasting = filnavn; a.klikk();
// Rydd opp i objekt-URLen etter nedlasting setTimeout(() => URL.revokeObjectURL(url), 100); }
Denne funksjonen fungerer ved å lage en Blob (binært stort objekt) fra dataene dine, generere en midlertidig URL for det, og programmatisk klikke på en nedlastingslenke. Oppryddingen sikrer at vi ikke lekker minne. Trinn 2: Håndter JSON-eksport JSON er perfekt for å bevare den komplette datastrukturen:
// =================================== // EKSPORTER SOM JSON // ===================================
document.getElementById("export-json").addEventListener("klikk", () => { // Sjekk om det er noe å eksportere if (!frames.length) { console.warn("Ingen opptak tilgjengelig for eksport."); returnere; }
// Lag en nyttelast med metadata og rammer const nyttelast = { opprettet ved: ny dato().toISOString(), rammer };
// Last ned som formatert JSON last ned fil( "gamepad-log.json", JSON.stringify(nyttelast, null, 2), "applikasjon/json" ); });
JSON-formatet holder alt strukturert og enkelt parsebart, noe som gjør det ideelt for å laste tilbake til utviklerverktøy eller dele med lagkamerater. Trinn 3: Håndter CSV-eksport For CSV-eksporter må vi flate ut de hierarkiske dataene i rader og kolonner:
//=================================== // EKSPORTER SOM CSV // ===================================
document.getElementById("export-csv").addEventListener("click", () => { // Sjekk om det er noe å eksportere if (!frames.length) { console.warn("Ingen opptak tilgjengelig for eksport."); returnere; }
// Bygg CSV-overskriftsrad (kolonner for tidsstempel, alle knapper, alle akser) const headerButtons = frames[0].buttons.map((_, i) => btn${i}); const headerAxes = rammer[0].axes.map((_, i) => akse${i}); const header = ["t", ...headerButtons, ...headerAxes].join(",") + "\n";
// Bygg CSV-datarader const rows = frames.map(f => { const btnVals = f.buttons.map(b => b.value); return [f.t, ...btnVals, ...f.axes].join(","); }).join("\n");
// Last ned som CSV downloadFile("gamepad-log.csv", overskrift + rader, "tekst/csv"); });
CSV er genialt for dataanalyse fordi den åpnes direkte i Excel eller Google Sheets, slik at du kan lage diagrammer, filtrere data eller se mønstre visuelt. Nå som eksportknappene er inne, vil du se to nye alternativer på panelet: Eksporter JSON og Eksporter CSV. JSON er fint hvis du vil kaste den rå loggen tilbake i utviklerverktøyene dine eller rote rundt strukturen. CSV, på den annen side, åpnes rett inn i Excel eller Google Sheets slik at du kan kartlegge, filtrere eller sammenligne inndata. Følgende figur viser hvordan panelet ser ut med de ekstra kontrollene.
3. Snapshot System Noen ganger trenger du ikke et fullstendig opptak, bare et raskt "skjermbilde" av inngangstilstander. Det er der en ta et øyeblikksbilde-knapp hjelper.
Og JavaScript:
// =================================== // TA STILLBILDE // ===================================
document.getElementById("snapshot").addEventListener("click", () => { // Få alle tilkoblede gamepads const pads = navigator.getGamepads(); const activePads = [];
// Gå gjennom og fange statusen til hver tilkoblede gamepad for (konst gp av pads) { hvis (!gp) fortsetter; // Hopp over tomme spor
activePads.push({ id: gp.id, // Kontrollernavn/modell tidsstempel: performance.now(), knapper: gp.buttons.map(b => ({ trykket: b. trykket, verdi: b.verdi })), akser: [...gp.akser] }); }
// Sjekk om noen gamepads ble funnet if (!activePads.length) { console.warn("Ingen spillkontroller tilkoblet for øyeblikksbilde."); alert("Ingen kontroller oppdaget!"); returnere; }
// Logg og varsle bruker console.log("Øyeblikksbilde:", activePads); alert(Øyeblikksbilde tatt! Fanget ${activePads.length} kontroller(er).); });
Øyeblikksbilder fryser den nøyaktige tilstanden til kontrolleren på ett øyeblikk. 4. Ghost Input Replay Nå til det morsomme: replay av spøkelsesinnspill. Dette tar en logg og spiller den av visuelt som om en fantomspiller brukte kontrolleren.
JavaScript for replay: // =================================== // SPØKELSE REPLAY // ===================================
document.getElementById("replay").addEventListener("klikk", () => { // Sørg for at vi har et opptak å spille av if (!frames.length) { alert("Ingen opptak å spille av!"); returnere; }
console.log("Starter spøkelsesreplay...");
// Sporetiming for synkronisert avspilling la startTime = ytelse.nå(); la frameIndex = 0;
// Spill av animasjonssløyfe på nytt funksjon trinn() { const nå = ytelse.nå(); const elapsed = nå - starttid;
// Behandle alle rammer som skulle ha oppstått nå while (frameIndex < frames.length && frames[frameIndex].t <= elapsed) { const frame = frames[frameIndex];
// Oppdater brukergrensesnittet med de registrerte knappene btnA.classList.toggle("active", frame.buttons[0].pressed); btnB.classList.toggle("active", frame.buttons[1].pressed); btnX.classList.toggle("aktiv", frame.buttons[2].pressed);
// Oppdater statusvisning la trykket = []; frame.buttons.forEach((btn, i) => { if (btn.pressed) pressed.push("Knapp " + i); }); if (trykket.lengde > 0) { status.textContent = "Ghost: " + pressed.join(", "); }
frameIndex++; }
// Fortsett loop hvis det er flere rammer if (frameIndex < frames.length) { requestAnimationFrame(trinn); } annet { console.log("Spill på nyttferdig."); status.textContent = "Avspilling fullført"; } }
// Start reprisen trinn(); });
For å gjøre feilsøkingen litt mer praktisk, la jeg til en spøkelsesreplay. Når du har spilt inn en økt, kan du trykke på replay og se brukergrensesnittet spille det ut, nesten som en fantomspiller kjører pad. En ny Replay Ghost-knapp vises i panelet for dette.
Trykk på Record, rot litt med kontrolleren, stopp og spill av. Brukergrensesnittet gjenspeiler bare alt du gjorde, som et spøkelse som følger inndataene dine. Hvorfor bry seg med disse ekstramaterialene?
Opptak/eksport gjør det enkelt for testere å vise nøyaktig hva som skjedde. Øyeblikksbilder fryser et øyeblikk, veldig nyttig når du jakter på rare feil. Ghost replay er flott for veiledninger, tilgjengelighetssjekker eller bare sammenligning av kontrolloppsett side ved side.
På dette tidspunktet er det ikke bare en pen demo lenger, men noe du faktisk kan sette i gang. Reelle brukstilfeller Nå har vi denne feilsøkeren som kan gjøre mye. Den viser live input, registrerer logger, eksporterer dem og til og med spiller av ting på nytt. Men det virkelige spørsmålet er: hvem bryr seg egentlig? Hvem er dette nyttig for? Spillutviklere Kontrollere er en del av jobben, men feilsøke dem? Vanligvis en smerte. Tenk deg at du tester en kampspillkombinasjon, som ↓ → + slag. I stedet for å be, trykket du på den på samme måte to ganger, du tar den opp en gang og spiller den på nytt. Ferdig. Eller du bytter JSON-logger med en lagkamerat for å sjekke om flerspillerkoden din reagerer likt på maskinen deres. Det er enormt. Tilgjengelighetsutøvere Denne ligger mitt hjerte nært. Ikke alle spiller med en "standard" kontroller. Adaptive kontrollere kaster ut rare signaler noen ganger. Med dette verktøyet kan du se nøyaktig hva som skjer. Lærere, forskere, hvem som helst. De kan hente logger, sammenligne dem eller spille av innganger side ved side. Plutselig blir usynlige ting åpenbare. Kvalitetssikringstesting Testere skriver vanligvis notater som "Jeg moset knapper her og det gikk i stykker." Ikke veldig nyttig. Nå? De kan fange de nøyaktige pressene, eksportere loggen og sende den av gårde. Ingen gjetting. Lærere Hvis du lager opplæringsprogrammer eller YouTube-videoer, er spøkelsesreplay gull. Du kan bokstavelig talt si: "Her er hva jeg gjorde med kontrolleren," mens brukergrensesnittet viser at det skjer. Gjør forklaringer mye klarere. Beyond Games Og ja, dette handler ikke bare om spill. Folk har brukt kontrollere for roboter, kunstprosjekter og tilgjengelighetsgrensesnitt. Samme problem hver gang: hva ser nettleseren egentlig? Med dette trenger du ikke å gjette. Konklusjon Å feilsøke en kontrollerinngang har alltid føltes som å fly i blinde. I motsetning til DOM eller CSS, er det ingen innebygd inspektør for gamepads; det er bare rå tall i konsollen, som lett blir borte i støyen. Med noen hundre linjer med HTML, CSS og JavaScript bygde vi noe annerledes:
En visuell debugger som gjør usynlige innganger synlige. Et lagdelt CSS-system som holder brukergrensesnittet rent og feilsøkbart. Et sett med forbedringer (opptak, eksport, øyeblikksbilder, spøkelsesavspilling) som løfter det fra demo til utviklerverktøy.
Dette prosjektet viser hvor langt du kan gå ved å blande nettplattformens kraft med litt kreativitet i CSS Cascade Layers. Verktøyet jeg nettopp forklarte i sin helhet er åpen kildekode. Du kan klone GitHub-repoen og prøve den selv. Men enda viktigere, du kan gjøre den til din egen. Legg til dine egne lag. Bygg din egen replay-logikk. Integrer den med spillprototypen din. Eller til og med bruke den på måter jeg ikke har forestilt meg. For undervisning, tilgjengelighet eller dataanalyse. På slutten av dagen handler dette ikke bare om feilsøking av gamepads. Det handler om å kaste lys over skjulte innganger, og gi utviklere selvtilliten til å jobbe med maskinvare som nettet fortsatt ikke fullt ut omfavner. Så plugg inn kontrolleren, åpne editoren og begynn å eksperimentere. Du kan bli overrasket over hva nettleseren din og CSS-en din virkelig kan utrette.