Interface
Controles de toque para um jogo de corrida em WebView
Um jogo de corrida no celular pede duas mãos ocupadas ao mesmo tempo: uma para direção, outra para acelerar e frear. Em uma WebView isso esbarra em comportamentos pensados para páginas, não para jogos. Este texto descreve como os controles do Super Pinguin GP foram construídos e o que ainda falta validar.
Botões, inclinação ou deslizar
Havia três caminhos óbvios. A inclinação do aparelho é intuitiva, mas exige permissão de sensores em alguns navegadores, cansa em sessões longas e funciona mal quando a pessoa está deitada ou no transporte. Deslizar o dedo dá controle analógico, mas o ponto de origem do gesto é invisível e o dedo acaba saindo da área prevista. Botões fixos são menos sofisticados, porém previsíveis: o jogador sabe onde está cada comando sem olhar.
O jogo adotou botões: duas setas grandes à esquerda e os pedais à direita, com o freio em vermelho e o acelerador em verde. No modo de câmbio manual, aparece um botão central de marcha; no automático, ele some para não ocupar espaço nem ser tocado por engano.
Pointer Events e vários dedos
Eventos de toque (touchstart) e de mouse exigem dois caminhos de código. Pointer Events unificam os dois e trazem um identificador por dedo, o pointerId. O controle guarda um mapa desse identificador para a ação correspondente. Enquanto um polegar segura “direita” e outro segura “acelerar”, existem duas entradas no mapa, e cada uma é removida apenas quando o próprio dedo é levantado.
O detalhe que mais faz diferença é a captura do ponteiro. Em uma curva longa, o polegar escorrega alguns milímetros e sai do botão. Sem captura, o navegador dispara pointerleave e o carro deixa de virar no pior momento. Com setPointerCapture, o botão continua recebendo os eventos daquele dedo até ele ser levantado. O trecho real é curto:
button.addEventListener('pointerdown', event => {
if (!this.enabled() || (event.pointerType === 'mouse' && event.button !== 0)) return;
event.preventDefault();
this.pointers.set(event.pointerId, { action, button });
try { button.setPointerCapture?.(event.pointerId) } catch {}
this.sync();
});
for (const name of ['pointerup', 'pointercancel', 'lostpointercapture'])
button.addEventListener(name, release);
Também é tratado o pointercancel, que o sistema envia quando decide que o gesto virou outra coisa (uma notificação, um gesto de sistema). Ignorá-lo é a causa clássica do “acelerador preso”.
Um único estado de entrada
Teclado e toque não são tratados como fontes concorrentes. A cada mudança, o controle recalcula o estado a partir de tudo o que está pressionado: uma ação está ativa se houver uma tecla ou um dedo segurando-a. Isso evita que soltar a tecla “seta para cima” desligue o acelerador que ainda está sendo segurado na tela, situação comum em testes com teclado e mouse ao mesmo tempo.
Há uma regra explícita de prioridade: se freio e acelerador estiverem pressionados, vale o freio. Ao soltar o freio, o acelerador ainda segurado volta a valer sem que o jogador precise levantar o dedo. O mesmo recálculo atualiza aria-pressed e a classe visual de cada botão, então o que aparece na tela é sempre o que o jogo está usando.
Soltar tudo na hora certa
O pior defeito de um controle de toque é o comando fantasma: o jogo pausa, o jogador levanta o dedo durante a pausa e, ao voltar, o carro continua acelerando porque o evento de soltar nunca chegou. Por isso existe uma rotina que limpa todos os ponteiros e teclas e libera as capturas. Ela é chamada na pausa, na troca de tela, na perda de foco da janela, em pagehide e em visibilitychange.
No Android, a camada Flutter observa o ciclo de vida do aplicativo. Quando ele vai para segundo plano, a WebView recebe um evento de perda de foco, o que pausa a corrida e aciona a mesma limpeza. Uma ligação telefônica no meio de uma volta não deixa nenhum comando ativo.
Desligando comportamentos de página
Uma WebView ainda é um navegador. Segurar o dedo sobre um elemento abre menu de contexto ou seleção de texto; arrastar pode iniciar um arraste de imagem ou rolar a página. A área de controles usa touch-action: none, user-select: none e -webkit-touch-callout: none, e os botões cancelam contextmenu e dragstart. Sem isso, a primeira curva mais longa abre uma lupa de seleção sobre a palavra “ACELERAR”.
Tamanho, posição e área segura
Os botões têm 74 pixels de altura, reduzidos para 66 em telas muito baixas. A grade da tela reserva espaço para eles, em vez de sobrepô-los à pista, e respeita as margens de env(safe-area-inset-*) para não ficarem sob o recorte da câmera ou a barra de gestos. Os controles aparecem quando o dispositivo tem ponteiro grosso (toque) ou a tela é pequena, e somem fora da corrida, para que menus e resultados fiquem livres.
O jogo roda travado em paisagem e em modo imersivo no Android. A área de jogo mantém proporção 16:9 e é calculada a partir da altura disponível menos a faixa de controles.
Entrada digital, resposta gradual
Botões são digitais: virar é −1, 0 ou +1. Aplicar isso direto ao carro produziria movimentos bruscos. A física converte o comando em ângulo de volante com suavização exponencial dependente do tempo:
state.wheelAngle = lerp(state.wheelAngle, steering * .62, 1 - Math.exp(-dt * 12));
O fator 1 - Math.exp(-dt * 12) faz a resposta depender do tempo real, não do número de quadros. Toques curtos geram correções pequenas; segurar produz a curva completa. Um teste automatizado confere que a direção “entra progressivamente e mantém resposta semelhante a 30 e 120 quadros por segundo”. A técnica é discutida em Física independente da taxa de quadros.
Como não há entrada analógica, não há zona morta para calibrar, uma decisão a menos para o jogador e para o código.
O que ainda não foi feito
- Não há vibração (haptics) ao bater ou trocar de marcha.
- Não há suporte a controles físicos (gamepad).
- Os botões não podem ser reposicionados ou redimensionados pelo jogador.
- O gesto com dois polegares passa nos testes automatizados, que simulam ponteiros, mas ainda precisa ser validado em aparelho Android físico. É um item aberto no projeto, e o jogo não será publicado antes disso.
A lição mais geral é que o controle de toque é, em boa parte, tratamento de exceções: dedos que escorregam, eventos que não chegam, o sistema que interrompe. A parte visível, os botões, foi a mais simples.