Como construir um jogo de corredor sem fim em realidade virtual (parte 2)

Publicados: 2022-03-10
Resumo rápido ↬ Se você já se perguntou como são criados os jogos com suporte sem teclado para fones de ouvido VR, este tutorial explica exatamente o que você está procurando. Veja como você também pode dar vida a um jogo de realidade virtual básico e funcional.

Na Parte 1 desta série, vimos como um modelo de realidade virtual com efeitos de iluminação e animação pode ser criado. Nesta parte, implementaremos a lógica central do jogo e utilizaremos manipulações de ambiente A-Frame mais avançadas para construir a parte “jogo” deste aplicativo. No final, você terá um jogo de realidade virtual funcional com um verdadeiro desafio.

Este tutorial envolve várias etapas, incluindo (mas não se limitando a) detecção de colisão e mais conceitos de A-Frame, como mixins.

  • Demonstração do produto final

Pré-requisitos

Assim como no tutorial anterior, você precisará do seguinte:

  • Acesso à Internet (especificamente para glitch.com);
  • Um projeto Glitch concluído da parte 1. (Você pode continuar a partir do produto final navegando até https://glitch.com/edit/#!/ergo-1 e clicando em “Remixar para editar”;
  • Um fone de ouvido de realidade virtual (opcional, recomendado). (Eu uso o Google Cardboard, que é oferecido a US$ 15 por peça.)
Mais depois do salto! Continue lendo abaixo ↓

Etapa 1: projetando os obstáculos

Nesta etapa, você projeta as árvores que usaremos como obstáculos. Em seguida, você adicionará uma animação simples que move as árvores em direção ao jogador, como a seguir:

Árvores de modelo se movendo em direção ao jogador
Árvores de modelo se movendo em direção ao player (visualização grande)

Essas árvores servirão como modelos para os obstáculos que você gera durante o jogo. Para a parte final desta etapa, removeremos essas “árvores de modelo”.

Para começar, adicione vários mixins A-Frame diferentes. Mixins são conjuntos comumente usados ​​de propriedades de componentes. No nosso caso, todas as nossas árvores terão a mesma cor, altura, largura, profundidade etc. Em outras palavras, todas as suas árvores terão a mesma aparência e, portanto, usarão alguns mixins compartilhados.

Nota : Em nosso tutorial, seus únicos ativos serão mixins. Visite a página A-Frame Mixins para saber mais.

Em seu editor, navegue até index.html . Logo após o céu e antes das luzes, adicione uma nova entidade A-Frame para manter seus ativos:

 <a-sky...></a-sky> <!-- Mixins --> <a-assets> </a-assets> <!-- Lights --> ...

Em sua nova entidade a-assets , comece adicionando um mixin para sua folhagem. Este mixins define propriedades comuns para a folhagem da árvore de template. Resumindo, é uma pirâmide branca, de sombreado plano, para um efeito low poly.

 <a-assets> <a-mixin geometry=" primitive: cone; segments-height: 1; segments-radial:4; radius-bottom:0.3;" material="color:white;flat-shading: true;"></a-mixin> </a-assets>

Logo abaixo do mixin de folhagem, adicione um mixin para o tronco. Este tronco será um pequeno prisma retangular branco.

 <a-assets> ... <a-mixin geometry=" primitive: box; height:0.5; width:0.1; depth:0.1;" material="color:white;"></a-mixin> </a-assets>

Em seguida, adicione os objetos de árvore de modelo que usarão esses mixins. Ainda em index.html , role para baixo até a seção de plataformas. Logo antes da seção do jogador, adicione uma nova seção de árvore, com três entidades de árvore vazias:

 <a-entity ...> <!-- Trees --> <a-entity></a-entity> <a-entity></a-entity> <a-entity></a-entity> <!-- Player --> ...

Em seguida, reposicione, redimensione e adicione sombras às entidades de árvore.

 <!-- Trees --> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity>

Agora, preencha as entidades da árvore com um tronco e folhagem, usando os mixins que definimos anteriormente.

 <!-- Trees --> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity>

Navegue até sua visualização e agora você deve ver as seguintes árvores de modelo.

Árvores de modelo para obstáculos
Árvores de modelo para obstáculos (visualização grande)

Agora, anime as árvores de um local distante na plataforma em direção ao usuário. Como antes, use a tag a-animation :

 <!-- Trees --> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="0 0.6 -7" to="0 0.6 1.5" dur="5000"></a-animation> </a-entity> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="-0.5 0.55 -7" to="-0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="0.5 0.55 -7" to="0.5 0.55 1.5" dur="5000"></a-animation> </a-entity>

Certifique-se de que seu código corresponda ao seguinte.

 <a-entity...> <!-- Trees --> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="0 0.6 -7" to="0 0.6 1.5" dur="5000"></a-animation> </a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="-0.5 0.55 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="-0.5 0.55 -7" to="-0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0.5 0.55 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="0.5 0.55 -7" to="0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <!-- Player --> ...

Navegue até sua visualização e agora você verá as árvores se movendo em sua direção.

Árvores de modelo se movendo em direção ao jogador
Árvores de modelo se movendo em direção ao playerÁrvores de modelo se movendo em direção ao player (visualização grande)

Navegue de volta ao seu editor. Desta vez, selecione assets/ergo.js . Na seção do jogo, configure as árvores depois que a janela for carregada.

 /******** * GAME * ********/ ... window.onload = function() { setupTrees(); }

Abaixo dos controles, mas antes da seção Game, adicione uma nova seção TREES . Nesta seção, defina uma nova função setupTrees .

 /************ * CONTROLS * ************/ ... /********* * TREES * *********/ function setupTrees() { } /******** * GAME * ********/ ...

Na nova função setupTrees , obtenha referências aos objetos DOM da árvore de modelo e disponibilize as referências globalmente.

 /********* * TREES * *********/ var templateTreeLeft; var templateTreeCenter; var templateTreeRight; function setupTrees() { templateTreeLeft = document.getElementById('template-tree-left'); templateTreeCenter = document.getElementById('template-tree-center'); templateTreeRight = document.getElementById('template-tree-right'); }

Em seguida, defina um novo utilitário removeTree . Com este utilitário, você pode remover as árvores de modelo da cena. Abaixo da função setupTrees , defina seu novo utilitário.

 function setupTrees() { ... } function removeTree(tree) { tree.parentNode.removeChild(tree); }

De volta a setupTrees , use o novo utilitário para remover as árvores de modelo.

 function setupTrees() { ... removeTree(templateTreeLeft); removeTree(templateTreeRight); removeTree(templateTreeCenter); }

Certifique-se de que suas seções de árvore e jogo correspondam ao seguinte:

 /********* * TREES * *********/ var templateTreeLeft; var templateTreeCenter; var templateTreeRight; function setupTrees() { templateTreeLeft = document.getElementById('template-tree-left'); templateTreeCenter = document.getElementById('template-tree-center'); templateTreeRight = document.getElementById('template-tree-right'); removeTree(templateTreeLeft); removeTree(templateTreeRight); removeTree(templateTreeCenter); } function removeTree(tree) { tree.parentNode.removeChild(tree); } /******** * GAME * ********/ setupControls(); // TODO: AFRAME.registerComponent has to occur before window.onload? window.onload = function() { setupTrees(); }

Reabra sua visualização e suas árvores agora devem estar ausentes. A visualização deve corresponder ao nosso jogo no início deste tutorial.

Parte 1 produto acabado
Produto acabado da parte 1 (visualização grande)

Isso conclui o design da árvore do modelo.

Nesta etapa, cobrimos e usamos mixins A-Frame, que nos permitem simplificar o código definindo propriedades comuns. Além disso, aproveitamos a integração do A-Frame com o DOM para remover objetos da cena A-Frame VR.

Na próxima etapa, geraremos vários obstáculos e projetaremos um algoritmo simples para distribuir árvores entre diferentes faixas.

Etapa 2: Obstáculos de desova

Em um jogo de corrida sem fim, nosso objetivo é evitar obstáculos voando em nossa direção. Nesta implementação específica do jogo, usamos três pistas como é mais comum.

Ao contrário da maioria dos jogos de corredor sem fim, este jogo só suporta movimentos para a esquerda e para a direita . Isso impõe uma restrição ao nosso algoritmo para gerar obstáculos: não podemos ter três obstáculos em todas as três pistas, ao mesmo tempo, voando em nossa direção. Se isso ocorrer, o jogador terá zero chance de sobrevivência. Como resultado, nosso algoritmo de geração precisa acomodar essa restrição.

Nesta etapa, todas as nossas edições de código serão feitas em assets/ergo.js . O arquivo HTML permanecerá o mesmo. Navegue até a seção TREES de assets/ergo.js .

Para começar, adicionaremos utilitários para gerar árvores. Cada árvore precisará de um ID exclusivo, que definiremos ingenuamente como o número de árvores que existem quando a árvore é gerada. Comece rastreando o número de árvores em uma variável global.

 /********* * TREES * *********/ ... var numberOfTrees = 0; function setupTrees() { ...

Em seguida, inicializaremos uma referência ao elemento DOM do contêiner de árvore, ao qual nossa função spawn adicionará árvores. Ainda na seção TREES , adicione uma variável global e então faça a referência.

 ... var treeContainer; var numberOfTrees ... function setupTrees() { ... templateTreeRight = ... treeContainer = document.getElementById('tree-container'); removeTree(...); ... }

Usando o número de árvores e o contêiner da árvore, escreva uma nova função que gera árvores.

 function removeTree(tree) { ... } function addTree(el) { numberOfTrees += 1; el.id = 'tree-' + numberOfTrees; treeContainer.appendChild(el); } ...

Para facilitar o uso mais tarde, você criará uma segunda função que adiciona a árvore correta à pista correta. Para começar, defina um novo array de templates na seção TREES .

 var templates; var treeContainer; ... function setupTrees() { ... templates = [templateTreeLeft, templateTreeCenter, templateTreeRight]; removeTree(...); ... }

Usando esta matriz de modelos, adicione um utilitário que gera árvores em uma pista específica, dado um ID que representa a esquerda, o meio ou a direita.

 function function addTree(el) { ... } function addTreeTo(position_index) { var template = templates[position_index]; addTree(template.cloneNode(true)); }

Navegue até a visualização e abra o console do desenvolvedor. No console do desenvolvedor, invoque a função global addTreeTo .

 > addTreeTo(0); # spawns tree in left lane 
Invocando addTreeTo manualmente
Invoque addTreeTo manualmente (visualização grande)

Agora, você escreverá um algoritmo que gera árvores aleatoriamente:

  1. Escolha uma pista aleatoriamente (que ainda não foi escolhida, para este timestep);
  2. Gere uma árvore com alguma probabilidade;
  3. Se o número máximo de árvores foi gerado para este timestep, pare. Caso contrário, repita o passo 1.

Para efetuar esse algoritmo, vamos embaralhar a lista de modelos e processar um de cada vez. Comece definindo uma nova função, addTreesRandomly , que aceita vários argumentos de palavras-chave diferentes.

 function addTreeTo(position_index) { ... } /** * Add any number of trees across different lanes, randomly. **/ function addTreesRandomly( { probTreeLeft = 0.5, probTreeCenter = 0.5, probTreeRight = 0.5, maxNumberTrees = 2 } = {}) { }

Em sua nova função addTreesRandomly , defina uma lista de árvores de modelo e embaralhe a lista.

 function addTreesRandomly( ... ) { var trees = [ {probability: probTreeLeft, position_index: 0}, {probability: probTreeCenter, position_index: 1}, {probability: probTreeRight, position_index: 2}, ] shuffle(trees); }

Role para baixo até a parte inferior do arquivo e crie uma nova seção de utilitários, juntamente com um novo utilitário shuffle . Este utilitário irá embaralhar uma matriz no local.

 /******** * GAME * ********/ ... /************* * UTILITIES * *************/ /** * Shuffles array in place. * @param {Array} a items An array containing the items. */ function shuffle(a) { var j, x, i; for (i = a.length - 1; i > 0; i--) { j = Math.floor(Math.random() * (i + 1)); x = a[i]; a[i] = a[j]; a[j] = x; } return a; }

Navegue de volta para a função addTreesRandomly na seção Árvores. Adicione uma nova variável numberOfTreesAdded e percorra a lista de árvores definidas acima.

 function addTreesRandomly( ... ) { ... var numberOfTreesAdded = 0; trees.forEach(function (tree) { }); }

Na iteração sobre árvores, gere uma árvore apenas com alguma probabilidade e somente se o número de árvores adicionadas não exceder 2 . Atualize o loop for da seguinte maneira.

 function addTreesRandomly( ... ) { ... trees.forEach(function (tree) { if (Math.random() < tree.probability && numberOfTreesAdded < maxNumberTrees) { addTreeTo(tree.position_index); numberOfTreesAdded += 1; } }); }

Para concluir a função, retorne o número de árvores adicionadas.

 function addTreesRandomly( ... ) { ... return numberOfTreesAdded; }

Verifique se sua função addTreesRandomly corresponde ao seguinte.

 /** * Add any number of trees across different lanes, randomly. **/ function addTreesRandomly( { probTreeLeft = 0.5, probTreeCenter = 0.5, probTreeRight = 0.5, maxNumberTrees = 2 } = {}) { var trees = [ {probability: probTreeLeft, position_index: 0}, {probability: probTreeCenter, position_index: 1}, {probability: probTreeRight, position_index: 2}, ] shuffle(trees); var numberOfTreesAdded = 0; trees.forEach(function (tree) { if (Math.random() < tree.probability && numberOfTreesAdded < maxNumberTrees) { addTreeTo(tree.position_index); numberOfTreesAdded += 1; } }); return numberOfTreesAdded; }

Finalmente, para gerar árvores automaticamente, configure um cronômetro que acione a geração de árvores em intervalos regulares. Defina o cronômetro globalmente e adicione uma nova função de desmontagem para este cronômetro.

 /********* * TREES * *********/ ... var treeTimer; function setupTrees() { ... } function teardownTrees() { clearInterval(treeTimer); }

Em seguida, defina uma nova função que inicialize o cronômetro e salve o cronômetro na variável global definida anteriormente. O temporizador abaixo é executado a cada meio segundo.

 function addTreesRandomlyLoop({intervalLength = 500} = {}) { treeTimer = setInterval(addTreesRandomly, intervalLength); }

Por fim, inicie o cronômetro depois que a janela for carregada, na seção Jogo.

 /******** * GAME * ********/ ... window.onload = function() { ... addTreesRandomlyLoop(); }

Navegue até sua visualização e você verá árvores surgindo aleatoriamente. Observe que nunca há três árvores ao mesmo tempo.

Árvore desovando aleatoriamente
Árvore desovando aleatoriamente (visualização grande)

Isso conclui a etapa de obstáculos. Pegamos com sucesso várias árvores de modelos e geramos um número infinito de obstáculos a partir dos modelos. Nosso algoritmo de geração também respeita as restrições naturais do jogo para torná-lo jogável.

Na próxima etapa, vamos adicionar o teste de colisão.

Etapa 3: Teste de colisão

Nesta seção, implementaremos os testes de colisão entre os obstáculos e o jogador. Esses testes de colisão são mais simples do que os testes de colisão na maioria dos outros jogos; no entanto, o jogador só se move ao longo do eixo x, então sempre que uma árvore cruzar o eixo x, verifique se a pista da árvore é a mesma que a pista do jogador. Vamos implementar esta verificação simples para este jogo.

Navegue até index.html , até a seção TREES . Aqui, adicionaremos informações de pista a cada uma das árvores. Para cada uma das árvores, adicione data-tree-position-index= , conforme a seguir. Além disso, adicione class="tree" , para que possamos selecionar facilmente todas as árvores na linha:

 <a-entity data-tree-position-index="1" class="tree" ...> </a-entity> <a-entity data-tree-position-index="0" class="tree" ...> </a-entity> <a-entity data-tree-position-index="2" class="tree" ...> </a-entity>

Navegue até assets/ergo.js e invoque uma nova função setupCollisions na seção GAME . Além disso, defina uma nova variável global isGameRunning que indica se um jogo existente já está em execução ou não.

 /******** * GAME * ********/ var isGameRunning = false; setupControls(); setupCollision(); window.onload = function() { ...

Defina uma nova seção COLLISIONS logo após a seção TREES , mas antes da seção Game. Nesta seção, defina a função setupCollisions.

 /********* * TREES * *********/ ... /************** * COLLISIONS * **************/ const POSITION_Z_OUT_OF_SIGHT = 1; const POSITION_Z_LINE_START = 0.6; const POSITION_Z_LINE_END = 0.7; function setupCollision() { } /******** * GAME * ********/

Como antes, registraremos um componente AFRAME e usaremos o ouvinte de evento tick para executar o código a cada passo de tempo. Nesse caso, registraremos um componente com player e executaremos verificações em todas as árvores nesse listener:

 function setupCollisions() { AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { } } } }

No loop for , comece obtendo as informações relevantes da árvore:

 document.querySelectorAll('.tree').forEach(function(tree) { position = tree.getAttribute('position'); tree_position_index = tree.getAttribute('data-tree-position-index'); tree_id = tree.getAttribute('id'); }

Em seguida, ainda dentro do loop for , remova a árvore se estiver fora de vista, logo após extrair as propriedades da árvore:

 document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_OUT_OF_SIGHT) { removeTree(tree); } }

Em seguida, se não houver jogo em execução, não verifique se há colisão.

 document.querySelectorAll('.tree').forEach(function(tree) { if (!isGameRunning) return; }

Finalmente (ainda no loop for ), verifique se a árvore compartilha a mesma posição ao mesmo tempo com o jogador. Em caso afirmativo, chame uma função gameOver ainda a ser definida:

 document.querySelectorAll('.tree').forEach(function(tree) { ... if (POSITION_Z_LINE_START < position.z && position.z < POSITION_Z_LINE_END && tree_position_index == player_position_index) { gameOver(); } }

Verifique se sua função setupCollisions corresponde ao seguinte:

 function setupCollisions() { AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { position = tree.getAttribute('position'); tree_position_index = tree.getAttribute('data-tree-position-index'); tree_id = tree.getAttribute('id'); if (position.z > POSITION_Z_OUT_OF_SIGHT) { removeTree(tree); } if (!isGameRunning) return; if (POSITION_Z_LINE_START < position.z && position.z < POSITION_Z_LINE_END && tree_position_index == player_position_index) { gameOver(); } }) } }) }

Isso conclui a configuração de colisão. Agora, adicionaremos algumas sutilezas para abstrair as sequências startGame e gameOver . Navegue até a seção GAME . Atualize o bloco window.onload para corresponder ao seguinte, substituindo addTreesRandomlyLoop por uma função startGame ainda a ser definida.

 window.onload = function() { setupTrees(); startGame(); }

Abaixo das invocações da função de configuração, crie uma nova função startGame . Essa função inicializará a variável isGameRunning adequadamente e evitará chamadas redundantes.

 window.onload = function() { ... } function startGame() { if (isGameRunning) return; isGameRunning = true; addTreesRandomlyLoop(); }

Por fim, defina gameOver , que alertará um “Game Over!” mensagem por enquanto.

 function startGame() { ... } function gameOver() { isGameRunning = false; alert('Game Over!'); teardownTrees(); }

Isso conclui a seção de teste de colisão do jogo de corredor sem fim.

Nesta etapa, usamos novamente os componentes A-Frame e vários outros utilitários que adicionamos anteriormente. Além disso, reorganizamos e abstraímos adequadamente as funções do jogo; posteriormente, aumentaremos essas funções de jogo para obter uma experiência de jogo mais completa.

Conclusão

Na parte 1, adicionamos controles compatíveis com VR: Olhe para a esquerda para mover para a esquerda e para a direita para mover para a direita. Nesta segunda parte da série, mostrei como pode ser fácil construir um jogo de realidade virtual básico e funcional. Adicionamos a lógica do jogo, para que o corredor sem fim corresponda às suas expectativas: corra para sempre e faça uma série infinita de obstáculos perigosos voarem para o jogador. Até agora, você construiu um jogo funcional com suporte sem teclado para fones de ouvido de realidade virtual.

Aqui estão recursos adicionais para diferentes controles e fones de ouvido de RV:

  • A-Frame para fones de ouvido VR
    Uma pesquisa de navegadores e fones de ouvido compatíveis com o A-Frame VR.
  • A-Frame para controladores VR
    Como o A-Frame não suporta controladores, controladores 3DoF e controladores 6DoF, além de outras alternativas de interação.

Na próxima parte, adicionaremos alguns toques finais e sincronizaremos os estados do jogo , o que nos aproxima um passo dos jogos multiplayer.