Cuando conectas un controlador, presionas botones, mueves las palancas, aprietas los gatillos... y como desarrollador, no ves nada de eso. El navegador lo capta, claro, pero a menos que estés registrando números en la consola, es invisible. Ese es el dolor de cabeza con la API de Gamepad. Ha existido durante años y, en realidad, es bastante poderoso. Puedes leer botones, palancas, disparadores, todo. Pero la mayoría de la gente no lo toca. ¿Por qué? Porque no hay retroalimentación. No hay panel en las herramientas de desarrollo. No hay una forma clara de saber si el controlador está haciendo lo que usted piensa. Se siente como volar a ciegas. Eso me molestó lo suficiente como para crear una pequeña herramienta: Gamepad Cascade Debugger. En lugar de mirar la salida de la consola, obtienes una vista interactiva en vivo del controlador. Presiona algo y reacciona en la pantalla. Y con CSS Cascade Layers, los estilos permanecen organizados, por lo que la depuración es más sencilla. En esta publicación, le mostraré por qué depurar controladores es tan complicado, cómo CSS ayuda a limpiarlo y cómo puede crear un depurador visual reutilizable para sus propios proyectos.
Incluso si puede registrarlos todos, rápidamente terminará con spam de consola ilegible. Por ejemplo: [0,0,1,0,0,0.5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]
¿Puedes decir qué botón se presionó? Tal vez, pero sólo después de forzar la vista y omitir algunas entradas. Entonces, no, la depuración no es fácil cuando se trata de leer entradas. Problema 3: falta de estructura Incluso si creas un visualizador rápido, los estilos pueden volverse confusos rápidamente. Los estados predeterminado, activo y de depuración pueden superponerse y, sin una estructura clara, su CSS se vuelve frágil y difícil de ampliar. Las capas en cascada CSS pueden ayudar. Agrupan estilos en "capas" que están ordenadas por prioridad, por lo que deja de luchar contra la especificidad y de adivinar: "¿Por qué no se muestra mi estilo de depuración?" En cambio, mantiene preocupaciones separadas:
Base: La apariencia inicial estándar del controlador. Activo: resaltados de botones presionados y palancas movidas. Depuración: superposiciones para desarrolladores (por ejemplo, lecturas numéricas, guías, etc.).
Si tuviéramos que definir capas en CSS de acuerdo con esto, tendríamos: /* prioridad más baja a más alta */ @layer base, activo, depurar;
@capa base { /* ... */ }
@capa activa { /* ... */ }
@capa de depuración { /* ... */ }
Como cada capa se acumula de forma predecible, siempre sabrás qué reglas ganan. Esa previsibilidad hace que la depuración no sólo sea más fácil, sino también manejable. Hemos cubierto el problema (entrada invisible y desordenada) y el enfoque (un depurador visual creado con Cascade Layers). Ahora veremos el proceso paso a paso para crear el depurador. El concepto de depurador La forma más sencilla de hacer visible la entrada oculta es simplemente dibujarla en la pantalla. Eso es lo que hace este depurador. Los botones, disparadores y joysticks se muestran visualmente.
Presione A: se ilumina un círculo. Empuja el palo: el círculo se desliza. Aprieta un gatillo hasta la mitad: una barra se llena hasta la mitad.
Ahora no estás mirando los 0 y 1, sino que estás viendo cómo el controlador reacciona en vivo. Por supuesto, una vez que comienzas a acumular estados como predeterminado, presionado, información de depuración, tal vez incluso un modo de grabación, el CSS comienza a volverse más grande y complejo. Ahí es donde las capas en cascada resultan útiles. Aquí hay un ejemplo simplificado: @capa base { .botón { antecedentes: #222; radio fronterizo: 50%; ancho: 40px; altura: 40 píxeles; } }
@capa activa { .botón.presionado { fondo: #0f0; /*verde brillante*/ } }
@capa de depuración { .botón::después { contenido: attr(valor-datos); tamaño de fuente: 12px; color: #fff; } }
El orden de las capas importa: base → activa → depuración.
La base dibuja el controlador. activo maneja los estados presionados. lanzamientos de depuración en superposiciones.
Dividirlo de esta manera significa que no estás librando extrañas guerras de especificidad. Cada capa tiene su lugar y siempre sabes qué gana. Construyéndolo Primero pongamos algo en la pantalla. No es necesario que se vea bien, solo debe existir para que tengamos algo con qué trabajar.
Depurador en cascada del gamepad
Literalmente son solo cajas. No es emocionante todavía, pero nos brinda herramientas para aprovechar más tarde con CSS y JavaScript. Bien, estoy usando capas en cascada aquí porque mantiene las cosas organizadas una vez que agregas más estados. Aquí hay un pase aproximado:
/* ==================================== CONFIGURACIÓN DE CAPAS EN CASCADA El orden importa: base → activo → depurar ==================================== */
/* Definir el orden de las capas por adelantado */ @layer base, activo, depurar;
/* Capa 1: Estilos base - apariencia predeterminada */ @capa base { .botón { antecedentes: #333; radio fronterizo: 50%; ancho: 70 píxeles; altura: 70 píxeles; pantalla: flexible; justificar-contenido: centro; alinear elementos: centro; }
.pausa { ancho: 20px; altura: 70 píxeles; antecedentes: #333; pantalla: bloque en línea; } }
/* Capa 2: Estados activos - maneja los botones presionados */ @capa activa { .botón.activo { fondo: #0f0; /* Verde brillante cuando se presiona */ transformar: escala (1.1); /* Agranda ligeramente el botón */ }
.pausa.activa { fondo: #0f0; transformar: escalaY(1.1); /* Se estira verticalmente cuando se presiona */ } }
/* Capa 3: superposiciones de depuración - información del desarrollador */ @capa de depuración { .botón::después { contenido: attr(valor-datos); /* Muestra el valor numérico */ tamaño de fuente: 12px; color: #fff; } }
Lo bueno de este enfoque es que cada capa tiene un propósito claro. La capa base nunca puede anular la activa y la activa nunca puede anular la depuración, independientemente de la especificidad. Esto elimina las guerras de especificidad de CSS que suelen afectar a las herramientas de depuración. Ahora parece que algunos grupos están asentados sobre un fondo oscuro. Sinceramente, no está tan mal.
Agregar el JavaScript Tiempo de JavaScript. Aquí es donde el controlador realmente hace algo. Construiremos esto paso a paso. Paso 1: configurar la gestión de estado Primero, necesitamos variables para rastrear el estado del depurador: // ===================================== // GESTIÓN DEL ESTADO // =====================================
dejar correr = falso; // Realiza un seguimiento si el depurador está activo dejar rafid; // Almacena el ID de requestAnimationFrame para cancelación
Estas variables controlan el bucle de animación que lee continuamente la entrada del gamepad. Paso 2: obtenga referencias DOM A continuación, obtenemos referencias a todos los elementos HTML que actualizaremos: // ===================================== // REFERENCIAS DE ELEMENTOS DOM // =====================================
const btnA = document.getElementById("btn-a"); const btnB = document.getElementById("btn-b"); const btnX = document.getElementById("btn-x"); const pausa1 = document.getElementById("pausa1"); const pausa2 = document.getElementById("pausa2"); estado constante = document.getElementById("estado");
Almacenar estas referencias por adelantado es más eficiente que consultar el DOM repetidamente. Paso 3: agregar respaldo de teclado Para realizar pruebas sin un controlador físico, asignaremos las teclas del teclado a los botones: // ===================================== // RESPALDO DEL TECLADO (para pruebas sin controlador) // =====================================
mapa de claves constante = { "a": btnA, "b": btnB, "x": btnX, "p": [pausa1, pausa2] // La tecla 'p' controla ambas barras de pausa };
Esto nos permite probar la interfaz de usuario presionando teclas en un teclado. Paso 4: cree el bucle de actualización principal Aquí es donde ocurre la magia. Esta función se ejecuta continuamente y lee el estado del gamepad: // ===================================== // BUCLE DE ACTUALIZACIÓN PRINCIPAL DEL GAMEPAD // =====================================
función actualizarGamepad() { // Obtener todos los gamepads conectados const gamepads = navigator.getGamepads(); si (! gamepads) regresa;
// Usa el primer gamepad conectado const gp = mandos[0];
si (po) { // Actualizar los estados del botón alternando la clase "activa" btnA.classList.toggle("activo", gp.buttons[0].presionado); btnB.classList.toggle("activo", gp.buttons[1].presionado); btnX.classList.toggle("activo", gp.buttons[2].presionado);
// Manejar el botón de pausa (índice de botón 9 en la mayoría de los controladores) const pausaPresionado = gp.botones[9].presionado; pausa1.classList.toggle("activo", pausaPressed); pausa2.classList.toggle("activo", pausaPressed);
// Crea una lista de los botones presionados actualmente para mostrar el estado dejar presionado = []; gp.botones.forEach((btn, i) => { si (btn.presionado)presionado.push("Botón " + i); });
// Actualiza el texto de estado si se presiona algún botón if (presionado.longitud > 0) { status.textContent = "Presionado: " + presionado.join(", "); } }
// Continuar el ciclo si el depurador se está ejecutando si (corriendo) { rafId = requestAnimationFrame(updateGamepad); } }
El método classList.toggle() agrega o elimina la clase activa en función de si se presiona el botón, lo que activa nuestros estilos de capa CSS. Paso 5: Manejar los eventos del teclado Estos detectores de eventos hacen que funcione el respaldo del teclado: // ===================================== // MANEJADORES DE EVENTOS DE TECLADO // =====================================
document.addEventListener("keydown", (e) => { si (keyMap[e.key]) { // Manejar elementos únicos o múltiples if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("active")); } más { keyMap[e.key].classList.add("activo"); } status.textContent = "Tecla presionada: " + e.key.toUpperCase(); } });
document.addEventListener("keyup", (e) => { si (keyMap[e.key]) { // Elimina el estado activo cuando se suelta la tecla if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("active")); } más { keyMap[e.key].classList.remove("activo"); } status.textContent = "Clave liberada: " + e.key.toUpperCase(); } });
Paso 6: Agregar control de inicio/parada Finalmente, necesitamos una forma de activar y desactivar el depurador: // ===================================== // ACTIVAR/DESACTIVAR EL DEPURADOR // =====================================
document.getElementById("alternar").addEventListener("hacer clic", () => { corriendo = !corriendo; // Voltear el estado de ejecución
si (corriendo) { status.textContent = "Depurador en ejecución..."; actualizarGamepad(); // Inicia el ciclo de actualización } más { status.textContent = "Depurador inactivo"; cancelarAnimationFrame(rafId); // Detener el ciclo } });
Entonces sí, presiona un botón y se ilumina. Empuja el palo y se mueve. Eso es todo. Una cosa más: valores brutos. A veces sólo quieres ver números, no luces.
En esta etapa, deberías ver:
Un simple controlador en pantalla, Botones que reaccionan cuando interactúas con ellos, y Una lectura de depuración opcional que muestra los índices de los botones presionados.
Para hacer esto menos abstracto, aquí hay una demostración rápida del controlador en pantalla reaccionando en tiempo real:
Ahora, al presionar Iniciar grabación se registra todo hasta que presiona Detener grabación. 2. Exportación de datos a CSV/JSON Una vez que tengamos un registro, querremos guardarlo.
Paso 1: cree el asistente de descarga Primero, necesitamos una función auxiliar que maneje las descargas de archivos en el navegador: // ===================================== // AYUDANTE DE DESCARGA DE ARCHIVOS // =====================================
función descargarArchivo(nombre de archivo, contenido, tipo = "texto/sin formato") { // Crea un blob a partir del contenido const blob = nuevo Blob([contenido], {tipo}); URL constante = URL.createObjectURL(blob);
// Crea un enlace de descarga temporal y haz clic en él. const a = document.createElement("a"); a.href = URL; a.descargar = nombre de archivo; a.hacer clic();
// Limpiar la URL del objeto después de la descarga setTimeout(() => URL.revokeObjectURL(url), 100); }
Esta función funciona creando un Blob (objeto binario grande) a partir de sus datos, generando una URL temporal para él y haciendo clic en un enlace de descarga mediante programación. La limpieza garantiza que no perdamos memoria. Paso 2: Manejar la exportación JSON JSON es perfecto para preservar la estructura de datos completa:
// ===================================== // EXPORTAR COMO JSON // =====================================
document.getElementById("exportar-json").addEventListener("hacer clic", () => { //Comprueba si hay algo para exportar si (!cuadros.longitud) { console.warn("No hay ninguna grabación disponible para exportar."); regresar; }
// Crea una carga útil con metadatos y marcos carga útil constante = { creado en: nueva fecha().toISOString(), marcos };
// Descargar como JSON formateado descargar archivo ( "gamepad-log.json", JSON.stringify(carga útil, nulo, 2), "aplicación/json" ); });
El formato JSON mantiene todo estructurado y fácilmente analizable, lo que lo hace ideal para volver a cargarlo en herramientas de desarrollo o compartirlo con compañeros de equipo. Paso 3: Manejar la exportación CSV Para las exportaciones CSV, necesitamos aplanar los datos jerárquicos en filas y columnas:
//===================================== // EXPORTAR COMO CSV // =====================================
document.getElementById("exportar-csv").addEventListener("hacer clic", () => { //Comprueba si hay algo para exportar si (!cuadros.longitud) { console.warn("No hay ninguna grabación disponible para exportar."); regresar; }
// Crea una fila de encabezado CSV (columnas para la marca de tiempo, todos los botones, todos los ejes) const headerButtons = frames[0].buttons.map((_, i) => btn${i}); const headerAxes = frames[0].axes.map((_, i) => eje${i}); encabezado const = ["t", ...headerButtons, ...headerAxes].join(",") + "\n";
// Construir filas de datos CSV filas constantes = frames.map(f => { const btnVals = f.buttons.map(b => b.valor); return [f.t, ...btnVals, ...f.axes].join(","); }).join("\n");
// Descargar como CSV descargarFile("gamepad-log.csv", encabezado + filas, "texto/csv"); });
CSV es excelente para el análisis de datos porque se abre directamente en Excel o Google Sheets, lo que le permite crear gráficos, filtrar datos o detectar patrones visualmente. Ahora que los botones de exportación están activados, verá dos nuevas opciones en el panel: Exportar JSON y Exportar CSV. JSON es bueno si desea devolver el registro sin formato a sus herramientas de desarrollo o explorar la estructura. CSV, por otro lado, se abre directamente en Excel o Google Sheets para que puedas trazar, filtrar o comparar entradas. La siguiente figura muestra cómo se ve el panel con esos controles adicionales.
3. Sistema de instantáneas A veces no necesitas una grabación completa, solo una “captura de pantalla” rápida de los estados de entrada. Ahí es donde ayuda el botón Tomar instantánea.
Y el JavaScript:
// ===================================== // TOMAR INSTANTÁNEA // =====================================
document.getElementById("instantánea").addEventListener("hacer clic", () => { // Obtener todos los gamepads conectados almohadillas constantes = navigator.getGamepads(); const almohadillas activas = [];
// Recorre y captura el estado de cada gamepad conectado para (const gp de pads) { si (!gp) continúa; // Saltar espacios vacíos
activePads.push({ id: gp.id, // nombre/modelo del controlador marca de tiempo: performance.now(), botones: gp.buttons.map(b => ({ presionado: b.presionado, valor: b.valor })), ejes: [...gp.ejes] }); }
// Comprueba si se encontró algún gamepad si (!activePads.length) { console.warn("No hay gamepads conectados para la instantánea."); alert("¡No se detectó ningún controlador!"); regresar; }
// Iniciar sesión y notificar al usuario console.log("Instantánea:", activePads); alert(¡Instantánea tomada! Controlador(es) ${activePads.length} capturados). });
Las instantáneas congelan el estado exacto de su controlador en un momento dado. 4. Repetición de entrada fantasma Ahora viene lo divertido: la repetición de la entrada fantasma. Esto toma un registro y lo reproduce visualmente como si un jugador fantasma estuviera usando el controlador.
JavaScript para reproducción: // ===================================== // REPETICIÓN FANTASMA // =====================================
document.getElementById("reproducir").addEventListener("hacer clic", () => { // Asegurarnos de que tenemos una grabación para reproducir si (!cuadros.longitud) { alert("¡No hay grabación para reproducir!"); regresar; }
console.log("Iniciando reproducción fantasma...");
// Seguimiento del tiempo para reproducción sincronizada let startTime = rendimiento.now(); deje frameIndex = 0;
// Reproducir bucle de animación paso de función() { constante ahora = rendimiento.now(); const transcurrido = ahora - hora de inicio;
// Procesa todos los fotogramas que ya deberían haber ocurrido while (frameIndex < frames.length && frames[frameIndex].t <= transcurrido) { marco constante = marcos[frameIndex];
// Actualiza la interfaz de usuario con los estados de los botones grabados btnA.classList.toggle("activo", frame.buttons[0].presionado); btnB.classList.toggle("activo", frame.buttons[1].presionado); btnX.classList.toggle("activo", frame.buttons[2].presionado);
//Actualizar visualización de estado dejar presionado = []; marco.botones.forEach((btn, i) => { si (btn.presionado) presionado.push("Botón " + i); }); if (presionado.longitud > 0) { status.textContent = "Fantasma: " + presionado.join(", "); }
índice de marco++; }
//Continúa el ciclo si hay más fotogramas
if (frameIndex //Inicia la repetición
paso();
}); Para que la depuración sea un poco más práctica, agregué una repetición fantasma. Una vez que haya grabado una sesión, puede presionar Reproducir y ver cómo la interfaz de usuario la representa, casi como si un reproductor fantasma estuviera ejecutando el pad. Para esto, aparece un nuevo botón Replay Ghost en el panel. Presiona Grabar, juega un poco con el controlador, detente y luego vuelve a reproducir. La interfaz de usuario simplemente refleja todo lo que hiciste, como un fantasma siguiendo tus entradas.
¿Por qué molestarse con estos extras? La grabación/exportación facilita que los evaluadores muestren exactamente lo que sucedió.
Las instantáneas congelan un momento en el tiempo, lo que es muy útil cuando buscas errores extraños.
La repetición de Ghost es ideal para tutoriales, comprobaciones de accesibilidad o simplemente para comparar configuraciones de control una al lado de la otra. En este punto, ya no es sólo una demostración interesante, sino algo que realmente puedes poner en práctica.
Casos de uso del mundo real
Ahora tenemos este depurador que puede hacer mucho. Muestra entradas en vivo, registra registros, los exporta e incluso reproduce cosas. Pero la verdadera pregunta es: ¿a quién le importa realmente? ¿Para quién es esto útil?
Desarrolladores de juegos
Los controladores son parte del trabajo, pero ¿depurarlos? Generalmente es un dolor. Imagina que estás probando una combinación de un juego de lucha, como ↓ → + puñetazo. En lugar de orar, lo presionas de la misma manera dos veces, lo grabas una vez y lo reproduces. Hecho. O intercambias registros JSON con un compañero de equipo para comprobar si tu código multijugador reacciona igual en su máquina. Eso es enorme.
Profesionales de la accesibilidad
Este está cerca de mi corazón. No todo el mundo juega con un mando "estándar". Los controladores adaptativos a veces emiten señales extrañas. Con esta herramienta, puedes ver exactamente lo que está sucediendo. Profesores, investigadores, quien sea. Pueden tomar registros, compararlos o reproducir entradas una al lado de la otra. De repente, las cosas invisibles se vuelven obvias.
Pruebas de garantía de calidad
Los evaluadores suelen escribir notas como "Aplasté botones aquí y se rompió". No es muy útil. ¿Ahora? Pueden capturar las prensas exactas, exportar el registro y enviarlo. Sin adivinanzas.
Educadores
Si estás haciendo tutoriales o vídeos de YouTube, la repetición fantasma es oro. Literalmente puedes decir: "Esto es lo que hice con el controlador", mientras la interfaz de usuario muestra lo que sucede. Hace que las explicaciones sean mucho más claras.
Más allá de los juegos
Y sí, no se trata sólo de juegos. La gente ha utilizado controladores para robots, proyectos de arte e interfaces de accesibilidad. El mismo problema siempre: ¿qué ve realmente el navegador? Con esto, no tienes que adivinar.
Conclusión
Depurar una entrada de controlador siempre ha sido como volar a ciegas. A diferencia de DOM o CSS, no hay un inspector integrado para gamepads; son solo números sin procesar en la consola, que se pierden fácilmente en el ruido.
Con unos cientos de líneas de HTML, CSS y JavaScript, creamos algo diferente: Un depurador visual que hace visibles las entradas invisibles.
Un sistema CSS en capas que mantiene la interfaz de usuario limpia y depurable.
Un conjunto de mejoras (grabación, exportación, instantáneas, reproducción fantasma) que lo elevan de demostración a herramienta para desarrolladores. Este proyecto muestra hasta dónde se puede llegar combinando el poder de la plataforma web con un poco de creatividad en CSS Cascade Layers.
La herramienta que acabo de explicar en su totalidad es de código abierto. Puedes clonar el repositorio de GitHub y probarlo tú mismo.
Pero lo más importante es que puedes hacerlo tuyo. Añade tus propias capas. Construye tu propia lógica de repetición. Intégralo con tu prototipo de juego. O incluso usarlo de formas que no había imaginado. Para enseñanza, accesibilidad o análisis de datos.
Al fin y al cabo, no se trata sólo de depurar mandos. Se trata de arrojar luz sobre las entradas ocultas y brindar a los desarrolladores la confianza para trabajar con hardware que la web aún no adopta por completo.
Entonces, conecta tu controlador, abre tu editor y comienza a experimentar. Es posible que se sorprenda de lo que realmente pueden lograr su navegador y su CSS.