Desenvolvimento de Jogos para Programadores(as) WEB - Parte 1

gcnovout

Giovane Cardoso

Posted on February 23, 2021

Desenvolvimento de Jogos para Programadores(as) WEB - Parte 1

Sumário

  1. Introdução
  2. Game Loop
  3. Tick
  4. UI
  5. Ciclo de Vida
  6. ECS
  7. Informações Adicionais
  8. Parte 2 - PixiJS

Introdução

Neste artigo vamos buscar trazer a familiarização com DOM a forma que jogos funcionam internamente, assim conseguindo relacionar conceitos que um desenvolvedor(a) WEB possui com o que está sendo proposto.

Este artigo é recomendado para desenvolvedores(as) que possuem um bom conhecimento de DOM e sobre gerenciamento de estado, mas nada impede de seguir os exemplos já que tem muitos link's auxiliares :)

Game Loop

Os loops de jogo são o exemplo perfeito de um padrão de programação para jogos. Quase todo jogo tem um, e relativamente poucos programas fora dos jogos os usam.

Podemos fazer um paralelo entre os loop's convencionais com os Event Listeners do DOM, onde sempre estão esperando uma alteração para emitir um evento:

Loop convencional:

while (true) {
 InstanceManager.escutarComando(InputManager.carregarTeclado());
}
Enter fullscreen mode Exit fullscreen mode
  • InstanceManager: Seria o gerenciador responsável pela instância atual da aplicação e do que está sendo exibido. Nele poderíamos acessar a vida de um jogador, o tempo atual de jogo e os itens que estão no mapa, por exemplo.

  • InputManager: Seria o gerenciador responsável pela captura e emissão de dados que o sistema recebe, como por exemplo a entrada de dados do teclado/mouse.

DOM:

document.addEventListener('keydown', (event) => {
  dispararTeclado(event.keyCode); // ou event.key
});
Enter fullscreen mode Exit fullscreen mode
  • Com um Event Listener, conseguimos escutar eventos a qualquer momento e realizar o que queremos de forma separada, sem a necessidade de um loop fixo.

  • Nos exemplos iremos utilizar apenas JavaScript para melhor compreensão da semântica.

Tick

Você deve ter se perguntado: Se este loop não está bloqueando a entrada, o quão rápido ele gira? A Cada volta no loop o estado do jogo avança em alguma quantidade, chamamos isso de tick.

Podemos fazer um paralelo diretamente com FPS(Frames por Segundo). Por exemplo, se um jogo estiver renderizando 60 "fotos" por segundo, quer dizer que, no melhor dos casos, temos o nosso loop sendo percorrido a 60 ticks por segundo.

  • É importante ressaltar o frame-time, que é o tempo normalmente contado em milissegundos entre a demora de renderização de um frame a outro, normalmente não é 100% exato. Por exemplo, podemos ter um jogo em que o frametime é alto devido a placa de vídeo em não conseguir renderizar a tempo a qualidade das texturas, ou o processador não conseguir calcular a ação de todos os sprites a tempo. Normalmente game-engines possuem recursos para normalizar esse tipo de situação.

  • Se quiser entender um pouco mais afundo sobre frame-times, recomendo este fórum.

  • No JavaScript, faz mais sentido em utilizar setInterval() do que o while para executar estes loops, assim garantido um tickrate "esperado".

UI

Para o terror dos backend de plantão, "a regra de negócio dos jogos é feita no lado do cliente", e a interface é executada durante o tick, recebendo apenas as novas informações (se tiver) para renderizar alguma coisa.

As ferramentas gráficas modernas de construção de interfaces para jogos ficam "parados" até receber algum evento:

Loop tradicional:

while (true) {
  const evento = esperandoEvento();
  if(evento?.keyCode == 73) { // Se o evento retornar algo, verifica se a tecla 'I' do teclado foi pressionado
    InstanceManager.renderizarInventario(); // Irá renderizar o inventário pelo tick atual, se o usuário parar de pressionar, o inventário é fechado.
  }
}
Enter fullscreen mode Exit fullscreen mode

DOM:

document.addEventListener('keypress', (event) => {
  const inventario = document.getElementById(event.target.id);
  event.keyCode == 73 ? document.body.appendChild(inventario) : inventario?.remove(); 
  // Se a tecla 'I' for pressionada, adicionamos o inventário ao DOM, ao contrário, retiramos do DOM se ele estiver sendo renderizado.
});
Enter fullscreen mode Exit fullscreen mode
  • Se quisermos que o inventário seja renderizado até o usuário fechar manualmente, deixamos um booleano true quando keypress for acionado, e false quando keyup for acionado.

  • Os teclados possuem uma "trava" por padrão, esperando um tempo após o evento de pressionar para acionar outros eventos de 'keydown'. Podemos burlar da mesma forma do evento acima, usando um booleano. Em game-engines já temos este problema solucionado, agora para jogos na web as lib's Pixi.js e P5.js também possuem formas internas de evitar este problema.

  • Neste site, podemos encontrar o keyCode de cada tecla de uma maneira facilitada.

  • keyCodes não são padronizados, seja de navegador para navegador ou de sistema para sistema, recomendo fortemente em que procure a melhor forma de criar seu sistema de escuta.

  • É possível construir UI para jogos com HTML e CSS? Sim! Uma das grandes tecnologias do mercado e utilizada pelas empresas como Riot Games (VALORANT), Ubisoft (The Division), entre outras, é feito com HTML/CSS/C++. Por mais que seja uma tecnologia "á encomenda", temos a opção do Ultralight, também sendo HTML/CSS/C++.

Ciclo de Vida

Os loop's precisam possuir um padrão de código a ser executado, para evitar verificações dessincronizadas e a exibição de itens incorretamente. Para jogos em real-time, essa diferença pode parecer ser minúscula, mas para jogos por turno, por exemplo, pode causar de renderizar algo que sequer existe no contexto.

A seguir temos a forma padrão utilizada e que é recomendada:

Por Imagem:

Imagem exibindo o ciclo de vida de um jogo padrão

Por Código:

while (true) {
  entradaDeDados();
  atualizar();
  renderizar();
}
Enter fullscreen mode Exit fullscreen mode
  • entradaDeDados(): Aonde seria escutado a entrada de ações externas, como teclado e mouse. No DOM, não teríamos necessidade de colocar dentro de um loop, como mostrado no começo do artigo.

  • atualizar(): Aqui seria realizado todas as "regras de negócio" internas, como se o jogador levou dano, se um item foi adicionado no mapa, etc. Aqui também podemos realizar requisições HTTP e a serialização de dados.

  • renderizar(): irá buscar se possui novas informações, se não, continua renderizando as antigas informações.

ECS (Entity Component System)

Traduzido diretamente como Sistema de Componente e Entidade, é a forma que game-engines guardam e fornecem informações reativas para os nossos jogos. Na WEB são os Gerenciadores de Estado (Vuex do Vue, Redux/MobX do React), por seguirem a ideia de Pub/Sub.

Fluxo do Vuex:

Vuex

Fluxo de um ECS Genérico:

ECS

  • No JavaScript, temos um ECS simples chamado de Ensy, e um criado para React.js, mas longe de ser o ideal. Devido a isso, para jogos HTML5 se tem a necessidade de criar a própria logística interna. WebAssembly pode ser uma boa alternativa para conseguir utilizar um ECS robusto.

Informações Adicionais

  • Os Ciclos de Vida de um jogo padrão se comparado ao DOM pode mudar drasticamente dependendo da implementação desejada.

  • Se tratando de jogos HTML5, por mais que temos os WebWorkers, JavaScript continua sendo single-thread, limitando a forma que podemos tratar eventos e a quantidade de sprites a serem computadas.

  • Realizar requisições HTTP sem paralelismo pode ser um problema bem grande, principalmente se estiver no loop.

Parte 2 - PixiJS

Ainda não está disponível :(


Este artigo foi escrito por Giovane Cardoso. Twitter - Github

💖 💪 🙅 🚩
gcnovout
Giovane Cardoso

Posted on February 23, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related