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 por processos deste jogo casual de direção.

O Slow Roads é um jogo de direção casual com ênfase em cenários gerados de maneira infinita e processual, todos hospedados no navegador como um aplicativo WebGL. Para muitos, uma experiência tão intensa pode parecer deslocada no contexto limitado do navegador. De fato, reescrever essa atitude é uma das minhas metas com este projeto. Neste artigo, vou detalhar algumas das técnicas que usei para superar o obstáculo de desempenho na minha missão de destacar o potencial muitas vezes ignorado do 3D na Web.

Desenvolvimento em 3D no navegador

Depois de lançar o Slow Roads, vi um comentário recorrente no feedback: "Não sabia que isso era possível no navegador". Se você compartilha esse sentimento, certamente não faz parte de uma minoria. De acordo com a pesquisa State of JS 2022 (em inglês), cerca de 80% dos desenvolvedores ainda não testaram o WebGL. Para mim, é uma pena que tanto potencial pode ser perdido, especialmente quando se trata de jogos baseados em navegadores. Com o Slow Roads, espero colocar o WebGL ainda mais em destaque e, talvez, reduzir o número de desenvolvedores que se recusam à frase "mecanismo de jogo em JavaScript de alto desempenho".

O WebGL pode parecer misterioso e complexo para muitos, mas, nos últimos anos, seus ecossistemas de desenvolvimento amadureceram em ferramentas e bibliotecas altamente capazes e convenientes. Agora está mais fácil do que nunca para os desenvolvedores front-end incorporar a UX 3D ao trabalho, mesmo sem experiência prévia em computação gráfica. A Three.js, a principal biblioteca WebGL, serve como base para muitas expansões, incluindo react-three-fiber (link em inglês), que traz componentes 3D para o framework do React. Agora, também existem editores de jogos abrangentes baseados na Web, como Babylon.js ou PlayCanvas, que oferecem uma interface familiar e conjuntos de ferramentas integrados.

Apesar da utilidade notável dessas bibliotecas, projetos ambiciosos acabam sendo limitados por limitações técnicas. Os céticos à ideia de jogos com base em navegador podem destacar que o JavaScript tem uma linha de execução única e é limitada por recursos. No entanto, navegar por essas limitações desbloqueia 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 jogar com um clique, sem precisar instalar aplicativos e fazer login em serviços. Sem mencionar que os desenvolvedores aproveitam a conveniência elegante de ter frameworks de front-end robustos disponíveis para criar interface ou lidar com redes para modos multiplayer. Na minha opinião, esses valores são o que tornam o navegador uma plataforma excelente para jogadores e desenvolvedores. Além disso, como demonstrado pela Slow Roads, as limitações técnicas podem ser redutíveis a um problema de design.

Como ter uma boa performance em vias lentas

Como os principais elementos de Slow Roads envolvem movimento de alta velocidade e geração de cenários caras, a necessidade de um desempenho suave afirmou todas as minhas decisões de design. Minha estratégia principal era começar com um design de jogabilidade simples que permitisse atalhos contextuais de uso dentro da arquitetura do mecanismo. O ponto negativo é abandonar alguns recursos interessantes para buscar o minimalismo, mas isso resulta em um sistema sob medida e hiperotimizado que funciona muito bem em diferentes navegadores e dispositivos.

Veja a seguir uma descrição dos principais componentes que mantêm as estradas lentas enxutas.

Como modelar o mecanismo do ambiente em torno do jogo

Por ser o componente principal do jogo, o mecanismo de geração de ambiente é inevitavelmente caro, e é justificável que a maior parte dos orçamentos para memória e computação seja usada. O truque usado aqui é programar e distribuir a computação pesada durante um período, de modo a não interromper o frame rate com picos de desempenho.

O ambiente é composto de blocos de geometria, que diferem em tamanho e resolução (categorizados como "níveis de detalhe" ou LoDs), dependendo de quão perto eles aparecerão da câmera. Em jogos típicos com câmera de roaming livre, diferentes LoDs precisam ser carregados e descarregados constantemente para detalhar os arredores do jogador onde quer que ele escolha ir. Essa pode ser uma operação cara e desperdiçada, especialmente quando o próprio ambiente é gerado dinamicamente. Felizmente, essa convenção pode ser totalmente subvertida em vias lentas, graças à expectativa contextual de que o usuário precisa permanecer na estrada. Em vez disso, a geometria de alto detalhe pode ser reservada para o corredor estreito que circunda o trajeto.

Um diagrama que mostra como gerar a estrada com muita 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 ao lado da via. Porções 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 uma previsão precisa de exatamente quando e onde os detalhes do ambiente serão necessários. 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 só é possível porque a estrada é um caminho único e sem ramificação, um bom exemplo de como fazer compensações de jogabilidade que acomodam atalhos arquitetônicas.

Um diagrama que mostra como gerar a estrada com muita antecedência pode permitir a programação proativa e o armazenamento em cache da geração do ambiente.
Ao observar uma certa distância ao longo da estrada, os blocos do meio ambiente podem ser evitados e gerados gradualmente pouco antes de serem necessários. Além disso, todos os blocos que serão revisitados em um futuro próximo podem ser identificados e armazenados em cache para evitar regenerações desnecessárias.

Ser exigente com as leis da física

Depois da demanda computacional do mecanismo de ambiente está a simulação física. O Slow Roads usa um mecanismo de física mínimo e personalizado que pega todos os atalhos disponíveis.

A maior economia aqui é evitar a simulação de muitos objetos, dedicando-se ao contexto mínimo e zen descontando coisas como colisões dinâmicas e objetos destrutivos. A suposição de que o veículo vai permanecer na estrada significa que colisões com objetos fora dela podem ser razoavelmente ignoradas. Além disso, a codificação da via como uma linha média esparsa permite truques elegantes para detectar rapidamente colisão com a superfície da via e a proteção, tudo com base em uma verificação da distância até o centro da via. A condução off-road fica mais cara, mas esse é outro exemplo de uma compensação justa adequada ao contexto do jogo.

Gerenciar o consumo de memória

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

Uma visualização de antes e depois do perfil de memória durante a otimização da base de código de Slow Roads, indicando economias significativas e uma redução na taxa de coleta de lixo.
Embora a utilização geral da memória seja pouco alterada, a memória de loop de pré-alocação e reciclagem pode reduzir muito o impacto de coletas de lixo caras.

Também é muito importante que estruturas de dados mais pesadas, como geometrias e seus buffers de dados associados, sejam gerenciadas economicamente. Em um jogo infinitamente gerado, 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 novamente para o futuro, um padrão de design conhecido como pool de objetos.

Essas práticas ajudam a priorizar a execução enxuta, mas sem simplificar o código. Em contextos de alta performance, é importante estar ciente de como os recursos de conveniência às vezes emprestam do cliente para o desenvolvedor. Por exemplo, métodos como Object.keys() ou Array.map() são incrivelmente úteis, mas é fácil ignorar que cada um cria uma nova matriz para o valor de retorno. Entender o funcionamento interno dessas caixas pretas pode ajudar a enriquecer seu código e evitar hits de desempenho não autorizados.

Reduzir o tempo de carregamento com recursos gerados processualmente

Embora o desempenho no tempo de execução seja a principal preocupação para os desenvolvedores de jogos, os axiomas comuns relacionados ao tempo de carregamento inicial da página da Web ainda são verdadeiros. Os usuários podem ser mais criteriosos ao acessar conteúdo pesado de forma consciente, mas longos tempos de carregamento ainda podem ser prejudiciais para a experiência se não forem a retenção de usuários. Os jogos geralmente exigem recursos grandes na forma de texturas, sons e modelos 3D e, no mínimo, eles precisam ser cuidadosamente compactados sempre que for possível poupar detalhes.

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

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

A maior parte da geometria em Slow Roads é gerada de forma processual e simplista, com sombreadores personalizados combinando várias texturas para dar mais detalhes. A desvantagem é que essas texturas podem ser recursos pesados, embora existam mais oportunidades de economia aqui, com métodos como a textura estocástico capaz de conseguir mais detalhes com texturas de fonte pequena. E, em um nível extremo, também é possível gerar texturas inteiramente no cliente com ferramentas como texgen.js. O mesmo vale até para áudio, com a API Web Audio, que permite a geração de som com nós de áudio.

Com a vantagem dos ativos processuais, a geração do ambiente inicial leva em média apenas 3,2 segundos. Para aproveitar melhor o pequeno tamanho do download inicial, uma tela de apresentação simples recebe os novos visitantes e adia a inicialização cara da cena até após o pressionamento de um botão afirmativo. Isso também funciona como um buffer conveniente para sessões com rejeições, minimizando o desperdício de transferências de recursos carregados dinamicamente.

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

Como adotar uma abordagem ágil para a otimização tardia

Sempre considerei a base de código do Slow Roads experimental e, por isso, adotei uma abordagem de desenvolvimento extremamente ágil. Ao trabalhar com uma arquitetura de sistema complexa e em rápida evolução, pode ser difícil prever onde podem ocorrer os gargalos importantes. O foco precisa ser implementar os recursos desejados com rapidez, e não de forma limpa, e trabalhar de trás para frente para otimizar os sistemas onde isso é realmente importante. O criador de perfil de desempenho no Chrome DevTools é inestimável para esta etapa e me ajudou a diagnosticar alguns problemas importantes com versões anteriores do jogo. Seu tempo como desenvolvedor é valioso. Por isso, não perca tempo deliberando sobre problemas que possam ser insignificantes ou redundantes.

Como monitorar a experiência do usuário

Ao implementar todos esses truques, é importante garantir que o jogo tenha o desempenho esperado em condições gerais. Acomodar uma variedade de recursos de hardware é um aspecto básico de qualquer desenvolvimento de jogos, mas os jogos para Web podem ter como alvo um espectro muito mais amplo, que abrange computadores de última geração e dispositivos móveis antigos ao mesmo tempo. A maneira mais simples de abordar isso é oferecer configurações para adaptar os gargalos mais prováveis na sua base de código (para tarefas com uso intenso de GPU e CPU), conforme revelado pelo criador de perfil.

No entanto, a criação de perfil em sua própria máquina só pode cobrir muito, por isso é valioso fechar o ciclo de feedback com seus usuários de alguma forma. Para vias lentas, faço análises simples que geram relatórios sobre o desempenho e fatores contextuais, como a resolução da tela. Essas análises são enviadas para um back-end básico de nó usando socket.io, com qualquer feedback por escrito que o usuário enviar pelo formato do jogo. No início, essas análises detectaram muitos problemas importantes que poderiam ser atenuados com mudanças simples na UX, como destacar o menu de configurações quando um QPS consistentemente baixo é detectado ou alertar que um usuário pode precisar ativar a aceleração de hardware se o desempenho for particularmente ruim.

As vias lentas à frente

Mesmo depois de tomar todas essas medidas, ainda resta uma parte significativa da base de jogadores que precisa ser usada em configurações mais baixas, principalmente aquelas que usam dispositivos leves sem GPU. Embora a variedade de configurações de qualidade disponíveis leve a uma distribuição de performance bem 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 quadros por segundo alcançado em diferentes pareamentos. A distribuição é bem uniforme entre 45 e 60, sendo 60 o objetivo de um bom desempenho. Usuários em configurações baixas tendem a ver um QPS menor do que aqueles em configurações altas, o que destaca as diferenças na capacidade do hardware cliente.
Esses dados são um pouco distorcidos pelos usuários que executam o navegador com a aceleração de hardware desativada, muitas vezes causando um desempenho artificialmente baixo.

Felizmente, ainda há muitas oportunidades para economizar no desempenho. Além de adicionar mais truques de renderização para reduzir a demanda de GPU, espero fazer testes com Web workers carregando a geração do ambiente em paralelo e talvez precise incorporar o WASM ou a WebGPU à base de código. Qualquer margem que eu possa liberar permitirá ambientes mais ricos e diversos, que será o objetivo duradouro para o restante do projeto.

Para os projetos de hobbies, o Slow Roads tem sido uma maneira muito gratificante de demonstrar como os jogos para navegadores podem ser surpreendentemente elaborados, com desempenho e desempenho. Se eu consegui despertar seu interesse em WebGL, saiba que a tecnologia de vias lentas é um exemplo bastante superficial de seus recursos completos. Recomendo fortemente que os leitores conheçam a demonstração do three.js. Os interessados em desenvolvimento de jogos da Web, em particular, gostariam de conhecer a comunidade em webgamedev.com.