Skip to content

Simulando um runtime que diz não: como isolamos o navegador para agir como o WeChat

Por Oleg Sidorkin, CTO e cofundador da Cinevva

Uma pequena cena de jogo 3D dentro de uma cúpula de vidro transparente, com ícones de ferramentas riscados flutuando do lado de fora

Lançamos recentemente um modo de minijogo do WeChat em nosso Criador de Jogos: um perfil de build que mantém os jogos gerados por IA dentro do subconjunto da plataforma web capaz de sobreviver à portabilidade para o runtime do WeChat. O perfil consiste em um conjunto de regras de geração. Nada de interface em DOM, Pointer Lock, áudio que não seja MP3 ou código dinâmico. O modelo segue essas regras da mesma forma que modelos seguem regras, ou seja: geralmente.

“Geralmente” não basta quando toda violação é invisível no navegador e fatal após a exportação. Um jogo com uma barra de vida em HTML funciona perfeitamente em nossa prévia e produz uma tela em branco no WeChat, pois os minijogos do WeChat não têm DOM. Por isso, criamos algo que transforma essas regras de sugestões em leis da física: um sandbox de modo estrito que faz o próprio navegador se recusar a fazer o que o WeChat não consegue. Este artigo explica como ele funciona.

A decisão central: simular a ausência, não a presença

A abordagem óbvia seria emular o WeChat: implementar wx.createCanvas, wx.onTouchStart, wx.setStorageSync e executar os jogos sobre a superfície wx.* emulada. Seguimos o caminho oposto, e o motivo está no formato do pipeline de exportação.

Os jogos no modo WeChat ainda são escritos usando APIs do navegador. No empacotamento, um adaptador (seguindo a abordagem padrão da comunidade, a weapp-adapter) mapeia essas APIs do navegador para wx.*. Isso significa que nossos jogos nunca chamam wx.* diretamente, portanto um emulador de wx testaria caminhos de código que não existem. O que realmente impede uma portabilidade não é a ausência de wx.* no navegador. É a presença de APIs do navegador sem equivalente no WeChat, usadas silenciosamente durante a geração. document.createElement('div'). requestPointerLock(). IndexedDB. Um arquivo .ogg que é decodificado sem problemas no Chrome, mas nunca funcionará no WeChat para iOS.

Portanto, o sandbox simula a ausência. Tudo o que o WeChat não tem é removido, bloqueado ou sinalizado na prévia, e o jogo é desenvolvido desde o primeiro frame dentro da interseção entre as duas plataformas.

Onde ele fica: em um service worker que já existia

A prévia do nosso editor tem um caminho de entrega incomum que tornou isso quase gratuito. Os jogos no editor não são servidos pela rede. Um service worker intercepta solicitações para /game/* e entrega arquivos do IndexedDB, permitindo que o iframe da prévia seja recarregado instantaneamente sem uma ida e volta ao servidor. Esse worker já injeta dois arquivos virtuais em cada página de jogo: um script de captura de erros que encaminha a saída do console e as falhas ao editor, e um inspetor de cena em tempo real.

O sandbox é o terceiro arquivo virtual, _wechat-strict.js, injetado no <head> da página imediatamente após o script de captura e antes da execução de qualquer código do jogo. A ordem é essencial por dois motivos. Ele precisa ser executado depois da captura para que suas chamadas a console.error já sejam interceptadas e encaminhadas ao editor, e antes dos módulos do jogo para que os bloqueios estejam ativos quando a primeira linha de código do jogo for executada.

A ativação usa um truque de uma linha. A opção WeChat no criador é persistida em localStorage, e o iframe da prévia tem a mesma origem que o editor, portanto o harness simplesmente lê o sinalizador diretamente:

js
try {
  if (localStorage.getItem('cinevva-gc-profile') !== 'wechat-minigame') return;
} catch (e) { return; }

Para qualquer jogo que não seja do WeChat, o script faz uma única comparação de strings e retorna imediatamente. Sem sinalizador de build, sem ida e volta ao servidor e sem estado para sincronizar com o service worker.

A lista de bloqueios e a taxonomia de dois níveis

Nem toda violação merece a mesma resposta, portanto o harness diferencia dois níveis. APIs que não existem no WeChat lançam erros, porque é isso que acontece após a exportação, e uma falha durante a geração é a prévia honesta de uma falha na análise. Recursos que existem, mas falham mais tarde são sinalizados com erros explícitos no console sem alterar o comportamento, pois bloqueá-los ocultaria o estado real do jogo da pessoa que está trabalhando nele.

O nível que lança erros: criação de qualquer elemento de interface HTML, tanto por createElement quanto por createElementNS (o Three.js cria seu canvas pela variante NS, então os dois caminhos precisam da mesma barreira), eval, new Function, requestPointerLock e registro de service workers. Cada erro lançado inclui a correção em sua mensagem:

js
throw violate("document.createElement('" + tag + "') — WeChat mini games have no DOM. " +
  "Draw UI on canvas (offscreen 2D canvas -> THREE.CanvasTexture on a screen-space quad) " +
  "and hit-test taps yourself.");

O nível de sinalização: leitura de indexedDB (retorna undefined, exatamente como o WeChat, além de um erro indicando o uso de localStorage), áudio em qualquer formato que o WeChat para iOS não consiga decodificar (verificado em três pontos: no construtor Audio, no setter src do protótipo do elemento de mídia e nas URLs buscadas), solicitações de rede para qualquer origem que não seja a do próprio jogo ou nossa CDN de assets (no WeChat, elas exigem um domínio incluído em uma lista de permissões e registrado no ICP) e qualquer ocorrência do Tone.js.

Também há uma auditoria pós-carregamento executada pouco depois de a página se estabilizar: ela percorre o <body> e relata qualquer elemento HTML vindo da marcação do próprio jogo, em vez de uma criação dinâmica. HUDs estáticos passam despercebidos por uma barreira em createElement, então a barreira sozinha não é suficiente.

Cada relatório elimina duplicatas por mensagem e tem um limite de cinquenta ocorrências por sessão. Um bug de loop invertido que cria uma div por frame produz um erro, não uma enxurrada que afoga o sinal relevante.

As quatro exceções que mais nos ensinaram

Criar um sandbox consiste, em grande parte, em decidir o que não bloquear, e cada exceção que abrimos surgiu quando nossas próprias ferramentas pararam de funcionar durante o desenvolvimento.

Nosso depurador usa eval. A ferramenta execute_js do criador, usada pela IA para inspecionar o jogo em execução, avalia código pelo manipulador de mensagens do script de captura, que chama eval. Bloquear eval de forma ingênua teria cegado os próprios olhos da IA. A solução: antes de aplicar o bloqueio, o harness armazena a função real em uma propriedade não enumerável, e o manipulador do script de captura recorre a ela:

js
Object.defineProperty(window, '__cinevvaRealEval', { value: window.eval, enumerable: false });
window.eval = function () { throw violate('eval() is banned in WeChat mini games.'); };
// capture script, at message time:
returnValue = (window.__cinevvaRealEval || eval)(e.data.code);

O código do jogo que tentar usar eval ainda falha. O depurador não.

O gravador também cria elementos. Nosso gravador de reels é injetado no iframe do jogo como um <script> criado com o próprio document.createElement do iframe, e baixa os vídeos concluídos por meio de um clique sintético em <a>. Bloquear essas tags teria impedido a gravação exclusivamente no modo WeChat. Por isso, script continua permitido e a gera um aviso sem lançar erro. Um jogo que inclua um link de verdade ainda é sinalizado, e as ferramentas continuam funcionando.

Os eventos de teclado permanecem. A IA verifica os controles com uma ferramenta que sintetiza pressionamentos de teclas e depois lê como o estado do jogo mudou. Suprimir a entrada de teclado para simular um celular teria destruído o ciclo de verificação de controles que detecta bugs de câmera invertida. A prioridade para toque é aplicada pelas regras de geração e pelo esquema de controles do perfil, não pelo sandbox.

O WebGL2 permanece, e o motivo merece uma seção própria abaixo. A versão inicial da lista de bloqueios impedia getContext('webgl2') para forçar a renderização “compatível com WebGL1” exigida pelo perfil. Só que o Three.js removeu completamente o suporte ao WebGL1 na r163, e fixamos a versão r181: bloquear o WebGL2 deixaria todos os jogos 3D desse modo em branco. A regra de renderização do perfil era incoerente, escrita a partir de uma visão desatualizada da plataforma. A auditoria do sandbox acabou auditando a própria especificação do sandbox, e investigar esse ponto nos levou a uma conclusão importante.

A questão do WebGL2, respondida corretamente

Manter a criação de contexto intacta levantou a pergunta óbvia: se nosso motor exige WebGL2, os jogos deixam de funcionar em dispositivos com WeChat que só têm WebGL1? A investigação nos deu a visão mais clara que temos sobre os requisitos mínimos de renderização da plataforma, então aqui está ela, com fontes.

No Android, o runtime de minijogos do WeChat oferece WebGL2 em qualquer biblioteca-base moderna, e o hardware GLES3 subjacente é praticamente universal. A verdadeira questão é o iOS. A própria documentação de engenharia do WeChat sobre suporte ao WebGL2 e o modo high-performance+ deixa claro que, no iPhone, o WebGL2 só está disponível corretamente no runtime high-performance+, que exige uma versão recente do cliente WeChat (8.0.45 ou superior, ainda mais recente no iOS 14) e, na prática, iOS 15.5 ou posterior. Segundo a própria descrição da Tencent, o modo high-performance comum executa WebGL2 “com mais problemas”, e o runtime normal do iOS não oferece suporte algum.

A parte realmente perigosa é o formato da falha. Em ambientes sem suporte, getContext('webgl2') pode retornar um contexto válido em aparência, mas quebrado, em vez de null, um comportamento que desenvolvedores pediram diretamente à Tencent que corrigisse. Um jogo que confia no valor retornado não falha de forma clara nesse ambiente. Ele renderiza artefatos ou exibe silenciosamente uma tela preta.

Por isso, lidamos com a questão em três camadas. O sandbox não interfere na criação do contexto WebGL2, pois bloqueá-la entraria em conflito com nosso próprio motor. Agora, o perfil de geração exige uma barreira de inicialização em todos os jogos: a criação do renderer envolvida em try/catch, seguida por uma verificação funcional (typeof gl.createVertexArray === 'function', uma capacidade exclusiva do WebGL2 que um contexto mentiroso não terá), exibindo em caso de falha uma mensagem amigável desenhada no canvas, como “atualize o WeChat”, em vez de uma tela em branco. Esse é o mesmo padrão usado por minijogos convertidos a partir do Unity, motivo pelo qual vemos solicitações de atualização em jogos publicados, em vez de telas pretas. E o exportador, quando for lançado, fixará o modo high-performance+ no game.json para que o caminho compatível seja o padrão.

Qual é o custo de público de exigir WebGL2? Usuários em versões anteriores ao iOS 15.5 ou em clientes WeChat anteriores à versão 8.0.45 — poucos pontos percentuais em 2026, mas concentrados em dispositivos mais antigos, o que importa mais para alguns gêneros do que para outros. Se dados reais de distribuição algum dia mostrarem que vale a pena buscar essa parcela, temos uma alternativa barata de reserva: fixar o perfil do WeChat no Three r162, a última versão compatível com WebGL1. Os jogos do perfil usam apenas APIs centrais estáveis, portanto o downgrade consiste em uma alteração de uma linha no import map, deliberadamente reservada até que os dados a justifiquem.

Fechando o ciclo com o modelo

Esta é a parte que faz tudo isso valer a pena para um produto centrado em IA. Depois de cada build, o agente do criador chama uma ferramenta que lê o console do jogo e aguarda a conclusão da inicialização. O script de captura encaminha console.error para esse fluxo. Assim, uma violação [wechat-strict] não é um aviso que uma pessoa pode ignorar ao rolar a tela. Ela chega exatamente ao canal que o modelo já verifica antes de declarar que um build está concluído, na mesma interação e com a correção descrita na mensagem.

O perfil de geração fecha a última lacuna com uma instrução: trate toda mensagem [wechat-strict] como um bug que impede o build, corrija a causa e nunca tente detectar ou contornar o harness. Um jogo que só funciona com o sandbox desativado é um jogo que falhará após a exportação, e isso é explicado claramente ao modelo.

Na prática, o sandbox transforma um problema impreciso de conformidade (“o modelo seguiu todas as quatorze regras?”) no ciclo de depuração em que o sistema já é bom (“o console mostra um erro; corrija-o”).

O que um navegador não consegue simular

A seção da honestidade. Este sandbox detecta violações da superfície de APIs, que, segundo nossa estimativa, representam a grande maioria dos problemas que inviabilizam uma portabilidade. Ele não consegue detectar: desempenho em um celular real, execução de JavaScript sem JIT no iOS, peculiaridades da implementação de WebGL do WeChat ou diferenças de comportamento de codecs além das extensões de arquivo. Para isso, é necessário usar o runtime real.

Portanto, o sandbox é o primeiro de três níveis. O segundo, quando nosso exportador passar a gerar projetos para o WeChat DevTools, será o simulador oficial, controlado de forma headless pela CLI do DevTools e pelo miniprogram-automator: inicializar o pacote exportado, confirmar que o primeiro frame é renderizado e simular um toque. O terceiro é o caminho oficial em dispositivos, com códigos QR de prévia e depuração remota em celulares físicos, onde vivem as verdades que nenhum outro ambiente revela. Cada nível é mais lento e mais fiel que o anterior, e a função de cada um é tornar raras as idas ao próximo.

Se quiser experimentar o modo, procure a opção “Modo de minijogo do WeChat” no Criador de Jogos. Para conhecer o contexto comercial e regulatório de tudo isso, consulte nosso guia de campo para alcançar o meio bilhão de jogadores do WeChat.

Referências