Когато включите контролер, натискате бутони, местите стикове, дърпате спусъците... и като разработчик не виждате нищо от това. Браузърът го прихваща, разбира се, но освен ако не записвате числа в конзолата, той е невидим. Това е главоболието с API на Gamepad. Съществува от години и всъщност е доста мощен. Можете да четете бутони, стикове, спусъци, работата. Но повечето хора не го докосват. защо Защото няма обратна връзка. Няма панел в инструментите за разработчици. Няма ясен начин да разберете дали контролерът дори прави това, което мислите. Усещането е като да летиш на сляпо. Това ме изнерви достатъчно, за да създам малък инструмент: Gamepad Cascade Debugger. Вместо да се взирате в изхода на конзолата, вие получавате жив интерактивен изглед на контролера. Натиснете нещо и то реагира на екрана. А с CSS Cascade Layers стиловете остават организирани, така че е по-чисто да се отстраняват грешки. В тази публикация ще ви покажа защо отстраняването на грешки в контролерите е такава болка, как CSS помага да го изчистите и как можете да създадете визуален дебъгер за многократна употреба за вашите собствени проекти.
Дори и да можете да ги регистрирате всички, бързо ще се окажете с нечетлив конзолен спам. Например: [0,0,1,0,0,0,5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]
Можете ли да кажете кой бутон е натиснат? Може би, но само след като напрегнете очите си и пропуснете няколко входа. Така че, не, отстраняването на грешки не идва лесно, когато става въпрос за четене на входове. Проблем 3: Липса на структура Дори ако съберете бърз визуализатор, стиловете могат бързо да се объркат. Състоянието по подразбиране, активното и отстраняването на грешки могат да се припокриват и без ясна структура вашият CSS става крехък и труден за разширяване. CSS каскадните слоеве могат да помогнат. Те групират стиловете в „слоеве“, които са подредени по приоритет, така че да спрете да се борите със специфичността и да гадаете „Защо моят стил за отстраняване на грешки не се показва?“ Вместо това поддържате отделни притеснения:
База: стандартен първоначален външен вид на контролера. Активен: Акценти за натиснати бутони и преместени стикове. Отстраняване на грешки: наслагвания за разработчици (напр. цифрови показания, ръководства и т.н.).
Ако трябваше да дефинираме слоеве в CSS според това, щяхме да имаме: /* най-нисък към най-висок приоритет */ @слой база, активен, отстраняване на грешки;
@слой база { /* ... */ }
@слой активен { /* ... */ }
@layer отстраняване на грешки { /* ... */ }
Тъй като всеки слой се подрежда предсказуемо, винаги знаете кои правила печелят. Тази предсказуемост прави отстраняването на грешки не просто по-лесно, но всъщност управляемо. Покрихме проблема (невидимо, разхвърляно въвеждане) и подхода (визуален дебъгер, изграден с каскадни слоеве). Сега ще преминем през процеса стъпка по стъпка за изграждане на дебъгера. Концепцията за дебъгер Най-лесният начин да направите скрития вход видим е просто да го нарисувате на екрана. Това прави този дебъгер. Бутоните, тригерите и джойстиците получават визуално изображение.
Натиснете A: Светва кръг. Побутване на пръчката: кръгът се плъзга наоколо. Дръпнете спусъка наполовина: Лента се запълва наполовина.
Сега не се взирате в 0s и 1s, а всъщност наблюдавате как контролерът реагира на живо. Разбира се, след като започнете да трупате състояния като по подразбиране, натиснат, информация за отстраняване на грешки, може би дори режим на запис, CSS започва да става по-голям и по-сложен. Това е мястото, където каскадните слоеве са полезни. Ето съкратен пример: @слой база { .button { фон: #222; граница-радиус: 50%; ширина: 40px; височина: 40px; } }
@слой активен { .button.pressed { фон: #0f0; /* ярко зелено */ } }
@layer отстраняване на грешки { .button::after { съдържание: attr(стойност-данни); размер на шрифта: 12px; цвят: #fff; } }
Редът на слоевете има значение: база → активен → отстраняване на грешки.
база рисува контролера. активно обработва натиснати състояния. отстраняване на грешки хвърля върху наслагвания.
Разбиването му по този начин означава, че не водите странни войни за специфичност. Всеки слой има своето място и винаги знаете кое печели. Изграждане Нека първо да видим нещо на екрана. Не е нужно да изглежда добре - просто трябва да съществува, за да имаме с какво да работим.
Gamepad Cascade Debugger
Това са буквално само кутии. Все още не е вълнуващо, но ни дава манипулации, които да грабнем по-късно с CSS и JavaScript. Добре, използвам каскадни слоеве тук, защото поддържат нещата организирани, след като добавите повече състояния. Ето един груб пропуск:
/* ==================================== НАСТРОЙКА НА КАСКАДНИ СЛОЕВЕ Редът има значение: база → активен → отстраняване на грешки ==================================== */
/* Определете реда на слоевете предварително */ @слой база, активен, отстраняване на грешки;
/* Слой 1: Базови стилове - вид по подразбиране */ @слой база { .button { фон: #333; граница-радиус: 50%; ширина: 70px; височина: 70px; дисплей: гъвкав; justify-content: център; подравняване на елементи: център; }
.пауза { ширина: 20px; височина: 70px; фон: #333; дисплей: inline-block; } }
/* Слой 2: Активни състояния - обработва натиснати бутони */ @слой активен { .button.active { фон: #0f0; /* Ярко зелено при натискане */ трансформиране: мащаб (1.1); /* Леко уголемява бутона */ }
.pause.active { фон: #0f0; трансформация: scaleY(1.1); /* Разтяга се вертикално при натискане */ } }
/* Слой 3: наслагвания за отстраняване на грешки - информация за разработчици */ @layer отстраняване на грешки { .button::after { съдържание: attr(стойност-данни); /* Показва числовата стойност */ размер на шрифта: 12px; цвят: #fff; } }
Красотата на този подход е, че всеки слой има ясна цел. Базовият слой никога не може да замени активния, а активният никога не може да замени отстраняването на грешки, независимо от спецификата. Това елиминира войните за спецификата на CSS, които обикновено тормозят инструментите за отстраняване на грешки. Сега изглежда, че някои клъстери седят на тъмен фон. Честно казано, не е много лошо.
Добавяне на JavaScript време на JavaScript. Това е мястото, където контролерът всъщност прави нещо. Ще изградим това стъпка по стъпка. Стъпка 1: Настройте управление на състоянието Първо, имаме нужда от променливи, за да проследим състоянието на дебъгера: // ==================================== // ДЪРЖАВНО УПРАВЛЕНИЕ // ====================================
нека работи = невярно; // Проследява дали дебъгерът е активен нека rafId; // Съхранява ID на requestAnimationFrame за анулиране
Тези променливи контролират анимационния цикъл, който непрекъснато чете входа на геймпада. Стъпка 2: Вземете DOM препратки След това получаваме препратки към всички HTML елементи, които ще актуализираме: // ==================================== // РЕФЕРЕНЦИИ НА DOM ЕЛЕМЕНТИ // ====================================
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("статус");
Съхраняването на тези препратки предварително е по-ефективно от повторното запитване до DOM. Стъпка 3: Добавяне на резервна клавиатура За тестване без физически контролер ще съпоставим клавишите на клавиатурата с бутоните: // ==================================== // РЕЗЕРВНА КЛАВИАТУРА (за тестване без контролер) // ====================================
const keyMap = { "a": btnA, "b": btnB, "x": btnX, "p": [пауза1, пауза2] // Клавиш 'p' контролира и двете ленти за пауза };
Това ни позволява да тестваме потребителския интерфейс чрез натискане на клавиши на клавиатурата. Стъпка 4: Създайте основния цикъл за актуализиране Ето къде се случва магията. Тази функция работи непрекъснато и чете състоянието на геймпада: // ==================================== // ГЛАВЕН ЦИКЪЛ ЗА АКТУАЛИЗАЦИЯ НА ГЕЙМПАДА // ====================================
функция updateGamepad() { // Вземете всички свързани геймпадове const gamepads = navigator.getGamepads(); if (!gamepads) return;
// Използвайте първия свързан геймпад const gp = геймпади [0];
if (gp) { // Актуализиране на състоянията на бутоните чрез превключване на класа „активен“. btnA.classList.toggle("активен", gp.buttons[0].натиснат); btnB.classList.toggle("активен", gp.buttons[1].натиснат); btnX.classList.toggle("активен", gp.buttons[2].натиснат);
// Управление на бутона за пауза (индекс на бутона 9 на повечето контролери) const pausePressed = gp.buttons[9].pressed; pause1.classList.toggle("активен", pausePressed); pause2.classList.toggle("активен", pausePressed);
// Създаване на списък с текущо натиснати бутони за показване на състоянието нека е натиснат = []; gp.buttons.forEach((btn, i) => { ако (btn.pressed)pressed.push("Бутон " + i); });
// Актуализиране на текста на състоянието, ако бъдат натиснати бутони if (pressed.length > 0) { status.textContent = "Натиснат: " + pressed.join(", "); } }
// Продължаване на цикъла, ако дебъгерът работи ако (работи) { rafId = requestAnimationFrame(updateGamepad); } }
Методът classList.toggle() добавя или премахва активния клас въз основа на това дали бутонът е натиснат, което задейства нашите стилове на CSS слой. Стъпка 5: Обработвайте събития на клавиатурата Тези слушатели на събития карат резервната клавиатура да работи: // ==================================== // ОБРАБОТАТЕЛИ НА СЪБИТИЯ НА КЛАВИАТУРАТА // ====================================
document.addEventListener("keydown", (e) => { if (keyMap[e.key]) { // Обработва единични или множество елементи if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("active")); } иначе { keyMap[e.key].classList.add("активен"); } status.textContent = "Натиснат клавиш: " + e.key.toUpperCase(); } });
document.addEventListener("keyup", (e) => { if (keyMap[e.key]) { // Премахване на активно състояние при отпускане на клавиша if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("active")); } иначе { keyMap[e.key].classList.remove("активен"); } status.textContent = "Ключът е освободен: " + e.key.toUpperCase(); } });
Стъпка 6: Добавяне на контрол Старт/Стоп И накрая, имаме нужда от начин да включваме и изключваме дебъгера: // ==================================== // ВКЛЮЧВАНЕ/ИЗКЛЮЧВАНЕ НА ДЕБЪГЕР // ====================================
document.getElementById("toggle").addEventListener("click", () => { тичане = !бягане; // Обръщане на работещото състояние
ако (работи) { status.textContent = "Debugger работи..."; updateGamepad(); // Стартиране на цикъла за актуализиране } иначе { status.textContent = "Дебъгерът е неактивен"; cancelAnimationFrame(rafId); // Спрете цикъла } });
Така че да, натиснете бутон и той свети. Натиснете пръчката и тя се движи. Това е. Още нещо: сурови стойности. Понякога просто искате да видите числа, а не светлини.
На този етап трябва да видите:
Прост контролер на екрана, Бутони, които реагират, когато взаимодействате с тях, и Допълнително показание за отстраняване на грешки, показващо индекси на натиснати бутони.
За да направите това по-малко абстрактно, ето бърза демонстрация на екранния контролер, реагиращ в реално време:
Сега, натискането на Start Recording записва всичко, докато не натиснете Stop Recording. 2. Експортиране на данни в CSV/JSON След като имаме дневник, ще искаме да го запазим.
Стъпка 1: Създайте помощника за изтегляне Първо, имаме нужда от помощна функция, която обработва изтеглянията на файлове в браузъра: // ==================================== // ПОМОЩНИК ЗА ИЗТЕГЛЯНЕ НА ФАЙЛОВЕ // ====================================
функция downloadFile(име на файл, съдържание, тип = "текст/обикновен") { // Създаване на петно от съдържанието const blob = нов Blob([съдържание], { тип }); const url = URL.createObjectURL(blob);
// Създайте временна връзка за изтегляне и щракнете върху нея const a = document.createElement("a"); a.href = url; a.download = име на файл; a.click();
// Почистете URL адреса на обекта след изтегляне setTimeout(() => URL.revokeObjectURL(url), 100); }
Тази функция работи чрез създаване на Blob (двоичен голям обект) от вашите данни, генериране на временен URL за него и програмно щракване върху връзка за изтегляне. Почистването гарантира, че няма да изтече памет. Стъпка 2: Справете се с експортирането на JSON JSON е идеален за запазване на цялата структура на данните:
// ==================================== // ЕКСПОРТ КАТО JSON // ====================================
document.getElementById("export-json").addEventListener("click", () => { // Проверете дали има нещо за експортиране if (!frames.length) { console.warn("Няма наличен запис за експортиране."); връщане; }
// Създаване на полезен товар с метаданни и рамки const полезен товар = { createdAt: нова дата().toISOString(), рамки };
// Изтегляне като форматиран JSON изтегляне на файл ( "gamepad-log.json", JSON.stringify(полезен товар, нула, 2), "приложение/json" ); });
Форматът JSON поддържа всичко структурирано и лесно анализируемо, което го прави идеален за зареждане обратно в инструменти за разработка или споделяне със съотборници. Стъпка 3: Справете се с експортирането на CSV За CSV експортиране трябва да изравним йерархичните данни в редове и колони:
//==================================== // ЕКСПОРТ КАТО CSV // ====================================
document.getElementById("export-csv").addEventListener("click", () => { // Проверете дали има нещо за експортиране if (!frames.length) { console.warn("Няма наличен запис за експортиране."); връщане; }
// Създаване на заглавен ред на CSV (колони за клеймо за време, всички бутони, всички оси) 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";
// Създаване на CSV редове с данни const rows = frames.map(f => { const btnVals = f.buttons.map(b => b.value); връщане [f.t, ...btnVals, ...f.axes].join(","); }).присъединяване("\n");
// Изтегляне като CSV downloadFile("gamepad-log.csv", заглавка + редове, "текст/csv"); });
CSV е брилянтен за анализ на данни, тъй като се отваря директно в Excel или Google Sheets, което ви позволява да създавате диаграми, да филтрирате данни или да забелязвате визуално модели. Сега, когато бутоните за експортиране са включени, ще видите две нови опции на панела: Експортиране на JSON и Експортиране на CSV. JSON е хубав, ако искате да хвърлите необработения дневник обратно във вашите инструменти за разработка или да бъркате в структурата. CSV, от друга страна, се отваря направо в Excel или Google Таблици, така че можете да диаграмирате, филтрирате или сравнявате входове. Следната фигура показва как изглежда панелът с тези допълнителни контроли.
3. Система за моментни снимки Понякога не се нуждаете от пълен запис, а само от бърза „екранна снимка“ на състоянията на въвеждане. Това е мястото, където бутонът Правене на моментна снимка помага.
И JavaScript:
// ==================================== // НАПРАВЕТЕ МОМЕНТНА СНИМКА // ====================================
document.getElementById("моментна снимка").addEventListener("клик", () => { // Вземете всички свързани геймпадове const pads = navigator.getGamepads(); const activePads = [];
// Преглеждайте и улавяйте състоянието на всеки свързан геймпад за (const gp подложки) { ако (!gp) продължи; // Пропускане на празни слотове
activePads.push({ id: gp.id, // Име/модел на контролера клеймо: performance.now(), бутони: gp.buttons.map(b => ({ натиснат: b.натиснат, стойност: b.стойност })), оси: [...gp.axes] }); }
// Проверете дали са намерени геймпадове if (!activePads.length) { console.warn("Няма свързани геймпадове за моментна снимка."); предупреждение ("Не е открит контролер!"); връщане; }
// Влезте и уведомете потребителя console.log("Моментна снимка:", activePads); предупреждение (Направена е моментна снимка! Уловен ${activePads.length} контролер(и).); });
Моментните снимки замразяват точното състояние на вашия контролер в даден момент. 4. Възпроизвеждане на Ghost Input Сега за забавното: повторение на въвеждане на призрак. Това взема дневник и го възпроизвежда визуално, сякаш играч фантом използва контролера.
JavaScript за повторение: // ==================================== // ПОВТОРЕНИЕ НА ПРИЗРАК // ====================================
document.getElementById("replay").addEventListener("click", () => { // Уверете се, че имаме запис за възпроизвеждане if (!frames.length) { предупреждение ("Няма запис за повторение!"); връщане; }
console.log("Стартиране на повторение на призрак...");
// Проследяване на времето за синхронизирано възпроизвеждане нека startTime = performance.now(); нека frameIndex = 0;
// Възпроизвеждане на анимационен цикъл функция step() { const сега = performance.now(); const изтекло = сега - начален час;
// Обработка на всички кадри, които трябва да са се появили досега докато (frameIndex < frames.length && frames[frameIndex].t <= elapsed) { const frame = frames[frameIndex];
// Актуализиране на потребителския интерфейс със записаните състояния на бутоните btnA.classList.toggle("активен", frame.buttons[0].натиснат); btnB.classList.toggle("активен", frame.buttons[1].натиснат); btnX.classList.toggle("active", frame.buttons[2].pressed);
// Актуализиране на дисплея на състоянието нека е натиснат = []; frame.buttons.forEach((btn, i) => { if (btn.pressed) pressed.push("Button " + i); }); if (pressed.length > 0) { status.textContent = "Призрак: " + pressed.join(", "); }
frameIndex++; }
// Продължаване на цикъла, ако има още кадри if (frameIndex < frames.length) { requestAnimationFrame(стъпка); } иначе { console.log("Повторно пусканезавършен."); status.textContent = "Повторното завършване"; } }
// Стартиране на повторението стъпка(); });
За да направя отстраняването на грешки малко по-практическо, добавих призрачно повторение. След като сте записали сесия, можете да натиснете повторно възпроизвеждане и да гледате как потребителският интерфейс го изиграва, почти като фантомен играч, който управлява подложката. За това в панела се показва нов бутон Replay Ghost.
Натиснете Record, побъркайте се малко с контролера, спрете и след това повторете. Потребителският интерфейс просто отразява всичко, което сте направили, като призрак, който следва въведените от вас данни. Защо да се занимавате с тези екстри?
Записването/експортирането улеснява тестерите да покажат точно какво се е случило. Моментните снимки замръзват за момент във времето, супер полезни, когато преследвате странни грешки. Повторното възпроизвеждане на Ghost е страхотно за уроци, проверки за достъпност или просто сравняване на контролни настройки една до друга.
На този етап това вече не е просто чиста демонстрация, а нещо, което всъщност можете да приложите на работа. Случаи на употреба в реалния свят Сега имаме този дебъгер, който може да направи много. Той показва въвеждане на живо, записва регистрационни файлове, експортира ги и дори възпроизвежда неща. Но истинският въпрос е: на кого всъщност му пука? За кого е полезно това? Разработчици на игри Контролерите са част от работата, но тяхното отстраняване на грешки? Обикновено болка. Представете си, че тествате комбинация от бойна игра, като ↓ → + удар. Вместо да се молите, вие го натискате по същия начин два пъти, записвате го веднъж и го пускате отново. Готово. Или разменяте JSON регистрационни файлове със съотборник, за да проверите дали кодът ви за мултиплейър реагира по същия начин на тяхната машина. Това е огромно. Практици по достъпност Това е близо до сърцето ми. Не всеки играе със „стандартен“ контролер. Адаптивните контролери понякога изхвърлят странни сигнали. С този инструмент можете да видите точно какво се случва. Учители, изследователи, който и да е. Те могат да вземат регистрационни файлове, да ги сравняват или да възпроизвеждат въведени данни един до друг. Изведнъж невидимите неща стават очевидни. Тестване за осигуряване на качеството Тестерите обикновено пишат бележки като „Намачках бутони тук и се счупиха“. Не е много полезно. Сега? Те могат да заснемат точните преси, да експортират дневника и да го изпратят. Без гадаене. Педагози Ако правите уроци или видеоклипове в YouTube, повторението на призрак е злато. Можете буквално да кажете „Ето какво направих с контролера“, докато потребителският интерфейс показва, че това се случва. Прави обясненията много по-ясни. Отвъд игрите И да, това не е само за игри. Хората са използвали контролери за роботи, арт проекти и интерфейси за достъпност. Същият проблем всеки път: какво всъщност вижда браузърът? С това не е нужно да гадаете. Заключение Отстраняването на грешки на входа на контролера винаги се е чувствало като летене на сляпо. За разлика от DOM или CSS, няма вграден инспектор за геймпадове; това са просто необработени числа в конзолата, които лесно се губят в шума. С няколкостотин реда HTML, CSS и JavaScript създадохме нещо различно:
Визуален дебъгер, който прави невидимите входове видими. Многослойна CSS система, която поддържа потребителския интерфейс чист и с възможност за отстраняване на грешки. Набор от подобрения (запис, експортиране, моментни снимки, призрачно възпроизвеждане), които го издигат от демонстрационен до инструмент за разработчици.
Този проект показва колко далеч можете да стигнете, като смесите силата на уеб платформата с малко креативност в CSS Cascade Layers. Инструментът, който току-що обясних в неговата цялост, е с отворен код. Можете да клонирате репото на GitHub и да го изпробвате сами. Но по-важното е, че можете да го направите свой собствен. Добавете свои собствени слоеве. Изградете своя собствена логика за повторение. Интегрирайте го с вашия прототип на игра. Или дори да го използвам по начини, които не съм си представял. За преподаване, достъпност или анализ на данни. В крайна сметка не става въпрос само за отстраняване на грешки в геймпадове. Става въпрос за хвърляне на светлина върху скритите входове и даване на увереност на разработчиците да работят с хардуер, който мрежата все още не приема напълно. Така че, включете вашия контролер, отворете своя редактор и започнете да експериментирате. Може да се изненадате какво наистина могат да постигнат вашият браузър и вашият CSS.