Como construir um jogo de realidade virtual multijogador em tempo real (Parte 2)
Publicados: 2022-03-10Nesta série de tutoriais, construiremos um jogo de realidade virtual multijogador baseado na web, onde os jogadores precisarão colaborar para resolver um quebra-cabeça. Na primeira parte desta série, projetamos os orbes apresentados no jogo. Nesta parte da série, adicionaremos mecânicas de jogo e configuraremos protocolos de comunicação entre pares de jogadores.
A descrição do jogo aqui é extraída da primeira parte da série: Cada par de jogadores recebe um anel de orbes. O objetivo é “ligar” todos os orbes, onde um orbe está “ligado” se estiver elevado e brilhante. Um orbe está “desligado” se estiver mais baixo e escuro. No entanto, certos orbes “dominantes” afetam seus vizinhos: se ele mudar de estado, seus vizinhos também mudarão de estado. O jogador 2 pode controlar orbes de número par, e o jogador 1 pode controlar orbes de número ímpar. Isso força os dois jogadores a colaborar para resolver o quebra-cabeça.
As 8 etapas deste tutorial estão agrupadas em 3 seções:
- Preenchendo a interface do usuário (etapas 1 e 2)
- Adicionar mecânica de jogo (etapas 3 a 5)
- Comunicação de configuração (etapas 6 a 8)
Esta parte terminará com uma demonstração online totalmente funcional, para qualquer um jogar. Você usará A-Frame VR e várias extensões A-Frame.
Você pode encontrar o código-fonte finalizado aqui.
1. Adicione Indicadores Visuais
Para começar, adicionaremos indicadores visuais do ID de um orbe. Insira um novo elemento VR a-text
a como o primeiro filho de #container-orb0
, em L36.
<a-entity ...> <a-text class="orb-id" opacity="0.25" rotation="0 -90 0" value="4" color="#FFF" scale="3 3 3" position="0 -2 -0.25" material="side:double"></a-text> ... <a-entity position...> ... </a-entity> </a-entity>
As “dependências” de um orbe são os orbes que ele alternará, quando alternado: por exemplo, digamos que o orbe 1 tenha como dependências os orbes 2 e 3. Isso significa que se o orbe 1 for alternado, os orbes 2 e 3 também serão alternados. Adicionaremos indicadores visuais de dependências, como segue, diretamente após .animation-position
.
<a-animation class="animation-position" ... /> <a-text class="dep-right" opacity="0.25" rotation="0 -90 0" value="4" color="#FFF" scale="10 10 10" position="0 0 1" material="side:double" ></a-text> <a-text class="dep-left" opacity="0.25"rotation="0 -90 0" value="1" color="#FFF" scale="10 10 10" position="0 0 -3" material="side:double" ></a-text>
Verifique se seu código corresponde ao nosso código-fonte da Etapa 1. Seu orbe agora deve corresponder ao seguinte:
Isso conclui os indicadores visuais adicionais que precisaremos. Em seguida, adicionaremos orbes dinamicamente à cena VR, usando este orbe modelo.
2. Adicione orbes dinamicamente
Nesta etapa, adicionaremos orbs de acordo com uma especificação JSON de um nível. Isso nos permite especificar e gerar facilmente novos níveis. Usaremos o orbe da última etapa da parte 1 como modelo.
Para começar, importe o jQuery, pois isso facilitará as modificações do DOM e, portanto, as modificações na cena VR. Diretamente após a importação do A-Frame, adicione o seguinte a L8:
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
Especifique um nível usando uma matriz. A matriz conterá literais de objeto que codificam as “dependências” de cada orb. Dentro da tag <head>
, adicione a seguinte configuração de nível, :
<script> var orbs = [ {left: 1, right: 4}, {}, {on: true}, {}, {on: true} ]; </script>
Por enquanto, cada orbe só pode ter uma dependência à “direita” e uma à “esquerda”. Imediatamente após declarar orbs
acima, adicione um manipulador que será executado no carregamento da página. Esse manipulador (1) duplicará o orbe do modelo e (2) removerá o orbe do modelo, usando a configuração de nível fornecida:
$(document).ready(function() { function populateTemplate(orb, template, i, total) {} function remove(selector) {} for (var i=0; i < orbs.length; i++) { var orb = orbs[i]; var template = $('#template').clone(); template = populateTemplate(orb, template, i, orbs.length); $('#carousel').append(template); } remove('#template'); } function clickOrb(i) {}
Em seguida, preencha a função de remove
, que simplesmente remove um item da cena de RV, dado um seletor. Felizmente, o A-Frame observa alterações no DOM e, portanto, remover o item do DOM é suficiente para removê-lo da cena VR. Preencha a função de remove
da seguinte maneira.
function remove(selector) { var el = document.querySelector(selector); el.parentNode.removeChild(el); }
Preencha a função clickOrb
, que simplesmente aciona a ação de clique em um orbe.
function clickOrb(i) { document.querySelector("#container-orb" + i).click(); }
Em seguida, comece a escrever a função populateTemplate
. Nesta função, comece obtendo o .container
. Este contêiner para o orbe contém adicionalmente os indicadores visuais que adicionamos na etapa anterior. Além disso, precisaremos modificar o comportamento onclick
do orb, com base em suas dependências. Se existir uma dependência à esquerda, modifique o indicador visual e o comportamento ao onclick
para refletir isso; o mesmo vale para uma dependência à direita:
function populateTemplate(orb, template, i, total) { var container = template.find('.container'); var onclick = 'document.querySelector("#light-orb' + i + '").emit("switch");'; if (orb.left || orb.right) { if (orb.left) { onclick += 'clickOrb(' + orb.left + ');'; container.find('.dep-left').attr('value', orb.left); } if (orb.right) { onclick += 'clickOrb(' + orb.right + ');'; container.find('.dep-right').attr('value', orb.right); } } else { container.find('.dep-left').remove(); container.find('.dep-right').remove(); } }
Ainda na função populateTemplate
, defina o ID do orb corretamente em todos os elementos do orb e de seu contêiner.
container.find('.orb-id').attr('value', i); container.attr('id', 'container-orb' + i); template.find('.orb').attr('id', 'orb' + i); template.find('.light-orb').attr('id', 'light-orb' + i); template.find('.clickable').attr('data-id', i);
Ainda na função populateTemplate
, defina o comportamento onclick
, defina a semente aleatória para que cada orbe seja visualmente diferente e, por fim, defina a posição rotacional do orbe com base em seu ID.
container.attr('onclick', onclick); container.find('lp-sphere').attr('seed', i); template.attr('rotation', '0 ' + (360 / total * i) + ' 0');
Ao final da função, retorne o template
com todas as configurações acima.
return template;
Dentro do manipulador de carregamento de documentos e após remover o modelo com remove('#template')
, ative os orbs que foram configurados para serem ativados inicialmente.
$(document).ready(function() { ... setTimeout(function() { for (var i=0; i < orbs.length; i++) { var orb = orbs[i]; if (orb.on) { document.querySelector("#container-orb" + i).click(); } } }, 1000); });
Isso conclui as modificações do Javascript. Em seguida, alteraremos as configurações padrão do modelo para um orbe 'desligado'. Altere a posição e a escala de #container-orb0
para o seguinte:
position="8 0.5 0" scale="0.5 0.5 0.5"
Em seguida, altere a intensidade de #light-orb0
para 0.
intensity="0"
Verifique se seu código-fonte corresponde ao nosso código-fonte da Etapa 2.
Sua cena de RV agora deve apresentar 5 orbes, preenchidos dinamicamente. Além disso, um dos orbs deve ter indicadores visuais de dependências, como abaixo:
Isso conclui a primeira seção sobre como adicionar orbes dinamicamente. Na próxima seção, passaremos três etapas adicionando mecânicas de jogo. Especificamente, o jogador só poderá alternar orbes específicos dependendo do ID do jogador.
3. Adicionar Estado Terminal
Nesta etapa, adicionaremos um estado terminal. Se todos os orbes forem ativados com sucesso, o jogador verá uma página de “vitória”. Para fazer isso, você precisará rastrear o estado de todos os orbes. Toda vez que um orbe é ativado ou desativado, precisaremos atualizar nosso estado interno. Digamos que uma função auxiliar toggleOrb
atualize o estado para nós. Invoque a função toggleOrb
sempre que um orb mudar de estado: (1) adicione um ouvinte de clique ao manipulador onload e (2) adicione um toggleOrb(i);
invocação para clickOrb
. Finalmente, (3) defina um toggleOrb
vazio.
$(document).ready(function() { ... $('.orb').on('click', function() { var id = $(this).attr('data-id') toggleOrb(id); }); }); function toggleOrb(i) {} function clickOrb(i) { ... toggleOrb(i); }
Para simplificar, usaremos nossa configuração de nível para indicar o estado do jogo. Use toggleOrb
para alternar o estado on
para o i-ésimo orbe. toggleOrb
também pode acionar um estado terminal se todos os orbes estiverem ativados.
function toggleOrb(i) { orbs[i].on = !orbs[i].on; if (orbs.every(orb => orb.on)) console.log('Victory!'); }
Verifique novamente se seu código corresponde ao nosso código-fonte para a Etapa 3.
Isso conclui o modo “single-player” para o jogo. Neste ponto, você tem um jogo de realidade virtual totalmente funcional. No entanto, agora você precisará escrever o componente multiplayer e incentivar a colaboração por meio da mecânica do jogo.
4. Criar Objeto de Jogador
Nesta etapa, criaremos uma abstração para um jogador com um ID de jogador. Este ID de jogador será atribuído pelo servidor mais tarde.
Por enquanto, isso será simplesmente uma variável global. Diretamente após definir orbs
, defina um ID de jogador:
var orbs = ... var current_player_id = 1;
Verifique se seu código corresponde ao nosso código-fonte para a Etapa 4. Na próxima etapa, esse ID do jogador será usado para determinar quais orbes o jogador pode controlar.
5. Alternar Orbes Condicionalmente
Nesta etapa, modificaremos o comportamento de alternância de orbes. Especificamente, o jogador 1 pode controlar orbes de número ímpar e o jogador 2 pode controlar orbes de número par. Primeiro, implemente essa lógica em ambos os lugares onde os orbes mudam de estado:
$('.orb').on('click', function() { var id = ... if (!allowedToToggle(id)) return false; ... } ... function clickOrb(i) { if (!allowedToToggle(id)) return; ... }
Segundo, defina a função allowedToToggle
, logo após clickOrb
. Se o jogador atual for o jogador 1, os ids de número ímpar retornarão um valor de verdade y e, portanto, o jogador 1 poderá controlar orbes de número ímpar. O inverso é verdadeiro para o jogador 2. Todos os outros jogadores não podem controlar os orbes.
function allowedToToggle(id) { if (current_player_id == 1) { return id % 2; } else if (current_player_id == 2) { return !(id % 2); } return false; }
Verifique se o seu código corresponde ao nosso código-fonte para a Etapa 5. Por padrão, o jogador é o jogador 1. Isso significa que você, como jogador 1, só pode controlar orbes ímpares em sua visualização. Isso conclui a seção sobre mecânica de jogo.
Na próxima seção, facilitaremos a comunicação entre os dois jogadores por meio de um servidor.
6. Configurar Servidor com WebSocket
Nesta etapa, você configurará um servidor simples para (1) acompanhar os IDs dos jogadores e (2) retransmitir mensagens. Essas mensagens incluirão o estado do jogo, para que os jogadores possam ter certeza de que cada um vê o que o outro vê.
Vamos nos referir ao seu index.html
anterior como o código-fonte do lado do cliente. Vamos nos referir ao código nesta etapa como o código-fonte do lado do servidor. Navegue até glitch.com, clique em “novo projeto” no canto superior direito e, no menu suspenso, clique em “hello-express”.
No painel esquerdo, selecione “package.json” e adicione socket-io
às dependencies
. Seu dicionário de dependencies
agora deve corresponder ao seguinte.
"dependencies": { "express": "^4.16.4", "socketio": "^1.0.0" },
No painel esquerdo, selecione “index.js” e substitua o conteúdo desse arquivo pelo seguinte socket.io Hello World:
const express = require("express"); const app = express(); var http = require('http').Server(app); var io = require('socket.io')(http); /** * Run application on port 3000 */ var port = process.env.PORT || 3000; http.listen(port, function(){ console.log('listening on *:', port); });
O acima configura o socket.io na porta 3000 para um aplicativo expresso básico. Em seguida, defina duas variáveis globais, uma para manter a lista de jogadores ativos e outra para manter o menor ID de jogador não atribuído.
/** * Maintain player IDs */ var playerIds = []; var smallestPlayerId = 1;
Em seguida, defina a função getPlayerId
, que gera um novo ID do jogador e marca o novo ID do jogador como “obtido”, adicionando-o ao array playerIds
. Em particular, a função simplesmente marca o smallestPlayerId
e, em seguida, atualiza o smallestPlayerId
pesquisando o próximo menor inteiro não obtido.
function getPlayerId() { var playerId = smallestPlayerId; playerIds.push(playerId); while (playerIds.includes(smallestPlayerId)) { smallestPlayerId++; } return playerId; }
Defina a função removePlayer
, que atualiza o smallestPlayerId
de acordo e libera o playerId
fornecido para que outro jogador possa usar esse ID.
function removePlayer(playerId) { if (playerId < smallestPlayerId) { smallestPlayerId = playerId; } var index = playerIds.indexOf(playerId); playerIds.splice(index, 1); }
Finalmente, defina um par de manipuladores de eventos de soquete que registram novos players e cancelam o registro de players desconectados, usando o par de métodos acima.
/** * Handle socket interactions */ io.on('connection', function(socket) { socket.on('newPlayer', function() { socket.playerId = getPlayerId(); console.log("new player: ", socket.playerId); socket.emit('playerId', socket.playerId); }); socket.on('disconnect', function() { if (socket.playerId === undefined) return; console.log("disconnected player: ", socket.playerId); removePlayer(socket.playerId); }); });
Verifique novamente se o seu código corresponde ao nosso código-fonte para a Etapa 6. Isso conclui o registro e o cancelamento do registro básico do jogador. Cada cliente agora pode usar o ID do jogador gerado pelo servidor.
Na próxima etapa, modificaremos o cliente para receber e usar o ID do jogador emitido pelo servidor.
7. Aplicar ID do jogador
Nestas próximas duas etapas, completaremos uma versão rudimentar da experiência multiplayer. Para começar, integre a atribuição do ID do jogador no lado do cliente. Em particular, cada cliente solicitará ao servidor um ID de jogador. Navegue de volta para o index.html
do lado do cliente em que estávamos trabalhando nas Etapas 4 e anteriores.
Importe socket.io
na head
em L7:
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
Após o manipulador de carregamento do documento, instancie o soquete e emita um evento newPlayer
. Em resposta, o lado do servidor gerará um novo ID de jogador usando o evento playerId
. Abaixo, use o URL para a visualização do seu projeto Glitch em vez de lightful.glitch.me
. Você pode usar o URL de demonstração abaixo, mas é claro que quaisquer alterações de código que você fizer não serão refletidas.
$(document).ready(function() { ... }); socket = io("https://lightful.glitch.me"); socket.emit('newPlayer'); socket.on('playerId', function(player_id) { current_player_id = player_id; console.log(" * You are now player", current_player_id); });
Verifique se seu código corresponde ao nosso código-fonte da Etapa 7. Agora, você pode carregar seu jogo em dois navegadores ou guias diferentes para jogar os dois lados de um jogo multijogador. O jogador 1 poderá controlar orbes de número ímpar e o jogador 2 poderá controlar orbes de número par.
No entanto, observe que alternar os orbes para o jogador 1 não afetará o estado do orbe para o jogador 2. Em seguida, precisamos sincronizar os estados do jogo.
8. Sincronize o estado do jogo
Nesta etapa, sincronizaremos os estados do jogo para que os jogadores 1 e 2 vejam os mesmos estados de orbe. Se o orbe 1 estiver ativado para o jogador 1, também deve estar ativado para o jogador 2. No lado do cliente, anunciaremos e ouviremos as alternâncias de orbes. Para anunciar, vamos simplesmente passar o ID do orbe que está alternado.
Antes de ambas as invocações toggleOrb
, adicione a seguinte chamada socket.emit
.
$(document).ready(function() { ... $('.orb').on('click', function() { ... socket.emit('toggleOrb', id); toggleOrb(id); }); }); ... function clickOrb(i) { ... socket.emit('toggleOrb', i); toggleOrb(i); }
Em seguida, ouça as alternâncias de orbe e alterne o orbe correspondente. Diretamente abaixo do ouvinte do evento de soquete playerId
, adicione outro ouvinte para o evento toggleOrb
.
socket.on('toggleOrb', function(i) { document.querySelector("#container-orb" + i).click(); toggleOrb(i); });
Isso conclui as modificações no código do lado do cliente. Verifique novamente se seu código corresponde ao nosso código-fonte para a Etapa 8.
O lado do servidor agora precisa receber e transmitir o ID do orb alternado. No index.js
do lado do servidor, adicione o seguinte listener. Este ouvinte deve ser colocado diretamente abaixo do ouvinte de disconnect
do soquete.
socket.on('toggleOrb', function(i) { socket.broadcast.emit('toggleOrb', i); });
Verifique novamente se seu código corresponde ao nosso código-fonte para a Etapa 8. Agora, o jogador 1 carregado em uma janela e o jogador 2 carregado em uma segunda janela verão o mesmo estado de jogo. Com isso, você completou um jogo de realidade virtual multiplayer. Os dois jogadores, além disso, devem colaborar para completar o objetivo. O produto final corresponderá ao seguinte.
Conclusão
Isso conclui nosso tutorial sobre como criar um jogo multijogador de realidade virtual. No processo, você abordou vários tópicos, incluindo modelagem 3-D em A-Frame VR e experiências multijogador em tempo real usando WebSockets.
Com base nos conceitos que abordamos, como você garantiria uma experiência mais suave para os dois jogadores? Isso pode incluir verificar se o estado do jogo está sincronizado e alertar o usuário caso contrário. Você também pode criar indicadores visuais simples para o estado do terminal e o status da conexão do player.
Dada a estrutura que estabelecemos e os conceitos que introduzimos, agora você tem as ferramentas para responder a essas perguntas e construir muito mais.
Você pode encontrar o código-fonte finalizado aqui.