Saat Anda menyambungkan pengontrol, Anda menekan tombol, menggerakkan tongkat, menarik pelatuk… dan sebagai pengembang, Anda tidak melihatnya. Tentu saja, browser mengambilnya, tetapi kecuali Anda mencatat nomor di konsol, nomor tersebut tidak akan terlihat. Itulah yang memusingkan dengan Gamepad API. Sudah ada selama bertahun-tahun, dan sebenarnya cukup kuat. Anda dapat membaca tombol, stik, pemicu, cara kerjanya. Namun kebanyakan orang tidak menyentuhnya. Mengapa? Karena tidak ada umpan balik. Tidak ada panel di alat pengembang. Tidak ada cara yang jelas untuk mengetahui apakah pengontrol melakukan apa yang Anda pikirkan. Rasanya seperti terbang buta. Itu cukup mengganggu saya untuk membuat alat kecil: Gamepad Cascade Debugger. Daripada menatap keluaran konsol, Anda mendapatkan tampilan pengontrol yang langsung dan interaktif. Tekan sesuatu dan itu bereaksi di layar. Dan dengan CSS Cascade Layers, gayanya tetap teratur, sehingga lebih mudah untuk di-debug. Dalam postingan ini, saya akan menunjukkan kepada Anda mengapa pengontrol debug sangat merepotkan, bagaimana CSS membantu membersihkannya, dan bagaimana Anda dapat membuat debugger visual yang dapat digunakan kembali untuk proyek Anda sendiri.
Meskipun Anda dapat mencatat semuanya, Anda akan segera mendapatkan spam konsol yang tidak dapat dibaca. Misalnya: [0,0,1,0,0,0.5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]
Bisakah Anda mengetahui tombol apa yang ditekan? Mungkin, tetapi hanya setelah menajamkan mata dan melewatkan beberapa masukan. Jadi, tidak, proses debug tidak mudah dilakukan saat membaca masukan. Masalah 3: Kurangnya Struktur Bahkan jika Anda membuat visualisator cepat, gaya dapat dengan cepat menjadi berantakan. Status default, aktif, dan debug bisa tumpang tindih, dan tanpa struktur yang jelas, CSS Anda menjadi rapuh dan sulit untuk diperluas. Lapisan Bertingkat CSS dapat membantu. Mereka mengelompokkan gaya ke dalam “lapisan” yang diurutkan berdasarkan prioritas, sehingga Anda berhenti melawan kekhususan dan menebak-nebak, “Mengapa gaya debug saya tidak muncul?” Sebaliknya, Anda memiliki kekhawatiran terpisah:
Dasar: Standar pengontrol, tampilan awal. Aktif: Sorotan untuk tombol yang ditekan dan stik yang digerakkan. Debug: Hamparan untuk pengembang (misalnya pembacaan numerik, panduan, dan sebagainya).
Jika kita mendefinisikan lapisan dalam CSS berdasarkan ini, kita akan mendapatkan: /* prioritas terendah hingga tertinggi */ @lapisan dasar, aktif, debug;
@lapisan dasar { /* ... */ }
@lapisan aktif { /* ... */ }
@ debug lapisan { /* ... */ }
Karena setiap lapisan dapat diprediksi, Anda selalu tahu aturan mana yang menang. Prediktabilitas tersebut membuat proses debug tidak hanya lebih mudah, namun sebenarnya dapat dikelola. Kami telah membahas masalahnya (input yang tidak terlihat dan berantakan) dan pendekatannya (debugger visual yang dibuat dengan Cascade Layers). Sekarang kita akan menjalani proses langkah demi langkah untuk membuat debugger. Konsep Debugger Cara termudah untuk membuat masukan tersembunyi terlihat adalah dengan menggambarnya di layar. Itulah yang dilakukan debugger ini. Tombol, pemicu, dan joystick semuanya ditampilkan secara visual.
Tekan A: Sebuah lingkaran menyala. Sikut tongkatnya: Lingkarannya meluncur. Tarik pelatuk setengah: Bilah terisi separuh.
Sekarang Anda tidak menatap angka 0 dan 1, tetapi sebenarnya menyaksikan pengontrol bereaksi secara langsung. Tentu saja, begitu Anda mulai menumpuk status seperti default, pressed, debug info, bahkan mungkin mode perekaman, CSS mulai menjadi lebih besar dan lebih kompleks. Di situlah lapisan kaskade berguna. Berikut ini contoh sederhananya: @lapisan dasar { .tombol { latar belakang: #222; radius perbatasan: 50%; lebar: 40 piksel; tinggi: 40 piksel; } }
@lapisan aktif { .button.ditekan { latar belakang: #0f0; /* hijau terang */ } }
@ debug lapisan { .tombol::setelah { konten: attr(nilai data); ukuran font: 12px; warna: #fff; } }
Urutan lapisan penting: dasar → aktif → debug.
dasar menarik pengontrol. aktif menangani status yang ditekan. debug lemparan pada overlay.
Memecahnya seperti ini berarti Anda tidak terlibat dalam perang kekhususan yang aneh. Setiap lapisan mempunyai tempatnya masing-masing, dan Anda selalu tahu apa yang menang. Membangunnya Mari kita tampilkan sesuatu di layar terlebih dahulu. Tidak perlu terlihat bagus — hanya perlu ada agar kita punya sesuatu untuk dikerjakan.
Debugger Kaskade Gamepad
Itu sebenarnya hanya kotak. Belum menarik, tapi ini memberi kita pegangan untuk digunakan nanti dengan CSS dan JavaScript. Oke, saya menggunakan lapisan kaskade di sini karena membuat semuanya tetap teratur setelah Anda menambahkan lebih banyak status. Berikut ini gambaran kasarnya:
/* ===== PENYIAPAN LAPISAN CASCADE Urutan penting: basis → aktif → debug ===== */
/* Menentukan urutan lapisan di awal */ @lapisan dasar, aktif, debug;
/* Lapisan 1: Gaya dasar - tampilan default */ @lapisan dasar { .tombol { latar belakang: #333; radius perbatasan: 50%; lebar: 70 piksel; tinggi: 70 piksel; tampilan: fleksibel; justify-content: tengah; menyelaraskan-item: tengah; }
.jeda { lebar: 20 piksel; tinggi: 70 piksel; latar belakang: #333; tampilan: blok sebaris; } }
/* Lapisan 2: Status aktif - menangani tombol yang ditekan */ @lapisan aktif { .button.aktif { latar belakang: #0f0; /* Hijau terang saat ditekan */ transformasi: skala(1.1); /* Memperbesar sedikit tombol */ }
.pause.aktif { latar belakang: #0f0; transformasi: skalaY(1.1); /* Meregangkan secara vertikal saat ditekan */ } }
/* Lapisan 3: Debug overlay - info pengembang */ @ debug lapisan { .tombol::setelah { konten: attr(nilai data); /* Menampilkan nilai numerik */ ukuran font: 12px; warna: #fff; } }
Keunggulan dari pendekatan ini adalah setiap lapisan memiliki tujuan yang jelas. Lapisan dasar tidak pernah bisa menimpa yang aktif, dan yang aktif tidak pernah bisa menimpa debug, apa pun kekhususannya. Hal ini menghilangkan perang kekhususan CSS yang biasanya mengganggu alat debugging. Sekarang sepertinya beberapa cluster berada pada latar belakang gelap. Sejujurnya, tidak terlalu buruk.
Menambahkan JavaScript waktu JavaScript. Di sinilah pengontrol sebenarnya melakukan sesuatu. Kami akan membangunnya selangkah demi selangkah. Langkah 1: Siapkan Manajemen Negara Pertama, kita memerlukan variabel untuk melacak status debugger: // ===== // MANAJEMEN NEGARA // =====
biarkan berjalan = false; // Melacak apakah debugger aktif biarkan rafId; // Menyimpan ID requestAnimationFrame untuk pembatalan
Variabel-variabel ini mengontrol loop animasi yang terus membaca input gamepad. Langkah 2: Ambil Referensi DOM Selanjutnya, kami mendapatkan referensi ke semua elemen HTML yang akan kami perbarui: // ===== // REFERENSI ELEMEN DOM // =====
const btnA = dokumen.getElementById("btn-a"); const btnB = dokumen.getElementById("btn-b"); const btnX = dokumen.getElementById("btn-x"); const jeda1 = dokumen.getElementById("pause1"); const jeda2 = dokumen.getElementById("pause2"); const status = dokumen.getElementById("status");
Menyimpan referensi ini di awal lebih efisien daripada menanyakan DOM berulang kali. Langkah 3: Tambahkan Penggantian Keyboard Untuk pengujian tanpa pengontrol fisik, kami akan memetakan tombol keyboard ke tombol: // ===== // KEYBOARD FALLBACK (untuk pengujian tanpa pengontrol) // =====
const keyMap = { "a": btnA, "b": btnB, "x": btnX, "p": [pause1, pause2] // tombol 'p' mengontrol kedua bilah jeda };
Ini memungkinkan kita menguji UI dengan menekan tombol pada keyboard. Langkah 4: Buat Loop Pembaruan Utama Di sinilah keajaiban terjadi. Fungsi ini berjalan terus menerus dan membaca status gamepad: // ===== // LOOP PEMBARUAN GAMEPAD UTAMA // =====
pembaruan fungsiGamepad() { // Dapatkan semua gamepad yang terhubung const gamepad = navigator.getGamepads(); jika (!gamepads) kembali;
// Gunakan gamepad pertama yang terhubung const gp = gamepad[0];
jika (gp) { // Perbarui status tombol dengan mengganti kelas "aktif". btnA.classList.toggle("aktif", gp.buttons[0].ditekan); btnB.classList.toggle("aktif", gp.buttons[1].ditekan); btnX.classList.toggle("aktif", gp.buttons[2].ditekan);
// Tangani tombol jeda (tombol indeks 9 pada sebagian besar pengontrol) const jedaDitekan = gp.buttons[9].ditekan; pause1.classList.toggle("aktif",pausePressed); pause2.classList.toggle("aktif",pausePressed);
// Buat daftar tombol yang sedang ditekan untuk tampilan status biarkan ditekan = []; gp.buttons.forEach((btn, i) => { jika (btn.ditekan)ditekan.push("Tombol " + i); });
// Perbarui teks status jika ada tombol yang ditekan if (ditekan.panjang > 0) { status.textContent = "Ditekan: " + ditekan.join(", "); } }
// Lanjutkan perulangan jika debugger sedang berjalan jika (berjalan) { rafId = requestAnimationFrame(perbaruiGamepad); } }
Metode classList.toggle() menambah atau menghapus kelas aktif berdasarkan apakah tombol ditekan, yang memicu gaya lapisan CSS kita. Langkah 5: Tangani Acara Keyboard Pemroses acara berikut membuat penggantian keyboard berfungsi: // ===== // PENANGANAN ACARA KEYBOARD // =====
dokumen.addEventListener("keydown", (e) => { if (keyMap[e.key]) { // Menangani satu atau beberapa elemen if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("aktif")); } lain { keyMap[e.key].classList.add("aktif"); } status.textContent = "Tombol ditekan: " + e.key.toUpperCase(); } });
dokumen.addEventListener("keyup", (e) => { if (keyMap[e.key]) { // Hapus status aktif saat kunci dilepaskan if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("aktif")); } lain { keyMap[e.key].classList.remove("aktif"); } status.textContent = "Kunci dilepaskan: " + e.key.toUpperCase(); } });
Langkah 6: Tambahkan Kontrol Mulai/Hentikan Terakhir, kita memerlukan cara untuk mengaktifkan dan menonaktifkan debugger: // ===== // AKTIFKAN/MATIKAN DEBUGGER // =====
document.getElementById("toggle").addEventListener("klik", () => { berlari = !berlari; // Balikkan status berjalan
jika (berjalan) { status.textContent = "Debugger berjalan..."; perbaruiGamepad(); // Mulai perulangan pembaruan } lain { status.textContent = "Debugger tidak aktif"; cancelAnimationFrame(rafId); // Hentikan perulangan } });
Jadi ya, tekan tombol dan itu bersinar. Dorong tongkatnya dan tongkat itu bergerak. Itu saja. Satu hal lagi: nilai mentah. Terkadang Anda hanya ingin melihat angka, bukan lampu.
Pada tahap ini, Anda akan melihat:
Pengontrol sederhana di layar, Tombol yang bereaksi saat Anda berinteraksi dengannya, dan Pembacaan debug opsional menampilkan indeks tombol yang ditekan.
Untuk membuatnya tidak terlalu abstrak, berikut demo singkat pengontrol di layar yang bereaksi secara real-time:
Sekarang, menekan Mulai Merekam akan mencatat semuanya sampai Anda menekan Berhenti Merekam. 2. Mengekspor Data ke CSV/JSON Setelah kami memiliki log, kami ingin menyimpannya.
Langkah 1: Buat Pembantu Unduhan Pertama, kita memerlukan fungsi pembantu yang menangani pengunduhan file di browser: // ===== // PEMBANTU UNDUHAN FILE // =====
fungsi downloadFile(nama file, konten, ketik = "teks/polos") { // Membuat blob dari konten const gumpalan = gumpalan baru([konten], { mengetik }); const url = URL.createObjectURL(gumpalan);
// Buat tautan unduhan sementara dan klik const a = dokumen.createElement("a"); a.href = url; a.download = nama file; a.klik();
// Bersihkan URL objek setelah diunduh setTimeout(() => URL.revokeObjectURL(url), 100); }
Fungsi ini bekerja dengan membuat Blob (objek biner besar) dari data Anda, membuat URL sementara untuk data tersebut, dan mengeklik tautan unduhan secara terprogram. Pembersihan memastikan kami tidak membocorkan memori. Langkah 2: Tangani Ekspor JSON JSON sempurna untuk menjaga struktur data lengkap:
// ===== // EKSPOR SEBAGAI JSON // =====
document.getElementById("export-json").addEventListener("klik", () => { // Periksa apakah ada yang ingin diekspor if (!frames.length) { console.warn("Tidak ada rekaman yang tersedia untuk diekspor."); kembali; }
// Membuat payload dengan metadata dan frame muatan konstan = { dibuatPada: Tanggal baru().toISOString(), bingkai };
// Unduh sebagai JSON yang diformat unduhFile( "gamepad-log.json", JSON.stringify(muatan, nol, 2), "aplikasi/json" ); });
Format JSON menjaga semuanya tetap terstruktur dan mudah diurai, sehingga ideal untuk dimuat kembali ke alat pengembang atau dibagikan dengan rekan satu tim. Langkah 3: Tangani Ekspor CSV Untuk ekspor CSV, kita perlu meratakan data hierarki menjadi baris dan kolom:
//==================== // EKSPOR SEBAGAI CSV // =====
document.getElementById("export-csv").addEventListener("klik", () => { // Periksa apakah ada yang ingin diekspor if (!frames.length) { console.warn("Tidak ada rekaman yang tersedia untuk diekspor."); kembali; }
// Buat baris header CSV (kolom untuk stempel waktu, semua tombol, semua sumbu) const headerButtons = frame[0].buttons.map((_, i) => btn${i}); const headerAxes = frame[0].axes.map((_, i) => axis${i}); const header = ["t", ...headerButtons, ...headerAxes].join(",") + "\n";
// Membangun baris data CSV const baris = frame.peta(f => { const btnVals = f.buttons.map(b => b.value); return [f.t, ...btnVals, ...f.axes].join(","); }).join("\n");
// Unduh sebagai CSV downloadFile("gamepad-log.csv", header + baris, "teks/csv"); });
CSV sangat bagus untuk analisis data karena terbuka langsung di Excel atau Google Spreadsheet, memungkinkan Anda membuat diagram, memfilter data, atau melihat pola secara visual. Sekarang setelah tombol ekspor aktif, Anda akan melihat dua opsi baru di panel: Ekspor JSON dan Ekspor CSV. JSON bagus jika Anda ingin mengembalikan log mentah ke alat pengembang Anda atau melihat-lihat strukturnya. CSV, sebaliknya, terbuka langsung ke Excel atau Google Spreadsheet sehingga Anda dapat membuat bagan, memfilter, atau membandingkan masukan. Gambar berikut menunjukkan tampilan panel dengan kontrol tambahan tersebut.
3. Sistem Cuplikan Terkadang Anda tidak memerlukan rekaman lengkap, cukup “tangkapan layar” singkat dari status masukan. Di situlah tombol Ambil Snapshot membantu.
Dan JavaScriptnya:
// ===== // AMBIL GAMBAR // =====
document.getElementById("snapshot").addEventListener("klik", () => { // Dapatkan semua gamepad yang terhubung const pad = navigator.getGamepads(); const activePads = [];
// Ulangi dan tangkap status setiap gamepad yang terhubung untuk (const gp bantalan) { jika (!gp) lanjutkan; // Lewati slot kosong
activePads.push({ id: gp.id, // Nama/model pengontrol stempel waktu: performance.now(), tombol: gp.buttons.map(b => ({ ditekan: b.ditekan, nilai: b.nilai })), sumbu: [...gp.axes] }); }
// Periksa apakah ada gamepad yang ditemukan if (!activePads.length) { console.warn("Tidak ada gamepad yang terhubung untuk snapshot."); alert("Tidak ada pengontrol yang terdeteksi!"); kembali; }
// Catat dan beri tahu pengguna console.log("Snapshot:", activePads); alert(Snapshot diambil! Pengontrol ${activePads.length} diambil.); });
Snapshot membekukan keadaan persis pengontrol Anda pada suatu saat. 4. Pemutaran Ulang Masukan Hantu Sekarang yang menyenangkan: pemutaran ulang masukan hantu. Ini mengambil log dan memutarnya kembali secara visual seolah-olah ada pemain hantu yang menggunakan pengontrol.
JavaScript untuk diputar ulang: // ===== // PEMUTARAN ULANG HANTU // =====
document.getElementById("replay").addEventListener("klik", () => { // Pastikan kita mempunyai rekaman untuk diputar ulang if (!frames.length) { alert("Tidak ada rekaman yang perlu diputar ulang!"); kembali; }
console.log("Memulai pemutaran ulang hantu...");
// Lacak waktu untuk pemutaran yang disinkronkan biarkan startTime = performance.now(); biarkan frameIndex = 0;
// Memutar ulang loop animasi langkah fungsi() { const sekarang = kinerja.sekarang(); const elapsed = sekarang - startTime;
// Memproses semua frame yang seharusnya sudah muncul saat ini while (frameIndex < frame.length && frame[frameIndex].t <= berlalu) { const bingkai = bingkai[frameIndex];
// Perbarui UI dengan status tombol yang direkam btnA.classList.toggle("aktif", frame.buttons[0].ditekan); btnB.classList.toggle("aktif", frame.buttons[1].ditekan); btnX.classList.toggle("aktif", frame.buttons[2].ditekan);
// Perbarui tampilan status biarkan ditekan = []; frame.buttons.forEach((btn, i) => { if (btn.pressed) pressed.push("Tombol " + i); }); if (ditekan.panjang > 0) { status.textContent = "Hantu: " + ditekan.join(", "); }
bingkaiIndeks++; }
// Lanjutkan perulangan jika masih ada frame lagi if (frameIndex < frame.panjang) { requestAnimationFrame(langkah); } lain { console.log("Putar ulangselesai."); status.textContent = "Pemutaran ulang selesai"; } }
// Mulai pemutaran ulang langkah(); });
Untuk membuat proses debug lebih praktis, saya menambahkan tayangan ulang hantu. Setelah Anda merekam suatu sesi, Anda dapat menekan replay dan melihat UI memerankannya, hampir seperti pemain hantu sedang menjalankan pad. Tombol Replay Ghost baru muncul di panel untuk ini.
Tekan Rekam, main-main dengan pengontrol sedikit, hentikan, lalu putar ulang. UI hanya menggemakan semua yang Anda lakukan, seperti hantu yang mengikuti masukan Anda. Mengapa repot-repot dengan tambahan ini?
Perekaman/ekspor memudahkan penguji untuk menunjukkan apa yang sebenarnya terjadi. Snapshot berhenti sejenak, sangat berguna saat Anda mengejar bug aneh. Pemutaran ulang hantu sangat bagus untuk tutorial, pemeriksaan aksesibilitas, atau sekadar membandingkan pengaturan kontrol secara berdampingan.
Pada titik ini, ini bukan lagi sekedar demo yang bagus, tetapi sesuatu yang benar-benar dapat Anda terapkan. Kasus Penggunaan di Dunia Nyata Sekarang kami memiliki debugger yang dapat melakukan banyak hal. Ini menunjukkan input langsung, mencatat log, mengekspornya, dan bahkan memutar ulang sesuatu. Namun pertanyaan sebenarnya adalah: siapa sebenarnya yang peduli? Untuk siapa ini berguna? Pengembang Game Pengontrol adalah bagian dari pekerjaan, tetapi men-debugnya? Biasanya menyakitkan. Bayangkan Anda sedang menguji kombo game pertarungan, seperti ↓ → + pukulan. Daripada berdoa, Anda menekannya dengan cara yang sama dua kali, Anda merekamnya sekali, dan memutarnya kembali. Selesai. Atau Anda menukar log JSON dengan rekan satu tim untuk memeriksa apakah kode multipemain Anda bereaksi sama di mesin mereka. Itu sangat besar. Praktisi Aksesibilitas Yang ini dekat dengan hatiku. Tidak semua orang bermain dengan pengontrol “standar”. Pengontrol adaptif terkadang mengeluarkan sinyal aneh. Dengan alat ini, Anda dapat melihat dengan tepat apa yang terjadi. Guru, peneliti, siapapun. Mereka dapat mengambil log, membandingkannya, atau memutar ulang input secara berdampingan. Tiba-tiba, hal-hal yang tidak terlihat menjadi jelas. Pengujian Jaminan Mutu Penguji biasanya menulis catatan seperti "Saya menekan tombol di sini dan tombolnya rusak". Tidak terlalu membantu. Sekarang? Mereka dapat menangkap hasil cetakan yang tepat, mengekspor log, dan mengirimkannya. Jangan menebak-nebak. Pendidik Jika Anda membuat tutorial atau video YouTube, pemutaran ulang hantu adalah solusi terbaiknya. Anda benar-benar dapat mengatakan, “Inilah yang saya lakukan dengan pengontrolnya,” sementara UI menunjukkan hal itu terjadi. Membuat penjelasan menjadi lebih jelas. Melampaui Permainan Dan ya, ini bukan hanya tentang permainan. Orang-orang telah menggunakan pengontrol untuk robot, proyek seni, dan antarmuka aksesibilitas. Masalah yang sama setiap saat: apa yang sebenarnya dilihat browser? Dengan ini, Anda tidak perlu menebak-nebak. Kesimpulan Men-debug input pengontrol selalu terasa seperti terbang buta. Berbeda dengan DOM atau CSS, tidak ada inspektur bawaan untuk gamepad; itu hanya angka mentah di konsol, mudah hilang dalam kebisingan. Dengan beberapa ratus baris HTML, CSS, dan JavaScript, kami membuat sesuatu yang berbeda:
Debugger visual yang membuat masukan yang tidak terlihat menjadi terlihat. Sistem CSS berlapis yang menjaga UI tetap bersih dan dapat di-debug. Serangkaian penyempurnaan (perekaman, ekspor, snapshot, pemutaran ulang hantu) yang meningkatkannya dari demo ke alat pengembang.
Proyek ini menunjukkan seberapa jauh Anda dapat melangkah dengan menggabungkan kekuatan Platform Web dengan sedikit kreativitas dalam CSS Cascade Layers. Alat yang baru saya jelaskan secara keseluruhan adalah sumber terbuka. Anda dapat mengkloning repo GitHub dan mencobanya sendiri. Namun yang lebih penting, Anda bisa membuatnya sendiri. Tambahkan lapisan Anda sendiri. Bangun logika replay Anda sendiri. Integrasikan dengan prototipe game Anda. Atau bahkan menggunakannya dengan cara yang tidak pernah saya bayangkan. Untuk pengajaran, aksesibilitas, atau analisis data. Pada akhirnya, ini bukan hanya tentang men-debug gamepad. Ini tentang menyoroti masukan yang tersembunyi, dan memberikan kepercayaan diri kepada pengembang untuk bekerja dengan perangkat keras yang masih belum sepenuhnya diterima oleh web. Jadi, colokkan pengontrol Anda, buka editor Anda, dan mulailah bereksperimen. Anda mungkin terkejut dengan apa yang sebenarnya dapat dicapai oleh browser dan CSS Anda.