När du kopplar in en handkontroll, mosar du knappar, flyttar pinnarna, drar i avtryckarna... och som utvecklare ser du inget av det. Webbläsaren plockar upp det, visst, men om du inte loggar nummer i konsolen är det osynligt. Det är huvudvärken med Gamepad API. Det har funnits i flera år, och det är faktiskt ganska kraftfullt. Du kan läsa knappar, stickor, triggers, verken. Men de flesta rör det inte. Varför? För det finns ingen feedback. Ingen panel i utvecklarverktyg. Inget tydligt sätt att veta om kontrollern ens gör vad du tror. Det känns som att flyga blind. Det störde mig tillräckligt för att bygga ett litet verktyg: Gamepad Cascade Debugger. Istället för att stirra på konsolutgången får du en levande, interaktiv vy av kontrollern. Tryck på något och det reagerar på skärmen. Och med CSS Cascade Layers förblir stilarna organiserade, så det är renare att felsöka. I det här inlägget kommer jag att visa dig varför det är så jobbigt att felsöka kontroller, hur CSS hjälper till att rensa upp det och hur du kan bygga en återanvändbar visuell debugger för dina egna projekt.

Även om du kan logga dem alla kommer du snabbt att få oläslig konsolspam. Till exempel: [0,0,1,0,0,0,5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]

Kan du säga vilken knapp som trycktes på? Kanske, men bara efter att ha ansträngt ögonen och missat några ingångar. Så nej, felsökning är inte lätt när det gäller att läsa indata. Problem 3: Brist på struktur Även om du sätter ihop en snabb visualiserare kan stilar snabbt bli röriga. Standard, aktiva och felsökningslägen kan överlappa varandra, och utan en tydlig struktur blir din CSS skör och svår att utöka. CSS Cascade Layers kan hjälpa. De grupperar stilar i "lager" som är ordnade efter prioritet, så du slutar bekämpa specificitet och gissa, "Varför visas inte min felsökningsstil?" Istället upprätthåller du separata bekymmer:

Bas: Styrenhetens standard, initiala utseende. Aktiv: Höjdpunkter för nedtryckta knappar och flyttade pinnar. Felsökning: Överlägg för utvecklare (t.ex. numeriska avläsningar, guider och så vidare).

Om vi skulle definiera lager i CSS enligt detta, skulle vi ha: /* lägsta till högsta prioritet */ @lagerbas, aktiv, debug;

@lagerbas { /* ... */ }

@lager aktivt { /* ... */ }

@lager felsöka { /* ... */ }

Eftersom varje lager staplas förutsägbart vet du alltid vilka regler som vinner. Den förutsägbarheten gör felsökning inte bara lättare, utan faktiskt hanterbar. Vi har täckt problemet (osynlig, rörig ingång) och tillvägagångssättet (en visuell debugger byggd med Cascade Layers). Nu ska vi gå igenom steg-för-steg-processen för att bygga felsökaren. Debugger-konceptet Det enklaste sättet att göra dold input synlig är att bara rita den på skärmen. Det är vad den här debuggern gör. Knappar, triggers och joysticks får alla en bild.

Tryck på A: En cirkel tänds. Knuffa pinnen: Cirkeln glider runt. Dra en avtryckare halvvägs: En stapel fylls halvvägs.

Nu stirrar du inte på 0:or och 1:or, utan ser faktiskt kontrollenheten reagera live. Naturligtvis, när du börjar samla på tillstånd som standard, nedtryckt, felsökningsinformation, kanske till och med ett inspelningsläge, börjar CSS bli större och mer komplex. Det är där kaskadlager kommer till användning. Här är ett avskalat exempel: @lagerbas { .button { bakgrund: #222; gränsradie: 50%; bredd: 40px; höjd: 40px; } }

@lager aktivt { .button.pressed { bakgrund: #0f0; /* ljusgrön */ } }

@lager felsöka { .button::efter { innehåll: attr(data-värde); teckenstorlek: 12px; färg: #fff; } }

Lagerordningen spelar roll: bas → aktiv → debug.

basen drar styrenheten. aktiva hanterar tryckta tillstånd. felsöka kast på överlägg.

Att bryta upp det så här betyder att du inte utkämpar konstiga specificitetskrig. Varje lager har sin plats, och du vet alltid vad som vinner. Bygger ut det Låt oss få upp något på skärmen först. Det behöver inte se bra ut – behöver bara finnas så att vi har något att jobba med.

Gamepad Cascade Debugger

A
B
X

Debugger inaktiv

Det är bokstavligen bara lådor. Inte spännande än, men det ger oss handtag att ta tag i senare med CSS och JavaScript. Okej, jag använder kaskadlager här eftersom det håller saker organiserade när du lägger till fler tillstånd. Här är ett grovt pass:

/* =================================== INSTÄLLNING AV CASCADE LAYERS Beställning spelar roll: bas → aktiv → felsöka ====================================== */

/* Definiera lagerordning i förväg */ @lagerbas, aktiv, debug;

/* Lager 1: Basstilar - standardutseende */ @lagerbas { .button { bakgrund: #333; gränsradie: 50%; bredd: 70px; höjd: 70px; display: flex; motivera-innehåll: center; align-items: center; }

.pause { bredd: 20px; höjd: 70px; bakgrund: #333; display: inline-block; } }

/* Lager 2: Aktiva tillstånd - hanterar nedtryckta knappar */ @lager aktivt { .button.active { bakgrund: #0f0; /* Ljusgrön när du trycker på */ transform: skala(1.1); /* Förstorar knappen något */ }

.pause.active { bakgrund: #0f0; transform: skalaY(1.1); /* Sträcker sig vertikalt när den trycks ned */ } }

/* Lager 3: Felsökning av överlagringar - utvecklarinformation */ @lager felsöka { .button::efter { innehåll: attr(data-värde); /* Visar det numeriska värdet */ teckenstorlek: 12px; färg: #fff; } }

Det fina med detta tillvägagångssätt är att varje lager har ett tydligt syfte. Baslagret kan aldrig åsidosätta aktiv, och aktiv kan aldrig åsidosätta felsökning, oavsett specificitet. Detta eliminerar CSS-specificitetskrig som vanligtvis plågar felsökningsverktyg. Nu ser det ut som att några kluster sitter på en mörk bakgrund. Ärligt talat, inte så illa.

Lägger till JavaScript JavaScript-tid. Det är här kontrollenheten faktiskt gör något. Vi bygger detta steg för steg. Steg 1: Konfigurera State Management Först behöver vi variabler för att spåra felsökarens tillstånd: // =================================== // STATENS LEDNING // ===================================

låt köra = falskt; // Spårar om felsökaren är aktiv låt rafId; // Lagrar requestAnimationFrame ID för annullering

Dessa variabler styr animationsslingan som kontinuerligt läser gamepad-indata. Steg 2: Ta tag i DOM-referenser Därefter får vi referenser till alla HTML-element som vi kommer att uppdatera: // =================================== // DOM ELEMENT REFERENSER // ===================================

const btnA = document.getElementById("btn-a"); const btnB = document.getElementById("btn-b"); const btnX = document.getElementById("btn-x"); const paus1 = document.getElementById("paus1"); const paus2 = document.getElementById("paus2"); const status = document.getElementById("status");

Att lagra dessa referenser i förväg är effektivare än att fråga DOM upprepade gånger. Steg 3: Lägg till alternativt tangentbord För att testa utan en fysisk kontroller mappar vi tangentbordsknappar till knappar: // =================================== // KEYBOARD FALLBACK (för testning utan styrenhet) // ===================================

const keyMap = { "a": btnA, "b": btnB, "x": btnX, "p": [paus1, paus2] // 'p'-tangenten styr båda pausstaplarna };

Detta låter oss testa användargränssnittet genom att trycka på tangenterna på ett tangentbord. Steg 4: Skapa huvuduppdateringsslingan Här händer magin. Den här funktionen körs kontinuerligt och läser spelplattans tillstånd: // =================================== // UPPDATERING AV HUVUDSPELPADEN // ===================================

function updateGamepad() { // Skaffa alla anslutna gamepads const gamepads = navigator.getGamepads(); om (!gamepads) returnerar;

// Använd den första anslutna gamepaden const gp = gamepads[0];

if (gp) { // Uppdatera knapptillstånd genom att växla mellan den "aktiva" klassen btnA.classList.toggle("active", gp.buttons[0].pressed); btnB.classList.toggle("active", gp.buttons[1].pressed); btnX.classList.toggle("active", gp.buttons[2].pressed);

// Hantera pausknapp (knappindex 9 på de flesta kontroller) const pausePressed = gp.buttons[9].pressed; pause1.classList.toggle("aktiv", pausePressed); pause2.classList.toggle("aktiv", pausePressed);

// Skapa en lista med knappar som för närvarande är nedtryckta för statusvisning låt intryckt = []; gp.buttons.forEach((btn, i) => { if (btn.pressed)pressed.push("Knapp " + i); });

// Uppdatera statustext om någon knapp trycks in if (tryckt.längd > 0) { status.textContent = "Tryckt: " + pressed.join(", "); } }

// Fortsätt slingan om debugger körs if (kör) { rafId = requestAnimationFrame(updateGamepad); } }

Metoden classList.toggle() lägger till eller tar bort den aktiva klassen baserat på om knappen trycks ned, vilket utlöser våra CSS-lagerstilar. Steg 5: Hantera tangentbordshändelser Dessa händelseavlyssnare får tangentbordet att fungera: // =================================== // KEYBOARD HÄNDELSHANTERARE // ===================================

document.addEventListener("keydown", (e) => { if (keyMap[e.key]) { // Hantera enstaka eller flera element if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("active")); } annat { keyMap[e.key].classList.add("aktiv"); } status.textContent = "Tangent nedtryckt: " + e.key.toUpperCase(); } });

document.addEventListener("keyup", (e) => { if (keyMap[e.key]) { // Ta bort aktivt tillstånd när nyckeln släpps if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("active")); } annat { keyMap[e.key].classList.remove("active"); } status.textContent = "Nyckel släppt: " + e.key.toUpperCase(); } });

Steg 6: Lägg till start/stoppkontroll Slutligen behöver vi ett sätt att slå på och av felsökningen: // =================================== // VÄXLA DEBUGGER PÅ/AV // ===================================

document.getElementById("toggle").addEventListener("klick", () => { löpning = !löpning; // Vänd på körläget

if (kör) { status.textContent = "Debugger körs..."; updateGamepad(); // Starta uppdateringsslingan } annat { status.textContent = "Debugger inaktiv"; cancelAnimationFrame(rafId); // Stoppa slingan } });

Så ja, tryck på en knapp så lyser det. Tryck på pinnen och den rör sig. Det är det. En sak till: råa värden. Ibland vill man bara se siffror, inte ljus.

I detta skede bör du se:

En enkel kontroller på skärmen, Knappar som reagerar när du interagerar med dem, och En valfri felsökningsavläsning som visar nedtryckta knappindex.

För att göra detta mindre abstrakt, här är en snabb demo av kontrollenheten på skärmen som reagerar i realtid:

Om du trycker på Starta inspelning loggas allt tills du trycker på Stoppa inspelning. 2. Exportera data till CSV/JSON När vi väl har en logg vill vi spara den.

Steg 1: Skapa nedladdningshjälpen Först behöver vi en hjälpfunktion som hanterar filnedladdningar i webbläsaren: // =================================== // HJÄLPARE FÖR LADDA FIL // ===================================

function downloadFile(filnamn, innehåll, typ = "text/plain") { // Skapa en blob från innehållet const blob = new Blob([innehåll], {typ }); const url = URL.createObjectURL(blob);

// Skapa en tillfällig nedladdningslänk och klicka på den const a = document.createElement("a"); a.href = url; a.download = filnamn; a.click();

// Rensa upp objektets URL efter nedladdning setTimeout(() => URL.revokeObjectURL(url), 100); }

Den här funktionen fungerar genom att skapa en Blob (binärt stort objekt) från dina data, generera en temporär URL för den och klicka programmatiskt på en nedladdningslänk. Rengöringen säkerställer att vi inte läcker minne. Steg 2: Hantera JSON-export JSON är perfekt för att bevara hela datastrukturen:

// =================================== // EXPORTERA SOM JSON // ===================================

document.getElementById("export-json").addEventListener("klick", () => { // Kontrollera om det finns något att exportera if (!frames.length) { console.warn("Ingen inspelning tillgänglig att exportera."); återvända; }

// Skapa en nyttolast med metadata och ramar const nyttolast = { skapad vid: new Date().toISOString(), ramar };

// Ladda ner som formaterad JSON ladda ner fil( "gamepad-log.json", JSON.stringify(nyttolast, null, 2), "applikation/json" ); });

JSON-formatet håller allt strukturerat och lätt att analysera, vilket gör det idealiskt för att ladda tillbaka till utvecklarverktyg eller dela med lagkamrater. Steg 3: Hantera CSV-export För CSV-exporter måste vi platta till hierarkiska data i rader och kolumner:

//=================================== // EXPORTERA SOM CSV // ===================================

document.getElementById("export-csv").addEventListener("klick", () => { // Kontrollera om det finns något att exportera if (!frames.length) { console.warn("Ingen inspelning tillgänglig att exportera."); återvända; }

// Bygg CSV-rubrikrad (kolumner för tidsstämpel, alla knappar, alla axlar) const headerButtons = frames[0].buttons.map((_, i) => btn${i}); const headerAxes = frames[0].axes.map((_, i) => axel${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");

// Ladda ner som CSV downloadFile("gamepad-log.csv", rubrik + rader, "text/csv"); });

CSV är briljant för dataanalys eftersom den öppnas direkt i Excel eller Google Sheets, så att du kan skapa diagram, filtrera data eller se mönster visuellt. Nu när exportknapparna är i kommer du att se två nya alternativ på panelen: Exportera JSON och Exportera CSV. JSON är trevligt om du vill kasta tillbaka den råa loggen i dina dev-verktyg eller rota runt strukturen. CSV, å andra sidan, öppnas direkt i Excel eller Google Sheets så att du kan kartlägga, filtrera eller jämföra indata. Följande bild visar hur panelen ser ut med de extra kontrollerna.

3. Snapshot System Ibland behöver du inte en fullständig inspelning, bara en snabb "skärmdump" av inmatningstillstånd. Det är där en Ta ögonblicksbild-knapp hjälper.

Och JavaScript:

// =================================== // TA STILLBILD // ===================================

document.getElementById("snapshot").addEventListener("klick", () => { // Skaffa alla anslutna gamepads const pads = navigator.getGamepads(); const activePads = [];

// Gå igenom och fånga statusen för varje ansluten gamepad för (konst gp av kuddar) { om (!gp) fortsätt; // Hoppa över tomma platser

activePads.push({ id: gp.id, // Controllernamn/modell tidsstämpel: performance.now(), knappar: gp.buttons.map(b => ({ tryckte: b.tryckte, värde: b.värde })), axlar: [...gp.axes] }); }

// Kontrollera om några spelkontroller hittades if (!activePads.length) { console.warn("Inga gamepads anslutna för ögonblicksbild."); alert("Ingen styrenhet upptäckt!"); återvända; }

// Logga och meddela användaren console.log("Snapshot:", activePads); alert(Snapshot tagna! Fångade ${activePads.length} kontroller(er).); });

Ögonblicksbilder fryser det exakta tillståndet på din handkontroll vid ett ögonblick. 4. Ghost Input Replay Nu till det roliga: repris på spökinmatning. Detta tar en logg och spelar upp den visuellt som om en fantomspelare använde handkontrollen.

JavaScript för repris: // =================================== // GOST REPLAY // ===================================

document.getElementById("replay").addEventListener("klick", () => { // Se till att vi har en inspelning att spela upp igen if (!frames.length) { alert("Ingen inspelning att spela upp!"); återvända; }

console.log("Startar spökreplay...");

// Spårtiming för synkroniserad uppspelning låt startTime = performance.now(); låt frameIndex = 0;

// Spela om animationsslingan function step() { const nu = performance.now(); const elapsed = nu - startTime;

// Bearbeta alla ramar som borde ha inträffat vid det här laget while (frameIndex < frames.length && frames[frameIndex].t <= förflutit) { const frame = frames[frameIndex];

// Uppdatera UI med de inspelade knapptillstånden btnA.classList.toggle("active", frame.buttons[0].pressed); btnB.classList.toggle("active", frame.buttons[1].pressed); btnX.classList.toggle("active", frame.buttons[2].pressed);

// Uppdatera statusdisplay låt intryckt = []; frame.buttons.forEach((btn, i) => { if (btn.pressed) pressed.push("Knapp " + i); }); if (tryckt.längd > 0) { status.textContent = "Ghost: " + pressed.join(", "); }

frameIndex++; }

// Fortsätt loop om det finns fler ramar if (frameIndex < frames.length) { requestAnimationFrame(steg); } annat { console.log("Spela omfärdig."); status.textContent = "Omspelning slutförd"; } }

// Starta reprisen steg(); });

För att göra felsökningen lite mer praktisk lade jag till en spökrepris. När du väl har spelat in en session kan du trycka på replay och se gränssnittet spela ut det, nästan som en fantomspelare kör platta. En ny Replay Ghost-knapp dyker upp i panelen för detta.

Tryck på Spela in, bråka lite med kontrollen, stoppa och spela sedan om. Användargränssnittet ekar bara allt du gjorde, som ett spöke som följer dina ingångar. Varför bry sig om dessa extrafunktioner?

Inspelning/export gör det enkelt för testare att visa exakt vad som hände. Ögonblicksbilder fryser ett ögonblick i tiden, super användbart när du jagar udda buggar. Ghost replay är bra för självstudier, tillgänglighetskontroller eller bara för att jämföra kontrollinställningar sida vid sida.

Vid det här laget är det inte bara en snygg demo längre, utan något du faktiskt kan sätta igång. Användningsfall i verkliga världen Nu har vi den här felsökaren som kan göra mycket. Den visar liveinmatning, registrerar loggar, exporterar dem och spelar till och med upp saker. Men den verkliga frågan är: vem bryr sig egentligen? Vem är detta användbart för? Spelutvecklare Styrenheter är en del av jobbet, men felsöka dem? Vanligtvis smärta. Föreställ dig att du testar en kampspelskombination, som ↓ → + punch. Istället för att be, tryckte du på den på samma sätt två gånger, du spelar in den en gång och spelar upp den igen. Klart. Eller så byter du JSON-loggar med en lagkamrat för att kontrollera om din flerspelarkod reagerar likadant på deras maskin. Det är enormt. Tillgänglighetsutövare Den här ligger mig varmt om hjärtat. Alla spelar inte med en "standard" kontroller. Adaptiva kontroller kastar ut konstiga signaler ibland. Med det här verktyget kan du se exakt vad som händer. Lärare, forskare, vem som helst. De kan ta tag i loggar, jämföra dem eller spela in ingångar sida vid sida. Plötsligt blir osynliga saker uppenbara. Kvalitetssäkringstestning Testare skriver vanligtvis anteckningar som "Jag mosade knappar här och det gick sönder." Inte särskilt hjälpsam. Nu? De kan fånga de exakta pressarna, exportera loggen och skicka iväg den. Ingen gissning. Lärare Om du gör självstudier eller YouTube-videor är spökreplay guld. Du kan bokstavligen säga, "Här är vad jag gjorde med kontrollern", medan användargränssnittet visar att det händer. Gör förklaringarna mycket tydligare. Bortom spel Och ja, det här handlar inte bara om spel. Människor har använt kontroller för robotar, konstprojekt och tillgänglighetsgränssnitt. Samma problem varje gång: vad ser webbläsaren egentligen? Med detta behöver du inte gissa. Slutsats Att felsöka en kontrolleringång har alltid känts som att flyga i blindo. Till skillnad från DOM eller CSS finns det ingen inbyggd inspektör för gamepads; det är bara råa siffror i konsolen, lätt att förlora i bruset. Med några hundra rader HTML, CSS och JavaScript byggde vi något annat:

En visuell debugger som gör osynliga ingångar synliga. Ett lager CSS-system som håller användargränssnittet rent och felsökningsbart. En uppsättning förbättringar (inspelning, export, ögonblicksbilder, spökreplay) som lyfter den från demo till utvecklarverktyg.

Det här projektet visar hur långt du kan gå genom att blanda webbplattformens kraft med lite kreativitet i CSS Cascade Layers. Verktyget jag just förklarade i sin helhet är öppen källkod. Du kan klona GitHub-repo och prova det själv. Men ännu viktigare, du kan göra den till din egen. Lägg till dina egna lager. Bygg din egen reprislogik. Integrera det med din spelprototyp. Eller till och med använda det på sätt jag inte har föreställt mig. För undervisning, tillgänglighet eller dataanalys. I slutändan handlar det här inte bara om att felsöka gamepads. Det handlar om att belysa dolda ingångar och ge utvecklare självförtroendet att arbeta med hårdvara som webben fortfarande inte helt omfamnar. Så koppla in din kontroller, öppna din editor och börja experimentera. Du kanske blir förvånad över vad din webbläsare och din CSS verkligen kan åstadkomma.

You May Also Like

Enjoyed This Article?

Get weekly tips on growing your audience and monetizing your content — straight to your inbox.

No spam. Join 138,000+ creators. Unsubscribe anytime.

Create Your Free Bio Page

Join 138,000+ creators on Seemless.

Get Started Free