Als je een controller aansluit, druk je op knoppen, beweeg je de sticks, haal je de trekker over… en als ontwikkelaar zie je daar niets van. Zeker, de browser pikt het op, maar tenzij je nummers in de console registreert, is het onzichtbaar. Dat is de hoofdpijn met de Gamepad API. Het bestaat al jaren en is eigenlijk behoorlijk krachtig. Je kunt knoppen, sticks, triggers, de werking lezen. Maar de meeste mensen raken het niet aan. Waarom? Omdat er geen feedback is. Geen paneel in ontwikkelaarstools. Geen duidelijke manier om te weten of de controller zelfs doet wat u denkt. Het voelt als blind vliegen. Dat irriteerde me genoeg om een ​​klein hulpmiddel te bouwen: Gamepad Cascade Debugger. In plaats van naar de console-uitvoer te staren, krijg je een live, interactief beeld van de controller. Druk ergens op en het reageert op het scherm. En met CSS Cascade Layers blijven de stijlen overzichtelijk, zodat het eenvoudiger is om fouten op te sporen. In dit bericht laat ik je zien waarom het debuggen van controllers zo lastig is, hoe CSS helpt bij het opruimen ervan, en hoe je een herbruikbare visuele debugger voor je eigen projecten kunt bouwen.

Zelfs als je ze allemaal kunt loggen, zul je snel eindigen met onleesbare console-spam. Bijvoorbeeld: [0,0,1,0,0,0,5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]

Kunt u zien welke knop werd ingedrukt? Misschien, maar pas nadat je je ogen hebt ingespannen en een paar invoergegevens hebt gemist. Dus nee, foutopsporing is niet eenvoudig als het gaat om het lezen van invoer. Probleem 3: Gebrek aan structuur Zelfs als je een snelle visualisatietool in elkaar zet, kunnen stijlen snel rommelig worden. Standaard-, actieve- en foutopsporingsstatussen kunnen elkaar overlappen, en zonder een duidelijke structuur wordt uw CSS broos en moeilijk uit te breiden. CSS-cascadelagen kunnen helpen. Ze groeperen stijlen in ‘lagen’ die op prioriteit zijn geordend, zodat je niet meer hoeft te vechten tegen specificiteit en hoeft te raden: ‘Waarom wordt mijn foutopsporingsstijl niet weergegeven?’ In plaats daarvan onderhoudt u afzonderlijke zorgen:

Basis: het standaard uiterlijk van de controller. Actief: Hoogtepunten voor ingedrukte knoppen en verplaatste sticks. Debug: Overlays voor ontwikkelaars (bijvoorbeeld numerieke uitlezingen, handleidingen, enzovoort).

Als we lagen in CSS op deze manier zouden definiëren, zouden we het volgende hebben: /* laagste naar hoogste prioriteit */ @layer basis, actief, debuggen;

@laagbasis { /* ... */ }

@laag actief { /* ... */ }

@layer foutopsporing { /* ... */ }

Omdat elke laag voorspelbaar wordt gestapeld, weet je altijd welke regels winnen. Die voorspelbaarheid maakt het debuggen niet alleen eenvoudiger, maar ook beheersbaar. We hebben het probleem (onzichtbare, rommelige invoer) en de aanpak (een visuele debugger gebouwd met Cascade Layers) besproken. Nu zullen we het stapsgewijze proces doorlopen om de debugger te bouwen. Het debugger-concept De eenvoudigste manier om verborgen invoer zichtbaar te maken, is door deze gewoon op het scherm te tekenen. Dat is wat deze debugger doet. Knoppen, triggers en joysticks krijgen allemaal een visueel beeld.

Druk op A: Er licht een cirkel op. Duw de stok: de cirkel glijdt rond. Haal de trekker halverwege over: een balk vult zich halverwege.

Nu staar je niet naar nullen en enen, maar kijk je feitelijk hoe de controller live reageert. Zodra je statussen als standaard, ingedrukt, foutopsporingsinformatie en misschien zelfs een opnamemodus begint op te stapelen, wordt de CSS uiteraard groter en complexer. Dat is waar cascadelagen van pas komen. Hier is een uitgekleed voorbeeld: @laagbasis { .knop { achtergrond: #222; randradius: 50%; breedte: 40px; hoogte: 40px; } }

@laag actief { .knop.gedrukt { achtergrond: #0f0; /* heldergroen */ } }

@layer foutopsporing { .knop::na { inhoud: attr(gegevenswaarde); lettergrootte: 12px; kleur: #fff; } }

De volgorde van de lagen is van belang: basis → actief → debuggen.

basis trekt de controller. actief verwerkt ingedrukte toestanden. debug gooit op overlays.

Als je het op deze manier opsplitst, betekent dit dat je geen rare specificiteitsoorlogen voert. Elke laag heeft zijn plaats en je weet altijd wat wint. Het uitbouwen Laten we eerst iets op het scherm krijgen. Het hoeft er niet goed uit te zien; het moet gewoon bestaan, zodat we iets hebben om mee te werken.

Cascade-foutopsporing voor gamepads

A
B
X

Debugger inactief

Dat zijn letterlijk alleen maar dozen. Nog niet spannend, maar het geeft ons handvatten om later met CSS en JavaScript aan de slag te gaan. Oké, ik gebruik hier cascadelagen omdat het de boel overzichtelijk houdt zodra je meer staten toevoegt. Hier is een ruwe pass:

/* =================================== CASCADELAGEN INSTELLING Volgorde is belangrijk: basis → actief → debuggen =================================== */

/* Definieer de laagvolgorde vooraf */ @layer basis, actief, debuggen;

/* Laag 1: Basisstijlen - standaardweergave */ @laagbasis { .knop { achtergrond: #333; randradius: 50%; breedte: 70px; hoogte: 70px; weergave: flex; rechtvaardigen-inhoud: midden; items uitlijnen: centreren; }

.pauze { breedte: 20px; hoogte: 70px; achtergrond: #333; display: inline-blok; } }

/* Laag 2: Actieve statussen - verwerkt ingedrukte knoppen */ @laag actief { .knop.actief { achtergrond: #0f0; /* Heldergroen wanneer ingedrukt */ transformeren: schaal(1.1); /* Vergroot de knop iets */ }

.pauze.actief { achtergrond: #0f0; transformeren: schaalY(1.1); /* Rekt verticaal uit wanneer ingedrukt */ } }

/* Laag 3: Foutopsporingsoverlays - ontwikkelaarsinfo */ @layer foutopsporing { .knop::na { inhoud: attr(gegevenswaarde); /* Toont de numerieke waarde */ lettergrootte: 12px; kleur: #fff; } }

Het mooie van deze aanpak is dat elke laag een duidelijk doel heeft. De basislaag kan nooit de actieve laag overschrijven, en de actieve laag kan nooit debuggen overschrijven, ongeacht de specificiteit. Dit elimineert de CSS-specificiteitsoorlogen die gewoonlijk debugging-tools teisteren. Nu lijkt het erop dat sommige clusters op een donkere achtergrond staan. Eerlijk gezegd, niet zo slecht.

Het JavaScript toevoegen JavaScript-tijd. Dit is waar de controller daadwerkelijk iets doet. We bouwen dit stap voor stap op. Stap 1: Statusbeheer instellen Ten eerste hebben we variabelen nodig om de status van de debugger te volgen: //=================================== // STAATSBEHEER //===================================

laten lopen = false; // Houdt bij of de debugger actief is laat vlot; // Slaat de requestAnimationFrame-ID op voor annulering

Deze variabelen besturen de animatielus die continu de gamepad-invoer leest. Stap 2: Pak DOM-referenties Vervolgens krijgen we verwijzingen naar alle HTML-elementen die we gaan bijwerken: //=================================== // DOM-ELEMENTREFERENTIES //===================================

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

Het vooraf opslaan van deze referenties is efficiënter dan het herhaaldelijk opvragen van de DOM. Stap 3: Voeg toetsenbordterugval toe Voor testen zonder fysieke controller wijzen we toetsenbordtoetsen toe aan knoppen: //=================================== // KEYBOARD FALLBACK (voor testen zonder controller) //===================================

const sleutelkaart = { "a": btnA, "b": btnB, "x": btnX, "p": [pauze1, pauze2] // 'p'-toets bestuurt beide pauzebalken };

Hiermee kunnen we de gebruikersinterface testen door op toetsen op een toetsenbord te drukken. Stap 4: Maak de hoofdupdatelus Hier gebeurt de magie. Deze functie werkt continu en leest de gamepad-status: //=================================== // HOOFDGAMEPAD-UPDATE-LOOP //===================================

functie updateGamepad() { // Ontvang alle aangesloten gamepads const gamepads = navigator.getGamepads(); als (!gamepads) terugkeren;

// Gebruik de eerste aangesloten gamepad const gp = gamepads[0];

als (gp) { // Update de knopstatus door de klasse "actief" te schakelen btnA.classList.toggle("actief", gp.buttons[0].ingedrukt); btnB.classList.toggle("actief", gp.buttons[1].ingedrukt); btnX.classList.toggle("actief", gp.buttons[2].ingedrukt);

// Pauzeknop voor handgreep (knopindex 9 op de meeste controllers) const pausePressed = gp.buttons[9].gedrukt; pauze1.classList.toggle("actief", pauzePressed); pauze2.classList.toggle("actief", pauzePressed);

// Bouw een lijst op van momenteel ingedrukte knoppen voor statusweergave laat ingedrukt = []; gp.buttons.forEach((btn, i) => { als (btn.gedrukt)ingedrukt.push("Knop " + i); });

// Update de statustekst als er op een knop wordt gedrukt if (geperste lengte > 0) { status.textContent = "Ingedrukt: " + ingedrukt.join(", "); } }

// Vervolg de lus als debugger actief is als (rennen) { rafId = requestAnimationFrame(updateGamepad); } }

De methode classList.toggle() voegt de actieve klasse toe of verwijdert deze op basis van het feit of de knop wordt ingedrukt, waardoor onze CSS-laagstijlen worden geactiveerd. Stap 5: Behandel toetsenbordgebeurtenissen Deze gebeurtenislisteners zorgen ervoor dat de toetsenbordfallback werkt: //=================================== // TOETSENBORD GEBEURTENISSENHANDLERS //===================================

document.addEventListener("keydown", (e) => { if (sleutelkaart[e.key]) { // Behandel enkele of meerdere elementen if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("actief")); } anders { keyMap[e.key].classList.add("actief"); } status.textContent = "Toets ingedrukt: " + e.key.toUpperCase(); } });

document.addEventListener("keyup", (e) => { if (sleutelkaart[e.key]) { // Verwijder de actieve status wanneer de sleutel wordt losgelaten if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("actief")); } anders { keyMap[e.key].classList.remove("actief"); } status.textContent = "Sleutel vrijgegeven: " + e.key.toUpperCase(); } });

Stap 6: Start/Stop-bediening toevoegen Ten slotte hebben we een manier nodig om de debugger aan en uit te zetten: //=================================== // SCHAKEL DEBUGGER AAN/UIT //===================================

document.getElementById("toggle").addEventListener("klik", () => { hardlopen = !lopen; // Draai de actieve status om

als (rennen) { status.textContent = "Debugger actief..."; updateGamepad(); // Start de updatelus } anders { status.textContent = "Debugger inactief"; annuleerAnimatieFrame(rafId); // Stop de lus } });

Dus ja, druk op een knop en hij gloeit. Duw op de stok en hij beweegt. Dat is alles. Nog één ding: ruwe waarden. Soms wil je gewoon cijfers zien, geen lichten.

In dit stadium zou u het volgende moeten zien:

Een eenvoudige controller op het scherm, Knoppen die reageren terwijl u ermee communiceert, en Een optionele debug-uitlezing met indexen van ingedrukte knoppen.

Om dit minder abstract te maken, volgt hier een korte demo van de controller op het scherm die in realtime reageert:

Als u nu op Opname starten drukt, wordt alles geregistreerd totdat u op Opname stoppen drukt. 2. Gegevens exporteren naar CSV/JSON Zodra we een logboek hebben, willen we het opslaan.

Stap 1: Maak de downloadhelper Ten eerste hebben we een helperfunctie nodig die het downloaden van bestanden in de browser afhandelt: //=================================== // BESTAND DOWNLOADHELPER //===================================

function downloadFile(bestandsnaam, inhoud, type = "tekst/plain") { // Maak een blob van de inhoud const blob = nieuwe Blob([inhoud], {type }); const url = URL.createObjectURL(blob);

// Maak een tijdelijke downloadlink en klik erop const a = document.createElement("a"); a.href = url; a.download = bestandsnaam; a.klik();

// Ruim de object-URL op na het downloaden setTimeout(() => URL.revokeObjectURL(url), 100); }

Deze functie werkt door een Blob (binair groot object) van uw gegevens te maken, er een tijdelijke URL voor te genereren en programmatisch op een downloadlink te klikken. Het opruimen zorgt ervoor dat we geen geheugen lekken. Stap 2: Behandel JSON-export JSON is perfect voor het behouden van de volledige datastructuur:

//=================================== // EXPORTEREN ALS JSON //===================================

document.getElementById("export-json").addEventListener("klik", () => { // Controleer of er iets is om te exporteren if (!frames.lengte) { console.warn("Geen opname beschikbaar om te exporteren."); terugkeer; }

// Maak een payload met metadata en frames const-payload = { aangemaakt op: nieuwe datum().toISOString(), kaders };

// Downloaden als geformatteerde JSON downloadBestand( "gamepad-log.json", JSON.stringify(payload, null, 2), "applicatie/json" ); });

Het JSON-formaat houdt alles gestructureerd en gemakkelijk te analyseren, waardoor het ideaal is om terug te laden in dev-tools of te delen met teamgenoten. Stap 3: Behandel CSV-export Voor CSV-exports moeten we de hiërarchische gegevens in rijen en kolommen opsplitsen:

//=================================== // EXPORTEREN ALS CSV //===================================

document.getElementById("export-csv").addEventListener("klik", () => { // Controleer of er iets is om te exporteren if (!frames.lengte) { console.warn("Geen opname beschikbaar om te exporteren."); terugkeer; }

// Bouw een CSV-koprij (kolommen voor tijdstempel, alle knoppen, alle assen) 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";

// Bouw CSV-gegevensrijen const rijen = frames.map(f => { const btnVals = f.buttons.map(b => b.waarde); return [f.t, ...btnVals, ...f.axes].join(","); }).meld je aan("\n");

// Downloaden als CSV downloadFile("gamepad-log.csv", kop + rijen, "text/csv"); });

CSV is geweldig voor gegevensanalyse omdat het rechtstreeks in Excel of Google Spreadsheets wordt geopend, zodat u diagrammen kunt maken, gegevens kunt filteren of patronen visueel kunt herkennen. Nu de exportknoppen aanwezig zijn, zie je twee nieuwe opties op het paneel: JSON exporteren en CSV exporteren. JSON is leuk als je het onbewerkte logboek terug in je ontwikkeltools wilt gooien of door de structuur wilt snuffelen. CSV daarentegen wordt rechtstreeks geopend in Excel of Google Spreadsheets, zodat u invoer in kaart kunt brengen, filteren of vergelijken. De volgende afbeelding laat zien hoe het paneel eruit ziet met die extra bedieningselementen.

3. Snapshot-systeem Soms heb je geen volledige opname nodig, maar alleen een snelle “screenshot” van de invoerstatussen. Dat is waar een knop Take Snapshot helpt.

En het JavaScript:

//=================================== // Maak een momentopname //===================================

document.getElementById("snapshot").addEventListener("klik", () => { // Ontvang alle aangesloten gamepads const-pads = navigator.getGamepads(); const activePads = [];

// Loop door en leg de status van elke aangesloten gamepad vast voor (const gp van pads) { als (!gp) doorgaan; // Sla lege slots over

activePads.push({ id: gp.id, // Controllernaam/model tijdstempel: performance.now(), knoppen: gp.buttons.map(b => ({ ingedrukt: b.gedrukt, waarde: b.waarde })), assen: [...gp.assen] }); }

// Controleer of er gamepads zijn gevonden if (!activePads.lengte) { console.warn("Geen gamepads aangesloten voor momentopname."); alert("Geen controller gedetecteerd!"); terugkeer; }

// Log in en stel de gebruiker op de hoogte console.log("Momentopname:", activePads); alert(Momentopname gemaakt! ${activePads.length} controller(s) vastgelegd.); });

Snapshots bevriezen de exacte status van uw controller op een bepaald moment. 4. Herhaling van spookinvoer Nu voor de leuke: herhaling van spookinvoer. Hiervoor wordt een logbestand nodig en wordt dit visueel afgespeeld alsof een fantoomspeler de controller gebruikt.

JavaScript voor herhaling: //=================================== // SPOOKHERHAAL //===================================

document.getElementById("replay").addEventListener("klik", () => { // Zorg ervoor dat we een opname hebben om af te spelen if (!frames.lengte) { alert("Geen opname om opnieuw af te spelen!"); terugkeer; }

console.log("Ghost Replay starten...");

// Tracktiming voor gesynchroniseerd afspelen laat startTime = performance.now(); laat frameIndex = 0;

// Animatielus opnieuw afspelen functiestap() { const nu = prestatie.nu(); const verstreken = nu - starttijd;

// Verwerk alle frames die inmiddels hadden moeten plaatsvinden while (frameIndex < frames.lengte && frames[frameIndex].t <= verstreken) { const frame = frames[frameIndex];

// Update de gebruikersinterface met de opgenomen knopstatussen btnA.classList.toggle("actief", frame.knoppen[0].ingedrukt); btnB.classList.toggle("actief", frame.knoppen[1].ingedrukt); btnX.classList.toggle("actief", frame.knoppen[2].ingedrukt);

// Update statusweergave laat ingedrukt = []; frame.buttons.forEach((btn, i) => { if (btn.gedrukt) ingedrukt.push("Knop " + i); }); if (geperste lengte > 0) { status.textContent = "Ghost: " + ingedrukt.join(", "); }

frameIndex++; }

// Ga door met de lus als er meer frames zijn if (frameIndex

// Start de herhaling stap(); });

Om het debuggen wat praktischer te maken, heb ik een spookherhaling toegevoegd. Nadat je een sessie hebt opgenomen, kun je op Replay drukken en kijken hoe de gebruikersinterface het uitvoert, bijna alsof een fantoomspeler de pad bestuurt. Hiervoor verschijnt een nieuwe Replay Ghost-knop in het paneel.

Druk op Record, rommel een beetje met de controller, stop en speel opnieuw. De gebruikersinterface weerspiegelt gewoon alles wat u hebt gedaan, als een geest die uw invoer volgt. Waarom moeite doen met deze extra's?

Door op te nemen/exporteren kunnen testers gemakkelijk laten zien wat er precies is gebeurd. Snapshots bevriezen een moment in de tijd, super handig als je op zoek bent naar vreemde bugs. Ghost replay is geweldig voor tutorials, toegankelijkheidscontroles of gewoon om besturingsinstellingen naast elkaar te vergelijken.

Op dit moment is het niet alleen meer een leuke demo, maar iets waar je echt mee aan de slag kunt. Gebruiksscenario's uit de echte wereld Nu hebben we deze debugger die veel kan. Het toont live invoer, neemt logboeken op, exporteert ze en speelt zelfs dingen opnieuw af. Maar de echte vraag is: wie maakt het eigenlijk uit? Voor wie is dit nuttig? Game-ontwikkelaars Controllers horen bij het werk, maar debuggen ervan? Meestal pijn. Stel je voor dat je een combinatie van vechtgames aan het testen bent, zoals ↓ → + punch. In plaats van te bidden, drukte je twee keer op dezelfde manier, je nam het één keer op en speelde het opnieuw af. Klaar. Of u wisselt JSON-logboeken uit met een teamgenoot om te controleren of uw multiplayercode op hun machine hetzelfde reageert. Dat is enorm. Toegankelijkheidsbeoefenaars Deze ligt mij nauw aan het hart. Niet iedereen speelt met een “standaard” controller. Adaptieve controllers stoten soms rare signalen uit. Met deze tool kun je precies zien wat er gebeurt. Docenten, onderzoekers, wie dan ook. Ze kunnen logboeken verzamelen, vergelijken of invoer naast elkaar afspelen. Plotseling worden onzichtbare dingen duidelijk. Kwaliteitsborgingstesten Testers schrijven meestal opmerkingen als: "Ik heb hier knoppen geplet en deze is kapot gegaan." Niet erg behulpzaam. Nu? Ze kunnen de exacte persen vastleggen, het logboek exporteren en verzenden. Geen raden. Opvoeders Als je tutorials of YouTube-video's maakt, is ghost replay goud waard. Je kunt letterlijk zeggen: "Dit is wat ik met de controller heb gedaan", terwijl de gebruikersinterface laat zien dat het gebeurt. Maakt uitleg veel duidelijker. Voorbij spellen En ja, dit gaat niet alleen over games. Mensen hebben controllers gebruikt voor robots, kunstprojecten en toegankelijkheidsinterfaces. Elke keer hetzelfde probleem: wat ziet de browser eigenlijk? Hiermee hoef je niet te raden. Conclusie Het debuggen van een controllerinvoer voelde altijd als blind vliegen. In tegenstelling tot de DOM of CSS is er geen ingebouwde inspecteur voor gamepads; het zijn gewoon ruwe cijfers in de console, die gemakkelijk verloren gaan in het lawaai. Met een paar honderd regels HTML, CSS en JavaScript hebben we iets anders gebouwd:

Een visuele debugger die onzichtbare invoer zichtbaar maakt. Een gelaagd CSS-systeem dat de gebruikersinterface schoon en foutloos houdt. Een reeks verbeteringen (opnemen, exporteren, snapshots, ghost replay) die het van demo naar ontwikkelaarstool tillen.

Dit project laat zien hoe ver je kunt gaan door de kracht van het webplatform te combineren met een beetje creativiteit in CSS Cascade Layers. De tool die ik zojuist in zijn geheel heb uitgelegd, is open source. Je kunt de GitHub-repository klonen en het zelf proberen. Maar wat nog belangrijker is, je kunt het je eigen maken. Voeg je eigen lagen toe. Bouw uw eigen herhalingslogica. Integreer het met uw game-prototype. Of gebruik het zelfs op manieren die ik me niet had kunnen voorstellen. Voor lesgeven, toegankelijkheid of data-analyse. Uiteindelijk gaat het niet alleen om het debuggen van gamepads. Het gaat erom verborgen input aan het licht te brengen en ontwikkelaars het vertrouwen te geven om met hardware te werken die het internet nog steeds niet volledig omarmt. Sluit dus uw controller aan, open uw editor en begin met experimenteren. U zult misschien verbaasd zijn over wat uw browser en uw CSS werkelijk kunnen bereiken.

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