Quando você conecta um controlador, você aperta botões, move os manípulos, puxa os gatilhos… e como desenvolvedor, você não vê nada disso. O navegador está captando, claro, mas a menos que você registre números no console, ele fica invisível. Essa é a dor de cabeça da API Gamepad. Já existe há anos e é realmente muito poderoso. Você pode ler botões, manípulos, gatilhos, o que funciona. Mas a maioria das pessoas não toca nisso. Por que? Porque não há feedback. Nenhum painel nas ferramentas do desenvolvedor. Não há uma maneira clara de saber se o controlador está fazendo o que você pensa. É como voar às cegas. Isso me incomodou o suficiente para construir uma pequena ferramenta: Gamepad Cascade Debugger. Em vez de olhar para a saída do console, você obtém uma visão interativa e ao vivo do controlador. Pressione algo e ele reage na tela. E com CSS Cascade Layers, os estilos permanecem organizados, por isso é mais fácil depurar. Neste post, vou mostrar por que depurar controladores é tão chato, como o CSS ajuda a limpá-lo e como você pode construir um depurador visual reutilizável para seus próprios projetos.

Mesmo que você consiga registrar todos eles, você rapidamente acabará com spam ilegível no console. Por exemplo: [0,0,1,0,0,0,5,0,...] [0,0,0,0,1,0,0,...] [0,0,1,0,0,0,0,...]

Você pode dizer qual botão foi pressionado? Talvez, mas só depois de forçar os olhos e perder algumas entradas. Então, não, a depuração não é fácil quando se trata de ler entradas. Problema 3: Falta de estrutura Mesmo se você criar um visualizador rápido, os estilos podem ficar confusos rapidamente. Os estados padrão, ativo e de depuração podem se sobrepor e, sem uma estrutura clara, seu CSS se torna frágil e difícil de estender. Camadas CSS Cascade podem ajudar. Eles agrupam estilos em “camadas” ordenadas por prioridade, para que você pare de lutar contra a especificidade e de adivinhar: “Por que meu estilo de depuração não está aparecendo?” Em vez disso, você mantém preocupações separadas:

Base: A aparência inicial padrão do controlador. Ativo: Destaques para botões pressionados e sticks movidos. Depuração: sobreposições para desenvolvedores (por exemplo, leituras numéricas, guias e assim por diante).

Se definissemos camadas em CSS de acordo com isso, teríamos: /* prioridade da menor para a maior */ @camada base, ativa, depuração;

@camada base { /* ... */ }

@camada ativa { /* ... */ }

@depuração da camada { /* ... */ }

Como cada camada se acumula de maneira previsível, você sempre sabe quais regras vencem. Essa previsibilidade torna a depuração não apenas mais fácil, mas também gerenciável. Abordamos o problema (entrada invisível e confusa) e a abordagem (um depurador visual construído com Cascade Layers). Agora percorreremos o processo passo a passo para construir o depurador. O conceito do depurador A maneira mais fácil de tornar visível a entrada oculta é simplesmente desenhá-la na tela. Isso é o que este depurador faz. Botões, gatilhos e joysticks recebem um visual.

Pressione A: Um círculo acende. Empurre o bastão: o círculo desliza. Puxe um gatilho até a metade: uma barra enche até a metade.

Agora você não está olhando para 0s e 1s, mas na verdade observando a reação do controlador ao vivo. Claro, uma vez que você começa a acumular estados como padrão, pressionado, informações de depuração, talvez até mesmo um modo de gravação, o CSS começa a ficar maior e mais complexo. É aí que as camadas em cascata são úteis. Aqui está um exemplo simplificado: @camada base { .botão { histórico: #222; raio da fronteira: 50%; largura: 40px; altura: 40px; } }

@camada ativa { .botão.pressionado { plano de fundo: #0f0; /* verde brilhante */ } }

@depuração da camada { .button::depois { conteúdo: attr(valor dos dados); tamanho da fonte: 12px; cor: #fff; } }

A ordem das camadas é importante: base → ativa → depuração.

base desenha o controlador. active lida com estados pressionados. depuração lançada em sobreposições.

Dividir assim significa que você não está travando estranhas guerras de especificidade. Cada camada tem seu lugar e você sempre sabe o que ganha. Construindo Vamos colocar algo na tela primeiro. Não precisa ter uma boa aparência – só precisa existir para que tenhamos algo com que trabalhar.

Depurador em cascata do gamepad

A
B
X

Depurador inativo

Isso é literalmente apenas caixas. Ainda não é empolgante, mas nos dá recursos para usar mais tarde com CSS e JavaScript. Ok, estou usando camadas em cascata aqui porque elas mantêm as coisas organizadas quando você adiciona mais estados. Aqui está um passo difícil:

/* =================================== CONFIGURAÇÃO DE CAMADAS EM CASCATA O pedido é importante: base → ativo → depuração ================================== */

/* Define a ordem das camadas antecipadamente */ @camada base, ativa, depuração;

/* Camada 1: Estilos base - aparência padrão */ @camada base { .botão { histórico: #333; raio da fronteira: 50%; largura: 70px; altura: 70px; exibição: flexível; justificar-conteúdo: centro; alinhar itens: centro; }

.pausa { largura: 20px; altura: 70px; histórico: #333; display: bloco embutido; } }

/* Camada 2: Estados ativos - lida com botões pressionados */ @camada ativa { .botão.ativo { plano de fundo: #0f0; /* Verde brilhante quando pressionado */ transformar: escala(1,1); /* Aumenta ligeiramente o botão */ }

.pausa.ativo { plano de fundo: #0f0; transformar: escalaY(1,1); /* Estica verticalmente quando pressionado */ } }

/* Camada 3: sobreposições de depuração - informações do desenvolvedor */ @depuração da camada { .button::depois { conteúdo: attr(valor dos dados); /* Mostra o valor numérico */ tamanho da fonte: 12px; cor: #fff; } }

A beleza dessa abordagem é que cada camada tem um propósito claro. A camada base nunca pode substituir a ativa e a ativa nunca pode substituir a depuração, independentemente da especificidade. Isso elimina as guerras de especificidade CSS que geralmente afetam as ferramentas de depuração. Agora parece que alguns clusters estão sobre um fundo escuro. Honestamente, não é tão ruim.

Adicionando o JavaScript Hora do JavaScript. É aqui que o controlador realmente faz alguma coisa. Construiremos isso passo a passo. Etapa 1: configurar o gerenciamento de estado Primeiro, precisamos de variáveis para rastrear o estado do depurador: // =================================== // GESTÃO DO ESTADO // ===================================

deixe correr = falso; // Rastreia se o depurador está ativo deixe RafId; // Armazena o ID requestAnimationFrame para cancelamento

Essas variáveis controlam o loop de animação que lê continuamente a entrada do gamepad. Etapa 2: pegue as referências do DOM A seguir, obtemos referências a todos os elementos HTML que iremos atualizar: // =================================== // REFERÊNCIAS DE ELEMENTOS 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("status");

Armazenar essas referências antecipadamente é mais eficiente do que consultar o DOM repetidamente. Etapa 3: adicionar substituto de teclado Para testes sem um controlador físico, mapearemos as teclas do teclado para os botões: // =================================== // KEYBOARD FALLBACK (para testes sem controlador) // ===================================

const keyMap = { "a": btnA, "b": btnB, "x": btnX, "p": [pause1, pause2] // A tecla 'p' controla ambas as barras de pausa };

Isso nos permite testar a IU pressionando teclas em um teclado. Etapa 4: crie o loop de atualização principal É aqui que a mágica acontece. Esta função é executada continuamente e lê o estado do gamepad: // =================================== // LOOP DE ATUALIZAÇÃO DO GAMEPAD PRINCIPAL // ===================================

função atualizarGamepad() { // Obtém todos os gamepads conectados const gamepads=navigator.getGamepads(); if (!gamepads) retornar;

//Use o primeiro gamepad conectado const gp = gamepads[0];

se (gp) { //Atualiza os estados dos botões alternando a classe "ativa" btnA.classList.toggle("ativo", gp.buttons[0].pressionado); btnB.classList.toggle("ativo", gp.buttons[1].pressionado); btnX.classList.toggle("ativo", gp.buttons[2].pressionado);

// Manipula o botão de pausa (índice do botão 9 na maioria dos controladores) const pausePressed = gp.buttons[9].pressed; pause1.classList.toggle("ativo", pausePressed); pause2.classList.toggle("ativo", pausePressed);

//Constrói uma lista de botões atualmente pressionados para exibição de status deixe pressionado = []; gp.buttons.forEach((btn, i) => { if (btn.pressionado)pressionado.push("Botão " + i); });

//Atualiza o texto do status se algum botão for pressionado if (pressionado.comprimento > 0) { status.textContent = "Pressionado: " + pressionado.join(", "); } }

//Continua o loop se o depurador estiver em execução if (em execução) { rafId = requestAnimationFrame(atualizaçãoGamepad); } }

O método classList.toggle() adiciona ou remove a classe ativa com base no fato de o botão ser pressionado, o que aciona nossos estilos de camada CSS. Etapa 5: lidar com eventos de teclado Esses ouvintes de eventos fazem o substituto do teclado funcionar: // =================================== // MANIPULADORES DE EVENTOS DE TECLADO // ===================================

document.addEventListener("keydown", (e) => { if (keyMap[e.key]) { // Lida com elementos únicos ou múltiplos if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.add("ativo")); } senão { keyMap[e.key].classList.add("ativo"); } status.textContent = "Tecla pressionada: " + e.key.toUpperCase(); } });

document.addEventListener("keyup", (e) => { if (keyMap[e.key]) { // Remove o estado ativo quando a chave é liberada if (Array.isArray(keyMap[e.key])) { keyMap[e.key].forEach(el => el.classList.remove("ativo")); } senão { keyMap[e.key].classList.remove("ativo"); } status.textContent = "Chave liberada: " + e.key.toUpperCase(); } });

Etapa 6: adicionar controle de partida/parada Finalmente, precisamos de uma maneira de ativar e desativar o depurador: // =================================== //Ativa/desativa o depurador // ===================================

document.getElementById("alternar").addEventListener("clique", () => { correndo = !em execução; //Inverte o estado de execução

if (em execução) { status.textContent = "Depurador em execução..."; atualizarGamepad(); // Inicia o loop de atualização } senão { status.textContent = "Depurador inativo"; cancelarAnimationFrame(rafId); //Para o loop } });

Então, sim, pressione um botão e ele brilha. Empurre o stick e ele se moverá. É isso. Mais uma coisa: valores brutos. Às vezes você só quer ver números, não luzes.

Nesta fase, você deverá ver:

Um controlador simples na tela, Botões que reagem conforme você interage com eles e Uma leitura de depuração opcional mostrando índices de botões pressionados.

Para tornar isso menos abstrato, aqui está uma rápida demonstração do controlador na tela reagindo em tempo real:

Agora, pressionar Iniciar Gravação registra tudo até você clicar em Parar Gravação. 2. Exportando dados para CSV/JSON Assim que tivermos um log, queremos salvá-lo.

Etapa 1: crie o auxiliar de download Primeiro, precisamos de uma função auxiliar que lide com downloads de arquivos no navegador: // =================================== // AJUDANTE DE DOWNLOAD DE ARQUIVO // ===================================

function downloadFile(nome do arquivo, conteúdo, tipo = "texto/simples") { // Cria um blob a partir do conteúdo const blob = new Blob([conteúdo], { tipo }); const url = URL.createObjectURL(blob);

//Crie um link de download temporário e clique nele const a = document.createElement("a"); a.href = url; a.download = nome do arquivo; a.clique();

// Limpa a URL do objeto após o download setTimeout(() => URL.revokeObjectURL(url), 100); }

Esta função funciona criando um Blob (objeto binário grande) a partir de seus dados, gerando uma URL temporária para ele e clicando programaticamente em um link de download. A limpeza garante que não vazaremos memória. Etapa 2: lidar com a exportação JSON JSON é perfeito para preservar a estrutura de dados completa:

// =================================== //EXPORTA COMO JSON // ===================================

document.getElementById("export-json").addEventListener("clique", () => { // Verifica se há algo para exportar if (!frames.length) { console.warn("Nenhuma gravação disponível para exportação."); retornar; }

// Cria um payload com metadados e frames carga útil const = { criadoAt: new Date().toISOString(), quadros };

// Baixa como JSON formatado baixarArquivo( "gamepad-log.json", JSON.stringify(carga útil, nulo, 2), "aplicativo/json" ); });

O formato JSON mantém tudo estruturado e facilmente analisável, tornando-o ideal para carregar de volta em ferramentas de desenvolvimento ou compartilhar com colegas de equipe. Etapa 3: lidar com a exportação de CSV Para exportações CSV, precisamos nivelar os dados hierárquicos em linhas e colunas:

//================================== // EXPORTAR COMO CSV // ===================================

document.getElementById("export-csv").addEventListener("clique", () => { // Verifica se há algo para exportar if (!frames.length) { console.warn("Nenhuma gravação disponível para exportação."); retornar; }

//Constrói linha de cabeçalho CSV (colunas para carimbo de data/hora, todos os botões, todos os eixos) const headerButtons = frames[0].buttons.map((_, i) => btn${i}); const headerAxes = frames[0].axes.map((_, i) => eixo${i}); const cabeçalho = ["t", ...headerButtons, ...headerAxes].join(",") + "\n";

//Constrói linhas de dados CSV const linhas = frames.map(f => { const btnVals = f.buttons.map(b => b.valor); return [ft, ...btnVals, ...f.axes].join(","); }).join("\n");

//Baixar como CSV downloadFile("gamepad-log.csv", cabeçalho + linhas, "text/csv"); });

CSV é brilhante para análise de dados porque abre diretamente no Excel ou no Planilhas Google, permitindo criar gráficos, filtrar dados ou identificar padrões visualmente. Agora que os botões de exportação estão ativados, você verá duas novas opções no painel: Exportar JSON e Exportar CSV. JSON é bom se você quiser colocar o log bruto de volta em suas ferramentas de desenvolvimento ou vasculhar a estrutura. O CSV, por outro lado, abre diretamente no Excel ou no Planilhas Google para que você possa criar gráficos, filtrar ou comparar entradas. A figura a seguir mostra a aparência do painel com esses controles extras.

3. Sistema de instantâneo Às vezes você não precisa de uma gravação completa, apenas uma rápida “captura de tela” dos estados de entrada. É aí que o botão Tirar instantâneo ajuda.

E o JavaScript:

// =================================== // TIRAR INSTANTÂNEO // ===================================

document.getElementById("instantâneo").addEventListener("clique", () => { // Obtém todos os gamepads conectados const pads=navigator.getGamepads(); const activePads = [];

// Faz um loop e captura o estado de cada gamepad conectado for (const gp de pads) { se (!gp) continuar; //Ignora slots vazios

activePads.push({ id: gp.id, // Nome/modelo do controlador carimbo de data/hora: desempenho.now(), botões: gp.buttons.map(b => ({ pressionado: b.pressionado, valor: b.valor })), eixos: [...gp.axes] }); }

// Verifica se algum gamepad foi encontrado if (!activePads.length) { console.warn("Nenhum gamepad conectado para snapshot."); alert("Nenhum controlador detectado!"); retornar; }

// Registra e notifica o usuário console.log("Instantâneo:", activePads); alert(Instantâneo tirado! Controlador(es) ${activePads.length} capturado(s).); });

Os instantâneos congelam o estado exato do seu controlador em um determinado momento. 4. Repetição de entrada fantasma Agora, a parte divertida: repetição de entrada fantasma. Isso pega um registro e o reproduz visualmente como se um player fantasma estivesse usando o controlador.

JavaScript para reprodução: // =================================== // REPLAY FANTASMA // ===================================

document.getElementById("replay").addEventListener("clique", () => { // Certifique-se de que temos uma gravação para reproduzir if (!frames.length) { alert("Nenhuma gravação para reproduzir!"); retornar; }

console.log("Iniciando reprodução fantasma...");

// Rastreia o tempo para reprodução sincronizada deixe startTime = performance.now(); deixe frameIndex = 0;

// Repetir loop de animação função etapa() { const agora = desempenho.agora(); const decorrido = agora - startTime;

// Processa todos os frames que já deveriam ter ocorrido while (frameIndex

//Atualiza a UI com os estados dos botões gravados btnA.classList.toggle("ativo", frame.buttons[0].pressionado); btnB.classList.toggle("ativo", frame.buttons[1].pressionado); btnX.classList.toggle("ativo", frame.buttons[2].pressionado);

//Atualiza exibição de status deixe pressionado = []; frame.buttons.forEach((btn, i) => { if (btn.pressionado) pressionado.push("Botão " + i); }); if (pressionado.comprimento > 0) { status.textContent = "Fantasma: " + pressionado.join(", "); }

frameIndex++; }

//Continua o loop se houver mais frames if (frameIndex

// Inicia o replay passo(); });

Para tornar a depuração um pouco mais prática, adicionei um replay fantasma. Depois de gravar uma sessão, você pode clicar em reproduzir e observar a interface do usuário agir, quase como se um player fantasma estivesse executando o pad. Um novo botão Replay Ghost aparece no painel para isso.

Clique em Gravar, mexa um pouco no controlador, pare e reproduza novamente. A IU apenas ecoa tudo o que você fez, como um fantasma seguindo suas entradas. Por que se preocupar com esses extras?

A gravação/exportação torna mais fácil para os testadores mostrarem exatamente o que aconteceu. Os instantâneos congelam um momento no tempo, o que é muito útil quando você está perseguindo bugs estranhos. O Ghost Replay é ótimo para tutoriais, verificações de acessibilidade ou apenas para comparar configurações de controle lado a lado.

Neste ponto, não é mais apenas uma demonstração bacana, mas algo que você pode realmente colocar em prática. Casos de uso do mundo real Agora temos este depurador que pode fazer muito. Ele mostra entradas ao vivo, registra logs, exporta-os e até reproduz coisas. Mas a verdadeira questão é: quem realmente se importa? Para quem isso é útil? Desenvolvedores de jogos Os controladores fazem parte do trabalho, mas depurá-los? Geralmente uma dor. Imagine que você está testando um combo de jogo de luta, como ↓ → + soco. Em vez de orar, você pressiona da mesma forma duas vezes, grava uma vez e reproduz. Feito. Ou você troca logs JSON com um colega de equipe para verificar se o seu código multijogador reage da mesma forma na máquina dele. Isso é enorme. Profissionais de acessibilidade Este está perto do meu coração. Nem todo mundo joga com um controlador “padrão”. Às vezes, os controladores adaptativos emitem sinais estranhos. Com esta ferramenta, você pode ver exatamente o que está acontecendo. Professores, pesquisadores, quem quer que seja. Eles podem obter registros, compará-los ou reproduzir entradas lado a lado. De repente, coisas invisíveis se tornam óbvias. Testes de garantia de qualidade Os testadores geralmente escrevem notas como “Amassei botões aqui e ele quebrou”. Não é muito útil. Agora? Eles podem capturar as prensas exatas, exportar o registro e enviá-lo. Sem adivinhação. Educadores Se você estiver fazendo tutoriais ou vídeos no YouTube, o replay fantasma vale ouro. Você pode literalmente dizer: “Aqui está o que fiz com o controlador”, enquanto a IU mostra isso acontecendo. Torna as explicações muito mais claras. Além dos jogos E sim, não se trata apenas de jogos. As pessoas têm usado controladores para robôs, projetos de arte e interfaces de acessibilidade. Sempre o mesmo problema: o que o navegador está realmente vendo? Com isso, você não precisa adivinhar. Conclusão Depurar uma entrada de controlador sempre foi como voar às cegas. Ao contrário do DOM ou CSS, não há inspetor integrado para gamepads; são apenas números brutos no console, facilmente perdidos no ruído. Com algumas centenas de linhas de HTML, CSS e JavaScript, construímos algo diferente:

Um depurador visual que torna visíveis as entradas invisíveis. Um sistema CSS em camadas que mantém a IU limpa e depurável. Um conjunto de melhorias (gravação, exportação, instantâneos, reprodução fantasma) que o elevam de demonstração a ferramenta de desenvolvedor.

Este projeto mostra até onde você pode ir misturando o poder da plataforma Web com um pouco de criatividade em CSS Cascade Layers. A ferramenta que acabei de explicar na íntegra é de código aberto. Você pode clonar o repositório GitHub e experimentar você mesmo. Mas o mais importante é que você pode torná-lo seu. Adicione suas próprias camadas. Construa sua própria lógica de repetição. Integre-o ao protótipo do seu jogo. Ou até mesmo usá-lo de maneiras que não imaginei. Para ensino, acessibilidade ou análise de dados. No final das contas, não se trata apenas de depurar gamepads. Trata-se de iluminar entradas ocultas e dar aos desenvolvedores a confiança necessária para trabalhar com hardware que a web ainda não adota totalmente. Então, conecte seu controlador, abra seu editor e comece a experimentar. Você pode se surpreender com o que seu navegador e CSS podem realmente realizar.

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