Como esse jogo intriga jogadores e desenvolvedores, destacando os recursos surpreendentes do 3D no navegador

Descubra o potencial do WebGL com o cenário infinito gerado proceduralmente deste jogo de corrida casual.

Slow Roads é um jogo de direção casual com ênfase em cenários gerados proceduralmente, tudo hospedado no navegador como um aplicativo WebGL. Para muitas pessoas, essa experiência intensiva pode parecer fora do lugar no contexto limitado do navegador. Na verdade, corrigir essa atitude foi um dos meus objetivos com esse projeto. Neste artigo, vou explicar algumas das técnicas que usei para superar o obstáculo de desempenho na minha missão de destacar o potencial muitas vezes esquecido do 3D na Web.

Desenvolvimento em 3D no navegador

Depois de lançar o recurso "Ruas lentas", notei um comentário recorrente no feedback: "Eu não sabia que isso era possível no navegador". Se você compartilha esse sentimento, com certeza não é uma minoria. De acordo com a pesquisa Estado do JS de 2022, cerca de 80% dos desenvolvedores ainda não experimentaram o WebGL. Para mim, é uma pena que tanto potencial possa ser perdido, especialmente quando se trata de jogos para navegador. Com o Slow Roads, espero trazer o WebGL para os holofotes e talvez reduzir o número de desenvolvedores que se assustam com a frase "motor de jogo JavaScript de alto desempenho".

O WebGL pode parecer misterioso e complexo para muitos, mas nos últimos anos, os ecossistemas de desenvolvimento dele amadureceram muito e se tornaram ferramentas e bibliotecas muito eficientes e convenientes. Agora é mais fácil do que nunca para desenvolvedores front-end incorporarem a UX 3D ao trabalho, mesmo sem experiência anterior em gráficos de computador. A Three.js, a biblioteca líder do WebGL, serve como base para muitas expansões, incluindo a react-three-fiber, que traz componentes 3D para o framework React. Agora há também editores de jogos abrangentes baseados na Web, como Babylon.js ou PlayCanvas, que oferecem uma interface familiar e ferramentas integradas.

No entanto, apesar da utilidade dessas bibliotecas, projetos ambiciosos acabam sendo limitados por limitações técnicas. Os céticos em relação à ideia de jogos baseados em navegador podem destacar que o JavaScript é de linha única e limitado por recursos. No entanto, navegar por essas limitações revela o valor oculto: nenhuma outra plataforma oferece a mesma acessibilidade instantânea e compatibilidade em massa ativada pelo navegador. Os usuários em qualquer sistema compatível com navegador podem começar a assistir com um clique, sem precisar instalar aplicativos nem fazer login nos serviços. Sem mencionar que os desenvolvedores aproveitam a conveniência de ter frameworks front-end robustos disponíveis para criar interfaces ou processar redes para modos multiplayer. Na minha opinião, esses valores são o que torna o navegador uma plataforma excelente para jogadores e desenvolvedores. Como demonstrado por Slow Roads, as limitações técnicas podem ser reduzidas a um problema de design.

Como ter um bom desempenho em vias lentas

Como os elementos principais de "Slow Roads" envolvem movimentos de alta velocidade e geração de cenários caros, a necessidade de desempenho suave foi enfatizada em todas as decisões de design. Minha principal estratégia foi começar com um design de jogabilidade simplificado que permitisse atalhos contextuais na arquitetura do mecanismo. Por outro lado, isso significa trocar alguns recursos úteis em busca do minimalismo, mas resulta em um sistema personalizado e hiperotimizado que funciona bem em diferentes navegadores e dispositivos.

Confira a seguir os principais componentes que mantêm o Lean Road.

Como moldar o mecanismo do ambiente em torno da jogabilidade

Como componente principal do jogo, o mecanismo de geração de ambiente é inevitavelmente caro, justificando a maior proporção dos orçamentos de memória e computação. O truque usado aqui é programar e distribuir a computação pesada em um período de tempo para não interromper a taxa de quadros com picos de desempenho.

O ambiente é composto por blocos de geometria, que diferem em tamanho e resolução (categorizados como "níveis de detalhes" ou LODs) dependendo de quão próximos eles vão aparecer da câmera. Em jogos típicos com uma câmera de livre circulação, diferentes níveis de detalhes precisam ser carregados e removidos constantemente para detalhar o ambiente do jogador, onde quer que ele vá. Isso pode ser uma operação cara e ineficiente, especialmente quando o ambiente é gerado dinamicamente. Felizmente, essa convenção pode ser totalmente subvertida em estradas lentas, graças à expectativa contextual de que o usuário deve permanecer na estrada. Em vez disso, a geometria de alto detalhe pode ser reservada para o corredor estreito que fica ao lado da rota.

Um diagrama mostrando como gerar a estrada com antecedência pode permitir a programação proativa e o armazenamento em cache da geração do ambiente.
Uma visualização da geometria do ambiente em vias lentas renderizada como um wireframe, indicando corredores de geometria de alta resolução que margeiam a via. Partes distantes do ambiente, que nunca devem ser vistas de perto, são renderizadas em uma resolução muito mais baixa.

A linha média da estrada é gerada muito antes da chegada do jogador, permitindo a previsão precisa de quando e onde o detalhe do ambiente será necessário. O resultado é um sistema enxuto que pode programar proativamente trabalhos caros, gerando apenas o mínimo necessário em cada momento e sem esforço desperdiçado em detalhes que não serão vistos. Essa técnica é possível apenas porque a estrada é um caminho único e sem ramificações, um bom exemplo de como fazer trocas de jogabilidade que acomodam atalhos de arquitetura.

Um diagrama mostrando como gerar a estrada com antecedência pode permitir a programação proativa e o armazenamento em cache da geração do ambiente.
Ao olhar uma certa distância ao longo da estrada, os blocos do ambiente podem ser antecipados e gerados gradualmente pouco antes de serem necessários. Além disso, qualquer bloco que será revisitado em breve pode ser identificado e armazenado em cache para evitar a regeneração desnecessária.

Ser exigente com as leis da física

A segunda maior demanda computacional do mecanismo de ambiente é a simulação de física. O jogo usa um mecanismo de física mínimo e personalizado que usa todos os atalhos disponíveis.

A maior economia aqui é evitar a simulação de muitos objetos. Para isso, use o contexto mínimo e zen, descartando coisas como colisões dinâmicas e objetos destrutíveis. A suposição de que o veículo vai permanecer na estrada significa que as colisões com objetos fora da estrada podem ser ignoradas. Além disso, a codificação da estrada como uma linha média esparsa permite truques elegantes para a detecção rápida de colisões com a superfície da estrada e os guarda-corpos, tudo com base em uma verificação de distância até o centro da estrada. Dirigir fora da estrada fica mais caro, mas esse é outro exemplo de um bom compromisso adequado ao contexto da jogabilidade.

Gerenciar o consumo de memória

Como outro recurso restrito ao navegador, é importante gerenciar a memória com cuidado, apesar de o JavaScript ser coletado. Pode ser fácil ignorar isso, mas declarar até mesmo pequenas quantidades de nova memória em um loop de jogo pode se transformar em problemas significativos ao executar a 60 Hz. Além de consumir os recursos do usuário em um contexto em que ele provavelmente está fazendo várias tarefas, grandes coletas de lixo podem levar vários frames para serem concluídas, causando falhas perceptíveis. Para evitar isso, a memória do loop pode ser pré-alocada em variáveis de classe na inicialização e reciclada em cada frame.

Uma comparação do perfil de memória antes e depois da otimização do código-fonte do Slow Roads, indicando economias significativas e uma redução na taxa de coleta de lixo.
Embora a utilização geral de memória mal mude, a pré-alocação e a reciclagem de memória de loop podem reduzir bastante o impacto de coletas de lixo caras.

Também é muito importante que estruturas de dados mais pesadas, como geometrias e buffers de dados associados, sejam gerenciadas de forma econômica. Em um jogo gerado infinitamente, como o Slow Roads, a maior parte da geometria existe em uma espécie de esteira. Quando uma peça antiga fica para trás, as estruturas de dados dela podem ser armazenadas e recicladas para uma próxima peça do mundo, um padrão de design conhecido como agrupamento de objetos.

Essas práticas ajudam a priorizar a execução enxuta, sacrificando um pouco da simplicidade do código. Em contextos de alto desempenho, é importante estar ciente de como os recursos de conveniência às vezes são emprestados do cliente para o benefício do desenvolvedor. Por exemplo, métodos como Object.keys() ou Array.map() são muito úteis, mas é fácil esquecer que cada um deles cria uma nova matriz para o valor de retorno. Entender o funcionamento interno dessas caixas-pretas pode ajudar a apertar o código e evitar problemas de desempenho ocultos.

Como reduzir o tempo de carregamento com recursos gerados proceduralmente

Embora o desempenho de execução seja a principal preocupação dos desenvolvedores de jogos, os axiomas usuais sobre o tempo de carregamento inicial da página da Web ainda são válidos. Os usuários podem ser mais tolerantes ao acessar conteúdo pesado, mas os tempos de carregamento longos ainda podem prejudicar a experiência, se não a retenção do usuário. Os jogos geralmente exigem recursos grandes na forma de texturas, sons e modelos 3D. No mínimo, eles precisam ser compactados cuidadosamente sempre que detalhes puderem ser poupados.

Como alternativa, a geração procedural de recursos no cliente pode evitar transferências demoradas. Isso é um grande benefício para os usuários com conexões lentas e dá ao desenvolvedor um controle mais direto sobre como o jogo é constituído, não apenas para a etapa de carregamento inicial, mas também para adaptar os níveis de detalhes para diferentes configurações de qualidade.

Uma comparação que ilustra como a qualidade da geometria gerada proceduralmente em estradas lentas pode ser adaptada dinamicamente às necessidades de desempenho do usuário.

A maior parte da geometria em estradas lentas é gerada proceduralmente e é simples, com sombreadores personalizados que combinam várias texturas para dar detalhes. A desvantagem é que essas texturas podem ser recursos pesados, mas há outras oportunidades de economia aqui, com métodos como texturização estocástica capaz de alcançar mais detalhes de pequenas texturas de origem. E, em um nível extremo, também é possível gerar texturas inteiramente no cliente com ferramentas como texgen.js. O mesmo vale para o áudio, com a API Web Audio permitindo a geração de som com nós de áudio.

Com o benefício dos recursos procedurais, a geração do ambiente inicial leva apenas 3,2 segundos, em média. Para aproveitar ao máximo o tamanho pequeno do download inicial, uma tela de apresentação simples cumprimenta os novos visitantes e adia a inicialização de cena cara até depois de um botão de confirmação ser pressionado. Isso também funciona como um buffer conveniente para sessões rejeitadas, minimizando a transferência desperdiçada de recursos carregados dinamicamente.

Um histograma de tempos de carregamento mostrando um pico forte nos primeiros três segundos, representando mais de 60% dos usuários, seguido por uma queda rápida. O histograma mostra que mais de 97% dos usuários têm tempos de carregamento de menos de 10 segundos.

Usar uma abordagem ágil para a otimização tardia

Sempre considerei a base de código do Slow Roads como experimental e, por isso, usei uma abordagem extremamente ágil para o desenvolvimento. Ao trabalhar com uma arquitetura de sistema complexa e em rápida evolução, pode ser difícil prever onde os gargalos importantes podem ocorrer. O foco deve ser implementar os recursos desejados rapidamente, em vez de de forma limpa, e trabalhar de trás para a frente para otimizar os sistemas onde realmente importa. O perfilador de desempenho no Chrome DevTools é muito útil para essa etapa e me ajudou a diagnosticar alguns problemas importantes com versões anteriores do jogo. O tempo do desenvolvedor é valioso. Portanto, não perca tempo deliberando sobre problemas que podem ser insignificantes ou redundantes.

Monitorar a experiência do usuário

Ao implementar todos esses truques, é importante garantir que o jogo funcione como esperado no mundo real. Acomodar uma variedade de recursos de hardware é um aspecto básico de qualquer desenvolvimento de jogos, mas os jogos da Web podem segmentar um espectro muito mais amplo, que inclui computadores de última geração e dispositivos móveis com uma década de idade ao mesmo tempo. A maneira mais simples de fazer isso é oferecer configurações para adaptar os gargalos mais prováveis na base de código, para tarefas de uso intensivo de GPU e CPU, conforme revelado pelo perfilador.

No entanto, o perfil na sua própria máquina só pode cobrir um determinado nível, então é importante fechar o ciclo de feedback com os usuários de alguma forma. Para "Ruas lentas", eu faço análises simples que informam sobre a performance e fatores contextuais, como a resolução da tela. Essas análises são enviadas para um back-end básico do Node usando o socket.io, junto com qualquer feedback escrito que o usuário envia pelo formulário no jogo. No início, essas análises detectavam muitos problemas importantes que podiam ser mitigados com mudanças simples na UX, como destacar o menu de configurações quando um FPS consistentemente baixo era detectado ou avisar que um usuário precisava ativar a aceleração de hardware se a performance fosse particularmente ruim.

As vias lentas à frente

Mesmo depois de todas essas medidas, ainda há uma parte significativa da base de jogadores que precisa jogar em configurações mais baixas, principalmente aqueles que usam dispositivos leves sem GPU. Embora a variedade de configurações de qualidade disponíveis leve a uma distribuição de desempenho bastante uniforme, apenas 52% dos jogadores alcançam mais de 55 QPS.

Uma matriz definida pela configuração de distância de visualização em relação à configuração de detalhes, mostrando a média de frames por segundo alcançados em diferentes combinações. A distribuição está bastante uniforme entre 45 e 60, sendo 60 a meta para uma boa performance. Os usuários com configurações baixas tendem a ter uma taxa de QPS menor do que aqueles com configurações altas, destacando as diferenças na capacidade do hardware do cliente.
Esses dados são um pouco distorcidos pelos usuários que executam o navegador com a aceleração de hardware desativada, o que geralmente causa uma performance artificialmente baixa.

Felizmente, ainda há muitas oportunidades para economizar no desempenho. Além de adicionar outros truques de renderização para reduzir a demanda de GPU, espero experimentar trabalhadores da Web para paralelizar a geração de ambiente em breve e, eventualmente, perceber a necessidade de incorporar o WASM ou o WebGPU à base de código. Qualquer espaço que eu consiga liberar permitirá ambientes mais ricos e diversos, que serão a meta permanente para o restante do projeto.

Como projetos de passatempo, o Slow Roads foi uma maneira extremamente satisfatória de demonstrar como os jogos para navegador podem ser surpreendentemente elaborados, eficientes e populares. Se eu consegui despertar seu interesse no WebGL, saiba que, tecnologicamente, o Slow Roads é um exemplo bastante superficial dos recursos completos. Encorajamos os leitores a conferir o showcase do Three.js. E aqueles que têm interesse em desenvolvimento de jogos da Web em particular são bem-vindos à comunidade em webgamedev.com.