Po podłączeniu kontrolera naciskasz przyciski, poruszasz drążkami, pociągasz za spust… a jako programista nic z tego nie widzisz. Przeglądarka to oczywiście wychwytuje, ale jeśli nie logujesz liczb w konsoli, jest to niewidoczne. Na tym właśnie polega problem z interfejsem API Gamepada. Istnieje już od lat i jest naprawdę potężny. Możesz odczytać przyciski, drążki, wyzwalacze, działanie. Ale większość ludzi tego nie dotyka. Dlaczego? Ponieważ nie ma informacji zwrotnej. Brak panelu w narzędziach programistycznych. Nie ma jasnego sposobu, aby sprawdzić, czy kontroler w ogóle robi to, co myślisz. To jak latanie na oślep. To mnie na tyle wkurzyło, że zbudowałem małe narzędzie: Gamepad Cascade Debugger. Zamiast patrzeć na dane wyjściowe konsoli, otrzymujesz interaktywny widok kontrolera na żywo. Naciśnij coś, a zareaguje na ekranie. Dzięki warstwom kaskadowym CSS style pozostają zorganizowane, dzięki czemu debugowanie jest prostsze. W tym poście pokażę Ci, dlaczego debugowanie kontrolerów jest takie trudne, jak CSS pomaga to wyczyścić i jak możesz zbudować wizualny debugger wielokrotnego użytku do własnych projektów.
Nawet jeśli uda ci się zalogować je wszystkie, szybko skończy się to nieczytelnym spamem konsolowym. Na przykład: [0,0,1,0,0,0,5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]
Czy potrafisz powiedzieć, który przycisk został naciśnięty? Być może, ale dopiero po wytężeniu wzroku i pominięciu kilku danych wejściowych. Zatem nie, debugowanie nie jest łatwe, jeśli chodzi o odczytywanie danych wejściowych. Problem 3: Brak struktury Nawet jeśli stworzysz szybki wizualizator, style mogą szybko stać się nieuporządkowane. Stany domyślne, aktywne i debugowania mogą się na siebie nakładać, a bez jasnej struktury CSS staje się kruchy i trudny do rozszerzenia. Warstwy kaskadowe CSS mogą pomóc. Grupują style w „warstwy” uporządkowane według priorytetów, dzięki czemu przestajesz walczyć ze specyfiką i zgadywać: „Dlaczego mój styl debugowania nie jest wyświetlany?” Zamiast tego utrzymujesz oddzielne obawy:
Baza: standardowy, początkowy wygląd sterownika. Aktywne: Podświetlenie naciśniętych przycisków i poruszonych drążków. Debugowanie: nakładki dla programistów (np. odczyty numeryczne, przewodniki itp.).
Gdybyśmy mieli według tego zdefiniować warstwy w CSS, mielibyśmy: /* priorytet od najniższego do najwyższego */ @warstwa podstawowa, aktywna, debugowanie;
@baza warstwy { /* ... */ }
@warstwa aktywna { /* ... */ }
Debugowanie @warstwy { /* ... */ }
Ponieważ każda warstwa nakłada się w sposób przewidywalny, zawsze wiesz, które zasady wygrywają. Ta przewidywalność sprawia, że debugowanie jest nie tylko łatwiejsze, ale wręcz możliwe do zarządzania. Omówiliśmy problem (niewidoczne, niechlujne dane wejściowe) i podejście (debuger wizualny zbudowany z warstw kaskadowych). Teraz omówimy krok po kroku proces tworzenia debugera. Koncepcja debugera Najprostszym sposobem na uwidocznienie ukrytych danych wejściowych jest po prostu narysowanie ich na ekranie. Właśnie to robi ten debuger. Przyciski, spusty i joysticki mają wygląd wizualny.
Naciśnij A: zaświeci się okrąg. Szturchnij drążek: okrąg się ślizga. Pociągnij za spust do połowy: pasek wypełnia się do połowy.
Teraz nie patrzysz na 0 i 1, ale faktycznie obserwujesz reakcję kontrolera na żywo. Oczywiście, gdy zaczniesz gromadzić stany, takie jak domyślny, naciśnięty, informacje o debugowaniu, a może nawet tryb nagrywania, CSS stanie się większy i bardziej złożony. Tutaj z pomocą przychodzą warstwy kaskadowe. Oto uproszczony przykład: @baza warstwy { .przycisk { tło: #222; promień granicy: 50%; szerokość: 40px; wysokość: 40px; } }
@warstwa aktywna { .przycisk.wciśnięty { tło: #0f0; /*jasnozielony */ } }
Debugowanie @warstwy { .button::po { treść: attr(wartość-danych); rozmiar czcionki: 12px; kolor: #fff; } }
Kolejność warstw ma znaczenie: baza → aktywna → debugowanie.
base rysuje kontroler. aktywne uchwyty stany wciśnięte. debugowanie rzuca na nakładki.
Rozbicie tego w ten sposób oznacza, że nie toczycie dziwnych wojen specyfiki. Każda warstwa ma swoje miejsce i zawsze wiesz, co zwycięży. Budowanie tego Najpierw wyświetlmy coś na ekranie. Nie musi to dobrze wyglądać – musi po prostu istnieć, abyśmy mieli nad czym pracować.
Debuger kaskadowy gamepada
To dosłownie tylko pudełka. Nie jest to jeszcze ekscytujące, ale daje nam uchwyty do późniejszego wykorzystania w CSS i JavaScript. OK, używam tutaj warstw kaskadowych, ponieważ pozwalają one zachować porządek po dodaniu kolejnych stanów. Oto trudne podanie:
/* =================================== USTAWIENIE WARSTW KASKADOWYCH Kolejność ma znaczenie: baza → aktywna → debugowanie =================================== */
/* Zdefiniuj kolejność warstw od razu */ @warstwa podstawowa, aktywna, debugowanie;
/* Warstwa 1: Style podstawowe – wygląd domyślny */ @baza warstwy { .przycisk { tło: #333; promień granicy: 50%; szerokość: 70px; wysokość: 70px; wyświetlacz: elastyczny; justify-content: środek; wyrównanie elementów: środek; }
.pauza { szerokość: 20px; wysokość: 70px; tło: #333; wyświetlacz: blok inline; } }
/* Warstwa 2: Stany aktywne - obsługuje wciśnięte przyciski */ @warstwa aktywna { .przycisk.aktywny { tło: #0f0; /* Jasnozielony po naciśnięciu */ przekształcenie: skala (1.1); /* Nieznacznie powiększa przycisk */ }
.pauza.aktywna { tło: #0f0; transformacja: skalaY(1.1); /* Rozciąga się w pionie po naciśnięciu */ } }
/* Warstwa 3: Nakładki debugowania – informacje dla programistów */ Debugowanie @warstwy { .button::po { treść: attr(wartość-danych); /* Pokazuje wartość liczbową */ rozmiar czcionki: 12px; kolor: #fff; } }
Piękno tego podejścia polega na tym, że każda warstwa ma jasny cel. Warstwa podstawowa nigdy nie może zastąpić warstwy aktywnej, a warstwa aktywna nigdy nie może zastąpić debugowania, niezależnie od specyfiki. Eliminuje to wojny specyficzności CSS, które zwykle są plagą narzędzi do debugowania. Teraz wygląda na to, że niektóre gromady znajdują się na ciemnym tle. Szczerze mówiąc, nie jest tak źle.
Dodanie JavaScriptu Czas JavaScriptu. W tym miejscu kontroler faktycznie coś robi. Zbudujemy to krok po kroku. Krok 1: Skonfiguruj zarządzanie stanem Po pierwsze, potrzebujemy zmiennych do śledzenia stanu debuggera: // =================================== // ZARZĄDZANIE PAŃSTWEM // ===================================
niech działa = fałsz; // Śledzi, czy debuger jest aktywny niech raId; // Przechowuje identyfikator requestAnimationFrame w celu anulowania
Zmienne te kontrolują pętlę animacji, która w sposób ciągły odczytuje dane wejściowe z gamepada. Krok 2: Skorzystaj z referencji DOM Następnie otrzymujemy odniesienia do wszystkich elementów HTML, które będziemy aktualizować: // =================================== // REFERENCJE DO ELEMENTÓW DOM // ===================================
const btnA = document.getElementById("btn-a"); const btnB = document.getElementById("btn-b"); const btnX = document.getElementById("btn-x"); const pauza1 = document.getElementById("pause1"); const pauza2 = document.getElementById("pause2"); stały status = document.getElementById("status");
Przechowywanie tych odniesień z góry jest bardziej efektywne niż wielokrotne odpytywanie DOM. Krok 3: Dodaj klawiaturę zastępczą Do testów bez fizycznego kontrolera zamapujemy klawisze klawiatury na przyciski: // =================================== // KEYBOARD FALLBACK (do testów bez kontrolera) // ===================================
stała mapa kluczy = { „a”: btnA, „b”: btnB, „x”: btnX, „p”: [pauza1, pauza2] // Klawisz „p” steruje obydwoma paskami pauzy };
Dzięki temu możemy przetestować interfejs użytkownika, naciskając klawisze na klawiaturze. Krok 4: Utwórz główną pętlę aktualizacji Tutaj dzieje się magia. Ta funkcja działa w sposób ciągły i odczytuje stan gamepada: // =================================== // PĘTLA AKTUALIZACJI GŁÓWNEGO GAMEPADU // ===================================
funkcja updateGamepad() { // Pobierz wszystkie podłączone gamepady const gamepady = navigator.getGamepads(); jeśli (!gamepady) powrócą;
// Użyj pierwszego podłączonego gamepada const gp = gamepady[0];
jeśli (gp) { // Aktualizuj stany przycisków, przełączając klasę „aktywną”. btnA.classList.toggle("aktywny", gp.buttons[0].naciśnięty); btnB.classList.toggle("aktywny", gp.buttons[1].wciśnięty); btnX.classList.toggle("aktywny", gp.buttons[2].wciśnięty);
// Obsługa przycisku pauzy (przycisk z indeksem 9 na większości kontrolerów) const pauzaNaciśnięty = gp.buttons[9].naciśnięty; pauza1.classList.toggle("aktywna", pauzaNaciśnięta); pauza2.classList.toggle("aktywna", pauzaNaciśnięta);
// Zbuduj listę aktualnie wciśniętych przycisków w celu wyświetlenia statusu wciśnij = []; gp.buttons.forEach((btn, i) => { jeśli (btn.naciśnięty)naciśnięty.push("Przycisk " + i); });
// Zaktualizuj tekst statusu, jeśli naciśnięty zostanie jakikolwiek przycisk if (długość wciśnięcia > 0) { status.textContent = "Naciśnięto: " + naciśnięto.join(", "); } }
// Kontynuuj pętlę, jeśli debuger jest uruchomiony jeśli (działa) { rafId = requestAnimationFrame(updateGamepad); } }
Metoda classList.toggle() dodaje lub usuwa aktywną klasę w zależności od tego, czy przycisk został naciśnięty, co wyzwala nasze style warstw CSS. Krok 5: Obsługa zdarzeń klawiatury Te detektory zdarzeń sprawiają, że funkcja powrotu klawiatury działa: // =================================== // PROGRAM OBSŁUGI ZDARZEŃ KLAWIATUROWYCH // ===================================
document.addEventListener("keydown", (e) => { if (keyMap[e.key]) { // Obsługa jednego lub wielu elementów if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("active")); } jeszcze { keyMap[e.key].classList.add("aktywny"); } status.textContent = "Naciśnięto klawisz: " + e.key.toUpperCase(); } });
document.addEventListener("keyup", (e) => { if (keyMap[e.key]) { // Usuń stan aktywny po zwolnieniu klawisza if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("active")); } jeszcze { keyMap[e.key].classList.remove("aktywny"); } status.textContent = "Klucz zwolniony: " + e.key.toUpperCase(); } });
Krok 6: Dodaj kontrolę Start/Stop Na koniec potrzebujemy sposobu na włączanie i wyłączanie debugera: // =================================== // WŁĄCZ/WYŁĄCZ DEBUGGER // ===================================
document.getElementById("przełącz").addEventListener("kliknij", () => { bieganie = !bieganie; // Odwróć stan działania
jeśli (działa) { status.textContent = "Debuger działa..."; aktualizacjaGamepada(); // Rozpocznij pętlę aktualizacji } jeszcze { status.textContent = "Debugger nieaktywny"; anuluj ramkę animacji (rafId); // Zatrzymaj pętlę } });
Więc tak, naciśnij przycisk i zacznie świecić. Naciśnij drążek, a on się poruszy. To wszystko. Jeszcze jedno: surowe wartości. Czasami chcesz po prostu zobaczyć liczby, a nie światła.
Na tym etapie powinieneś zobaczyć:
Prosty kontroler ekranowy, Przyciski reagujące podczas interakcji z nimi oraz Opcjonalny odczyt debugowania pokazujący indeksy naciśniętych przycisków.
Aby było to mniej abstrakcyjne, oto krótka demonstracja kontrolera ekranowego reagującego w czasie rzeczywistym:
Teraz naciśnięcie przycisku Rozpocznij nagrywanie rejestruje wszystko, dopóki nie naciśniesz przycisku Zatrzymaj nagrywanie. 2. Eksportowanie danych do CSV/JSON Gdy już będziemy mieć dziennik, będziemy chcieli go zapisać.
Krok 1: Utwórz pomocnika pobierania Na początek potrzebujemy funkcji pomocniczej, która obsłuży pobieranie plików w przeglądarce: // =================================== // POMOC POBIERANIA PLIKU // ===================================
funkcja downloadFile(nazwa pliku, treść, typ = „tekst/zwykły”) { // Utwórz obiekt BLOB na podstawie zawartości const blob = nowy obiekt Blob([treść], {typ }); const url = URL.createObjectURL(blob);
// Utwórz tymczasowy link do pobrania i kliknij go const a = document.createElement("a"); a.href = adres URL; a.download = nazwa pliku; a.kliknięcie();
// Oczyść adres URL obiektu po pobraniu setTimeout(() => URL.revokeObjectURL(url), 100); }
Ta funkcja działa poprzez utworzenie obiektu Blob (duży obiekt binarny) na podstawie danych, wygenerowanie dla niego tymczasowego adresu URL i programowe kliknięcie łącza pobierania. Czyszczenie gwarantuje, że nie wycieknie pamięć. Krok 2: Obsługuj eksport JSON JSON jest idealny do zachowania pełnej struktury danych:
// =================================== // EKSPORT JAKO JSON // ===================================
document.getElementById("export-json").addEventListener("kliknij", () => { // Sprawdź, czy jest coś do wyeksportowania if (!frames.length) { console.warn("Brak nagrań do wyeksportowania."); powrót; }
// Utwórz ładunek z metadanymi i ramkami stały ładunek = { utworzonyAt: nowa Data().toISOString(), ramki };
// Pobierz w formacie JSON pobierz plik ( "log-gamepad.json", JSON.stringify(ładunek, null, 2), „aplikacja/json” ); });
Format JSON zapewnia uporządkowaną strukturę i łatwą analizę, dzięki czemu idealnie nadaje się do ponownego ładowania do narzędzi programistycznych lub udostępniania członkom zespołu. Krok 3: Obsługuj eksport CSV W przypadku eksportu CSV musimy spłaszczyć dane hierarchiczne w wierszach i kolumnach:
//=================================== // EKSPORT JAKO CSV // ===================================
document.getElementById("export-csv").addEventListener("kliknij", () => { // Sprawdź, czy jest coś do wyeksportowania if (!frames.length) { console.warn("Brak nagrań do wyeksportowania."); powrót; }
// Utwórz wiersz nagłówka CSV (kolumny ze znacznikiem czasu, wszystkie przyciski, wszystkie osie) const headerButtons = ramki[0].buttons.map((_, i) => btn${i}); const headerAxes = ramki[0].axes.map((_, i) => oś${i}); const nagłówek = ["t", ...headerButtons, ...headerAxes].join(",") + "\n";
// Utwórz wiersze danych CSV stałe wiersze = ramki.map(f => { const btnVals = f.buttons.map(b => b.value); return [f.t, ...btnVals, ...f.axes].join(","); }).dołącz("\n");
// Pobierz jako plik CSV downloadFile("gamepad-log.csv", nagłówek + wiersze, "tekst/csv"); });
CSV doskonale nadaje się do analizy danych, ponieważ otwiera się bezpośrednio w Excelu lub Arkuszach Google, umożliwiając tworzenie wykresów, filtrowanie danych lub wizualne wzorce punktowe. Teraz, gdy przyciski eksportu są już dostępne, na panelu zobaczysz dwie nowe opcje: Eksportuj JSON i Eksportuj CSV. JSON jest fajny, jeśli chcesz wrzucić surowy dziennik z powrotem do narzędzi programistycznych lub poszperać po strukturze. Z drugiej strony plik CSV otwiera się bezpośrednio w programie Excel lub Arkuszach Google, dzięki czemu możesz tworzyć wykresy, filtrować i porównywać dane wejściowe. Poniższy rysunek pokazuje, jak wygląda panel z dodatkowymi elementami sterującymi.
3. System migawek Czasami nie potrzebujesz pełnego nagrania, wystarczy szybki „zrzut ekranu” stanów wejściowych. W tym właśnie pomaga przycisk Zrób migawkę.
Oraz JavaScript:
// =================================== // ZRÓB ZDJĘCIE // ===================================
document.getElementById("migawka").addEventListener("kliknij", () => { // Pobierz wszystkie podłączone gamepady const pady = navigator.getGamepads(); const aktywne pady = [];
// Wykonaj pętlę i przechwyć stan każdego podłączonego gamepada for (const GP padów) { jeśli (!gp) kontynuuj; // Pomiń puste miejsca
aktywnePads.push({ id: gp.id, // Nazwa/model kontrolera znacznik czasu: wydajność.now(), przyciski: gp.buttons.map(b => ({ wciśnięty: b.wciśnięty, wartość: b.wartość })), osie: [...gp.osie] }); }
// Sprawdź, czy znaleziono jakieś gamepady if (!activePads.length) { console.warn("Brak podłączonych gamepadów do wykonania migawki."); alert("Nie wykryto kontrolera!"); powrót; }
// Zaloguj i powiadom użytkownika console.log("Migawka:", aktywne Pady); alert(Zrobiono migawkę! Przechwycono kontroler(y) ${activePads.length}).); });
Migawki zamrażają dokładny stan kontrolera w danym momencie. 4. Powtórka wejścia ducha A teraz zabawa: powtórka wejścia ducha. Spowoduje to pobranie dziennika i odtworzenie go wizualnie, tak jakby fantomowy gracz korzystał z kontrolera.
JavaScript do powtórki: // =================================== // POWTÓRKA DUCHA // ===================================
document.getElementById("powtórka").addEventListener("kliknij", () => { // Upewnij się, że mamy nagranie do odtworzenia if (!frames.length) { alert("Brak nagrania do odtworzenia!"); powrót; }
console.log("Rozpoczęcie odtwarzania ducha...");
// Czas śledzenia dla zsynchronizowanego odtwarzania niech startTime = wydajność.now(); niech indeks ramki = 0;
// Powtórz pętlę animacji krok funkcji() { stała teraz = wydajność.now(); const, który upłynął = teraz - czas rozpoczęcia;
// Przetwórz wszystkie ramki, które powinny już wystąpić póki (indeks ramki < długość klatek && ramki [Indeks ramki].t <= upłynął) { const ramka = ramki[frameIndex];
// Zaktualizuj interfejs użytkownika za pomocą zarejestrowanych stanów przycisków btnA.classList.toggle("aktywny", ramka.buttons[0].naciśnięty); btnB.classList.toggle("aktywny", ramka.buttons[1].naciśnięty); btnX.classList.toggle("aktywny", ramka.buttons[2].naciśnięty);
// Aktualizacja wyświetlania stanu wciśnij = []; ramka.buttons.forEach((btn, i) => { if (btn.pressed) naciśnięty.push("Przycisk " + i); }); if (długość wciśnięcia > 0) { status.textContent = "Duch: " + naciśnięty.join(", "); }
indeks ramki++; }
// Kontynuuj pętlę, jeśli jest więcej ramek if (indeks ramki < długość klatek) { requestAnimationFrame(krok); } jeszcze { console.log("Odtwórz ponownieskończone.”); status.textContent = "Powtórka zakończona"; } }
// Rozpocznij powtórkę krok(); });
Aby debugowanie było bardziej praktyczne, dodałem powtórkę-widmo. Po nagraniu sesji możesz kliknąć przycisk „Powtórz” i zobaczyć, jak interfejs użytkownika wykonuje daną sesję, zupełnie jakby fantomowy gracz uruchamiał pad. W tym celu w panelu pojawia się nowy przycisk Replay Ghost.
Naciśnij przycisk Nagraj, pobaw się trochę kontrolerem, zatrzymaj i odtwórz ponownie. Interfejs użytkownika po prostu odzwierciedla wszystko, co zrobiłeś, jak duch śledzący Twoje dane wejściowe. Po co zawracać sobie głowę tymi dodatkami?
Nagrywanie/eksportowanie ułatwia testerom dokładne pokazanie, co się stało. Migawki zatrzymują chwilę w czasie, co jest bardzo przydatne, gdy szukasz dziwnych błędów. Powtórka Ghost doskonale nadaje się do samouczków, sprawdzania dostępności lub po prostu porównywania konfiguracji sterowania.
W tym momencie nie jest to już tylko schludne demo, ale coś, co można faktycznie zastosować. Przypadki użycia w świecie rzeczywistym Teraz mamy debuger, który potrafi wiele. Pokazuje dane wejściowe na żywo, rejestruje logi, eksportuje je, a nawet odtwarza. Ale prawdziwe pytanie brzmi: kogo to właściwie obchodzi? Dla kogo jest to przydatne? Twórcy gier Kontrolery są częścią tego zadania, ale debugowanie ich? Zwykle ból. Wyobraź sobie, że testujesz kombinację bijatyk, na przykład ↓ → + uderzenie. Zamiast się modlić, nacisnąłeś go dwa razy w ten sam sposób, nagrałeś raz i odtworzyłeś. Gotowe. Lub wymieniasz dzienniki JSON z członkiem zespołu, aby sprawdzić, czy Twój kod gry wieloosobowej reaguje tak samo na jego komputerze. To ogromne. Praktycy dostępności Ten jest bliski mojemu sercu. Nie każdy gra ze „standardowym” kontrolerem. Kontrolery adaptacyjne czasami wysyłają dziwne sygnały. Dzięki temu narzędziu możesz dokładnie zobaczyć, co się dzieje. Nauczyciele, badacze, kimkolwiek. Mogą pobierać dzienniki, porównywać je lub odtwarzać dane wejściowe obok siebie. Nagle niewidzialne rzeczy stają się oczywiste. Testowanie zapewnienia jakości Testerzy zazwyczaj piszą notatki typu „Wcisnąłem tutaj przyciski i się zepsuło”. Niezbyt pomocne. Teraz? Mogą zarejestrować dokładne prasy, wyeksportować dziennik i wysłać go. Bez zgadywania. Wychowawcy Jeśli tworzysz tutoriale lub filmy na YouTube, odtwarzanie duchów jest złotem. Możesz dosłownie powiedzieć: „Oto, co zrobiłem z kontrolerem”, podczas gdy interfejs użytkownika pokazuje, że to się dzieje. Sprawia, że wyjaśnienia są znacznie jaśniejsze. Poza grami I tak, nie chodzi tu tylko o gry. Ludzie używali kontrolerów do robotów, projektów artystycznych i interfejsów dostępności. Za każdym razem ten sam problem: co tak naprawdę widzi przeglądarka? Dzięki temu nie musisz zgadywać. Wniosek Debugowanie wejścia kontrolera zawsze przypominało latanie na ślepo. W przeciwieństwie do DOM lub CSS, nie ma wbudowanego inspektora gamepadów; to tylko surowe liczby w konsoli, które łatwo zgubić w hałasie. Za pomocą kilkuset linii kodu HTML, CSS i JavaScript zbudowaliśmy coś innego:
Debuger wizualny, który sprawia, że niewidoczne dane wejściowe stają się widoczne. Warstwowy system CSS, który utrzymuje interfejs użytkownika w czystości i umożliwia debugowanie. Zestaw ulepszeń (nagrywanie, eksportowanie, migawki, odtwarzanie duchów), które przenoszą go z wersji demonstracyjnej do narzędzia programistycznego.
Ten projekt pokazuje, jak daleko można zajść, łącząc moc platformy internetowej z odrobiną kreatywności w warstwach kaskadowych CSS. Narzędzie, które właśnie wyjaśniłem w całości, jest oprogramowaniem typu open source. Możesz sklonować repozytorium GitHub i wypróbować je samodzielnie. Ale co ważniejsze, możesz zrobić to sam. Dodaj własne warstwy. Zbuduj własną logikę powtórek. Zintegruj go z prototypem gry. Albo nawet używać go w sposób, jakiego sobie nie wyobrażałem. Do nauczania, dostępności lub analizy danych. Ostatecznie nie chodzi tylko o debugowanie gamepadów. Chodzi o rzucenie światła na ukryte dane wejściowe i zapewnienie programistom pewności w pracy ze sprzętem, którego sieć wciąż nie obsługuje w pełni. Podłącz więc kontroler, otwórz edytor i zacznij eksperymentować. Możesz być zaskoczony tym, co naprawdę potrafi Twoja przeglądarka i Twój CSS.