Como construir um jogo de corredor sem fim em realidade virtual (Parte 3)

Publicados: 2022-03-10
Resumo rápido ↬ Na Parte 1, Alvin explicou o básico de como projetar um modelo de realidade virtual. Na Parte 2, ele mostrou como implementar a lógica central do jogo. Nesta parte final de seu tutorial, os toques finais serão adicionados, como os menus “Iniciar” e “Game Over”, bem como uma sincronização dos estados do jogo entre clientes móveis e desktop. Isso abre caminho para conceitos na construção de jogos multiplayer.

E assim nossa jornada continua. Nesta parte final da minha série sobre como construir um jogo de VR de corredor sem fim, mostrarei como você pode sincronizar o estado do jogo entre dois dispositivos, o que o aproximará um passo da construção de um jogo multiplayer. Vou apresentar especificamente o MirrorVR, que é responsável por lidar com o servidor mediador na comunicação cliente a cliente.

Nota : Este jogo pode ser jogado com ou sem um fone de ouvido VR. Você pode ver uma demonstração do produto final em ergo-3.glitch.me.

Para começar, você precisará do seguinte.

  • Acesso à Internet (especificamente para glitch.com);
  • Um projeto Glitch concluído da parte 2 deste tutorial. Você pode começar a partir do produto final da parte 2 navegando até https://glitch.com/edit/#!/ergo-2 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.)

Etapa 1: exibir a pontuação

O jogo como está funciona no mínimo, onde o jogador recebe um desafio: evitar os obstáculos. No entanto, fora das colisões de objetos, o jogo não fornece feedback ao jogador sobre o progresso no jogo. Para remediar isso, você implementará a exibição de pontuação nesta etapa. A partitura será um grande objeto de texto colocado em nosso mundo de realidade virtual, ao contrário de uma interface colada ao campo de visão do usuário.

Mais depois do salto! Continue lendo abaixo ↓

Na realidade virtual em geral, a interface do usuário é melhor integrada ao mundo do que presa à cabeça do usuário.

Exibição de pontuação
Exibição de pontuação (visualização grande)

Comece adicionando o objeto ao index.html . Adicione um mixin de text , que será reutilizado para outros elementos de texto:

 <a-assets> ... <a-mixin text=" font:exo2bold; anchor:center; align:center;"></a-mixin> ... </a-assets>

Em seguida, adicione um elemento de text à plataforma, logo antes do player:

 <!-- Score --> <a-text value="" mixin="text" height="40" width="40" position="0 1.2 -3" opacity="0.75"></a-text> <!-- Player --> ...

Isso adiciona uma entidade de texto à cena de realidade virtual. O texto não está visível no momento, porque seu valor está definido como vazio. No entanto, agora você preencherá a entidade de texto dinamicamente, usando JavaScript. Navegue até assets/ergo.js . Após a seção de collisions , adicione uma seção de score e defina várias variáveis ​​globais:

  • score : a pontuação atual do jogo.
  • countedTrees : IDs de todas as árvores que estão incluídas na pontuação. (Isso ocorre porque os testes de colisão podem ser acionados várias vezes para a mesma árvore.)
  • scoreDisplay : referência ao objeto DOM, correspondente a um objeto de texto no mundo da realidade virtual.
 /********* * SCORE * *********/ var score; var countedTrees; var scoreDisplay;

Em seguida, defina uma função de configuração para inicializar nossas variáveis ​​globais. Na mesma linha, defina uma função de teardown .

 ... var scoreDisplay; function setupScore() { score = 0; countedTrees = new Set(); scoreDisplay = document.getElementById('score'); } function teardownScore() { scoreDisplay.setAttribute('value', ''); }

Na seção Game , atualize gameOver , startGame e window.onload para incluir a configuração e a desmontagem da pontuação.

 /******** * GAME * ********/ function gameOver() { ... teardownScore(); } function startGame() { ... setupScore(); addTreesRandomlyLoop(); } window.onload = function() { setupScore(); ... }

Defina uma função que incremente a pontuação para uma árvore específica. Esta função verificará em countedTrees para garantir que a árvore não seja contada duas vezes.

 function addScoreForTree(tree_id) { if (countedTrees.has(tree_id)) return; score += 1; countedTrees.add(tree_id); }

Além disso, adicione um utilitário para atualizar a exibição da pontuação usando a variável global.

 function updateScoreDisplay() { scoreDisplay.setAttribute('value', score); }

Atualize o teste de colisão de acordo para invocar essa função de incremento de pontuação sempre que um obstáculo passar pelo jogador. Ainda em assets/ergo.js , navegue até a seção de collisions . Adicione a seguinte verificação e atualização.

 AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_LINE_END) { addScoreForTree(tree_id); updateScoreDisplay(); } }) } })

Por fim, atualize a exibição de pontuação assim que o jogo começar. Navegue até a seção Game e adicione updateScoreDisplay(); para startGame :

 function startGame() { ... setupScore(); updateScoreDisplay(); ... }

Certifique-se de que assets/ergo.js e index.html correspondam aos arquivos de código-fonte correspondentes. Em seguida, navegue até sua visualização. Você deve ver o seguinte:

Exibição de pontuação
Exibição de pontuação (visualização grande)

Isso conclui a exibição da pontuação. Em seguida, adicionaremos os menus de início e Game Over adequados, para que o jogador possa repetir o jogo conforme desejado.

Etapa 2: adicionar o menu Iniciar

Agora que o usuário pode acompanhar o progresso, você adicionará toques finais para completar a experiência do jogo. Nesta etapa, você adicionará um menu Iniciar e um menu Game Over , permitindo que o usuário inicie e reinicie os jogos.

Vamos começar com o menu Iniciar , onde o jogador clica no botão “Iniciar” para iniciar o jogo. Para a segunda metade desta etapa, você adicionará um menu Game Over , com um botão “Reiniciar”:

Menus de início e jogo
Menus de início e fim de jogo (visualização grande)

Navegue até index.html em seu editor. Em seguida, encontre a seção Mixins . Aqui, anexe o title mixin, que define estilos para textos particularmente grandes. Usamos a mesma fonte de antes, alinhamos o texto ao centro e definimos um tamanho apropriado para o tipo de texto. (Observe abaixo que a anchor é onde um objeto de texto é ancorado em sua posição.)

 <a-assets> ... <a-mixin text=" font:exo2bold; height:40; width:40; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>

Em seguida, adicione um segundo mixin para títulos secundários. Este texto é um pouco menor, mas é idêntico ao título.

 <a-assets> ... <a-mixin text=" font:exo2bold; height:10; width:10; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>

Para o terceiro e último mixin, defina as propriedades do texto descritivo — ainda menores que os títulos secundários.

 <a-assets> ... <a-mixin text=" font:exo2bold; height:5; width:5; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>

Com todos os estilos de texto definidos, você agora definirá os objetos de texto no mundo. Adicione uma nova seção de Menus abaixo da seção Score , com um contêiner vazio para o menu Iniciar :

 <!-- Score --> ... <!-- Menus --> <a-entity> <a-entity position="0 1.1 -3"> </a-entity> </a-entity>

Dentro do contêiner do menu iniciar, defina o título e um contêiner para todo o texto que não seja título:

 ... <a-entity ...> <a-entity position="0 1 0"> </a-entity> <a-text value="ERGO" mixin="title"></a-text> </a-entity> </a-entity>

Dentro do contêiner para texto sem título, adicione instruções para jogar o jogo:

 <a-entity...> <a-text value="Turn left and right to move your player, and avoid the trees!" mixin="copy"></a-text> </a-entity>

Para completar o menu Iniciar , adicione um botão que diz “Iniciar”:

 <a-entity...> ... <a-text value="Start" position="0 0.75 0" mixin="heading"></a-text> <a-box position="0 0.65 -0.05" width="1.5" height="0.6" depth="0.1"></a-box> </a-entity>

Verifique novamente se o código HTML do seu menu Iniciar corresponde ao seguinte:

 <!-- Menus --> <a-entity> <a-entity position="0 1.1 -3"> <a-entity position="0 1 0"> <a-text value="Turn left and right to move your player, and avoid the trees!" mixin="copy"></a-text> <a-text value="Start" position="0 0.75 0" mixin="heading"></a-text> <a-box position="0 0.65 -0.05" width="1.5" height="0.6" depth="0.1"></a-box> </a-entity> <a-text value="ERGO" mixin="title"></a-text> </a-entity> </a-entity>

Navegue até sua visualização e você verá o seguinte menu Iniciar :

Imagem do menu Iniciar
Menu Iniciar (visualização grande)

Ainda na seção Menus (diretamente abaixo do menu start ), adicione o menu game-over usando os mesmos mixins:

 <!-- Menus --> <a-entity> ... <a-entity position="0 1.1 -3"> <a-text value="?" mixin="heading" position="0 1.7 0"></a-text> <a-text value="Score" mixin="copy" position="0 1.2 0"></a-text> <a-entity> <a-text value="Restart" mixin="heading" position="0 0.7 0"></a-text> <a-box position="0 0.6 -0.05" width="2" height="0.6" depth="0.1"></a-box> </a-entity> <a-text value="Game Over" mixin="title"></a-text> </a-entity> </a-entity>

Navegue até seu arquivo JavaScript, assets/ergo.js . Crie uma nova seção Menus antes da seção Game . Além disso, defina três funções vazias: setupAllMenus , hideAllMenus e showGameOverMenu .

 /******** * MENU * ********/ function setupAllMenus() { } function hideAllMenus() { } function showGameOverMenu() { } /******** * GAME * ********/

Em seguida, atualize a seção Game em três lugares. No gameOver , mostre o menu Game Over :

 function gameOver() { ... showGameOverMenu(); } ``` In `startGame`, hide all menus: ``` function startGame() { ... hideAllMenus(); }

Em seguida, em window.onload , remova a invocação direta para startGame e chame setupAllMenus . Atualize seu ouvinte para corresponder ao seguinte:

 window.onload = function() { setupAllMenus(); setupScore(); setupTrees(); }

Navegue de volta para a seção Menu . Salve referências a vários objetos DOM:

 /******** * MENU * ********/ var menuStart; var menuGameOver; var menuContainer; var isGameRunning = false; var startButton; var restartButton; function setupAllMenus() { menuStart = document.getElementById('start-menu'); menuGameOver = document.getElementById('game-over'); menuContainer = document.getElementById('menu-container'); startButton = document.getElementById('start-button'); restartButton = document.getElementById('restart-button'); }

Em seguida, vincule os botões “Iniciar” e “Reiniciar” ao startGame :

 function setupAllMenus() { ... startButton.addEventListener('click', startGame); restartButton.addEventListener('click', startGame); }

Defina showStartMenu e invoque-o em setupAllMenus :

 function setupAllMenus() { ... showStartMenu(); } function hideAllMenus() { } function showGameOverMenu() { } function showStartMenu() { }

Para preencher as três funções vazias, você precisará de algumas funções auxiliares. Defina as duas funções a seguir, que aceitam um elemento DOM que representa uma entidade A-Frame VR e a exibe ou oculta. Defina ambas as funções acima showAllMenus :

 ... var restartButton; function hideEntity(el) { el.setAttribute('visible', false); } function showEntity(el) { el.setAttribute('visible', true); } function showAllMenus() { ...

Primeiro preencha hideAllMenus . Você removerá os objetos da vista e, em seguida, removerá os ouvintes de clique para ambos os menus:

 function hideAllMenus() { hideEntity(menuContainer); startButton.classList.remove('clickable'); restartButton.classList.remove('clickable'); }

Em segundo lugar, preencha showGameOverMenu . Aqui, restaure o contêiner para ambos os menus, bem como o menu Game Over e o ouvinte de clique do botão 'Reiniciar'. No entanto, remova o ouvinte de clique do botão 'Iniciar' e oculte o menu 'Iniciar'.

 function showGameOverMenu() { showEntity(menuContainer); hideEntity(menuStart); showEntity(menuGameOver); startButton.classList.remove('clickable'); restartButton.classList.add('clickable'); }

Terceiro, preencha showStartMenu . Aqui, reverta todas as alterações que o showGameOverMenu efetuou.

 function showStartMenu() { showEntity(menuContainer); hideEntity(menuGameOver); showEntity(menuStart); startButton.classList.add('clickable'); restartButton.classList.remove('clickable'); }

Verifique novamente se seu código corresponde aos arquivos de origem correspondentes. Em seguida, navegue até sua visualização e você observará o seguinte comportamento:

Menus de início e jogo
Menus de início e fim de jogo (visualização grande)

Isso conclui os menus Iniciar e Fim de Jogo .

Parabéns! Agora você tem um jogo totalmente funcional com um início e um fim adequados. No entanto, temos mais uma etapa neste tutorial: precisamos sincronizar o estado do jogo entre os diferentes dispositivos dos jogadores. Isso nos levará um passo mais perto dos jogos multiplayer.

Etapa 3: Sincronizando o estado do jogo com MirrorVR

Em um tutorial anterior, você aprendeu como enviar informações em tempo real pelos soquetes, para facilitar a comunicação unidirecional entre um servidor e um cliente. Nesta etapa, você desenvolverá um produto completo desse tutorial, MirrorVR, que lida com o servidor de mediação na comunicação cliente a cliente.

Nota : Você pode aprender mais sobre MirrorVR aqui.

Navegue até index.html . Aqui, carregaremos o MirrorVR e adicionaremos um componente à câmera, indicando que ele deve espelhar a visão de um dispositivo móvel, quando aplicável. Importe a dependência socket.io e MirrorVR 0.2.3.

 <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script> <script src="https://cdn.jsdelivr.net/gh/alvinwan/[email protected]/dist/mirrorvr.min.js"></script>

Em seguida, adicione um componente, camera-listener , à câmera:

 <a-camera camera-listener ...>

Navegue até assets/ergo.js . Nesta etapa, o dispositivo móvel enviará comandos e o dispositivo desktop espelhará apenas o dispositivo móvel.

Para facilitar isso, você precisa de um utilitário para distinguir entre dispositivos móveis e desktop. No final do seu arquivo, adicione uma função mobileCheck após shuffle :

 /** * Checks for mobile and tablet platforms. */ function mobileCheck() { var check = false; (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[aw])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); return check; };

Primeiro, vamos sincronizar o início do jogo. Em startGame , da seção Game , adicione uma notificação mirrorVR no final.

 function startGame() { ... if (mobileCheck()) { mirrorVR.notify('startGame', {}) } }

O cliente móvel agora envia notificações sobre o início de um jogo. Agora você implementará a resposta da área de trabalho.

No ouvinte de carregamento da janela, invoque uma função setupMirrorVR :

 window.onload = function() { ... setupMirrorVR(); }

Defina uma nova seção acima da seção Game para a configuração do MirrorVR:

 /************ * MirrorVR * ************/ function setupMirrorVR() { mirrorVR.init(); }

Em seguida, adicione argumentos de palavra-chave à função de inicialização para mirrorVR. Especificamente, definiremos o manipulador para notificações de início de jogo. Além disso, especificaremos um ID de quarto; isso garante que qualquer pessoa que carregue seu aplicativo seja sincronizada imediatamente.

 function setupMirrorVR() { mirrorVR.init({ roomId: 'ergo', state: { startGame: { onNotify: function(data) { hideAllMenus(); setupScore(); updateScoreDisplay(); } }, } }); }

Repita o mesmo processo de sincronização para Game Over . No gameOver na seção Game , adicione uma verificação para dispositivos móveis e envie uma notificação de acordo:

 function gameOver() { ... if (mobileCheck()) { mirrorVR.notify('gameOver', {}); } }

Navegue até a seção MirrorVR e atualize os argumentos de palavra-chave com um ouvinte gameOver :

 function setupMirrorVR() { mirrorVR.init({ state: { startGame: {... }, gameOver: { onNotify: function(data) { gameOver(); } }, } }) }

Em seguida, repita o mesmo processo de sincronização para a adição de árvores. Navegue até addTreesRandomly na seção Trees . Acompanhe quais pistas recebem novas árvores. Então, diretamente antes da diretiva de return , e envie uma notificação de acordo:

 function addTreesRandomly(...) { ... var numberOfTreesAdded ... var position_indices = []; trees.forEach(function (tree) { if (...) { ... position_indices.push(tree.position_index); } }); if (mobileCheck()) { mirrorVR.notify('addTrees', position_indices); } return ... }

Navegue até a seção MirrorVR e atualize os argumentos de palavra-chave para mirrorVR.init com um novo ouvinte para árvores:

 function setupMirrorVR() { mirrorVR.init({ state: { ... gameOver: {... }, addTrees: { onNotify: function(position_indices) { position_indices.forEach(addTreeTo) } }, } }) }

Por fim, sincronizamos a pontuação do jogo. Em updateScoreDisplay da seção Score , envie uma notificação quando aplicável:

 function updateScoreDisplay() { ... if (mobileCheck()) { mirrorVR.notify('score', score); } }

Atualize a inicialização do mirrorVR pela última vez, com um listener para alterações de pontuação:

 function setupMirrorVR() { mirrorVR.init({ state: { addTrees: { }, score: { onNotify: function(data) { score = data; updateScoreDisplay(); } } } }); }

Verifique novamente se seu código corresponde aos arquivos de código-fonte apropriados para esta etapa. Em seguida, navegue até a visualização da área de trabalho. Além disso, abra o mesmo URL em seu dispositivo móvel. Assim que seu dispositivo móvel carregar a página da web, sua área de trabalho deverá começar imediatamente a espelhar o jogo do dispositivo móvel.

Aqui está uma demonstração. Observe que o cursor da área de trabalho não está se movendo, indicando que o dispositivo móvel está controlando a visualização da área de trabalho.

Final Endless Runner Game com sincronização de estado do jogo MirrorVR
Resultado final do jogo de corredor sem fim com sincronização de estado do jogo MirrorVR (visualização grande)

Isso conclui seu projeto aumentado com mirrorVR.

Esta terceira etapa introduziu algumas etapas básicas de sincronização do estado do jogo; para tornar isso mais robusto, você pode adicionar mais verificações de sanidade e mais pontos de sincronização.

Conclusão

Neste tutorial, você adicionou toques finais ao seu jogo de corredor sem fim e implementou a sincronização em tempo real de um cliente de desktop com um cliente móvel, espelhando efetivamente a tela do dispositivo móvel em seu desktop. Isso conclui a série sobre a construção de um jogo de corrida sem fim em realidade virtual. Junto com as técnicas A-Frame VR, você aprendeu modelagem 3D, comunicação cliente a cliente e outros conceitos amplamente aplicáveis.

Os próximos passos podem incluir:

  • Modelagem mais avançada
    Isso significa modelos 3D mais realistas, potencialmente criados em um software de terceiros e importados. Por exemplo, (MagicaVoxel) simplifica a criação de arte voxel e (Blender) é uma solução completa de modelagem 3D.
  • Mais complexidade
    Jogos mais complexos, como um jogo de estratégia em tempo real, podem alavancar um mecanismo de terceiros para aumentar a eficiência. Isso pode significar evitar completamente o A-Frame e o webVR, em vez de publicar um jogo compilado (Unity3d).

Outras avenidas incluem suporte multiplayer e gráficos mais ricos. Com a conclusão desta série de tutoriais, agora você tem uma estrutura para explorar mais.