Come costruire un gioco di realtà virtuale multigiocatore in tempo reale (parte 2)
Pubblicato: 2022-03-10In questa serie di tutorial, costruiremo un gioco di realtà virtuale multiplayer basato sul Web, in cui i giocatori dovranno collaborare per risolvere un enigma. Nella prima parte di questa serie, abbiamo progettato le sfere presenti nel gioco. In questa parte della serie, aggiungeremo meccaniche di gioco e configureremo protocolli di comunicazione tra coppie di giocatori.
La descrizione del gioco qui è estratta dalla prima parte della serie: Ogni coppia di giocatori riceve un anello di sfere. L'obiettivo è "accendere" tutte le sfere, dove una sfera è "accesa" se è elevata e luminosa. Una sfera è "spenta" se è più bassa e debole. Tuttavia, alcune sfere "dominanti" influiscono sui loro vicini: se cambia stato, anche i suoi vicini cambiano stato. Il giocatore 2 può controllare le sfere di numero pari e il giocatore 1 può controllare le sfere di numero dispari. Questo costringe entrambi i giocatori a collaborare per risolvere il puzzle.
Gli 8 passaggi di questo tutorial sono raggruppati in 3 sezioni:
- Popolamento dell'interfaccia utente (passaggi 1 e 2)
- Aggiungi meccaniche di gioco (passaggi da 3 a 5)
- Configurazione della comunicazione (punti da 6 a 8)
Questa parte si concluderà con una demo online completamente funzionante, alla quale chiunque potrà giocare. Utilizzerai A-Frame VR e diverse estensioni A-Frame.
Puoi trovare il codice sorgente completo qui.

1. Aggiungi indicatori visivi
Per iniziare, aggiungeremo indicatori visivi dell'ID di una sfera. Inserisci un nuovo elemento VR a-text
come primo figlio di #container-orb0
, su 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>
Le "dipendenze" di una sfera sono le sfere che attiverà, quando commutate: ad esempio, supponiamo che la sfera 1 abbia come dipendenze le sfere 2 e 3. Ciò significa che se la sfera 1 è attivata, anche le sfere 2 e 3 verranno commutate. Aggiungeremo indicatori visivi delle dipendenze, come segue, subito dopo .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>
Verifica che il tuo codice corrisponda al nostro codice sorgente per il passaggio 1. La tua sfera ora dovrebbe corrispondere a quanto segue:

Questo conclude gli indicatori visivi aggiuntivi di cui avremo bisogno. Successivamente, aggiungeremo dinamicamente sfere alla scena VR, utilizzando questa sfera modello.
2. Aggiungi sfere dinamicamente
In questo passaggio, aggiungeremo sfere in base a una specifica JSON-esque di un livello. Questo ci permette di specificare e generare facilmente nuovi livelli. Useremo la sfera dell'ultimo passaggio nella parte 1 come modello.
Per iniziare, importa jQuery, poiché ciò renderà più semplici le modifiche al DOM e quindi le modifiche alla scena VR. Subito dopo l'importazione di A-Frame, aggiungi quanto segue a L8:
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
Specificare un livello utilizzando una matrice. L'array conterrà valori letterali oggetto che codificano le "dipendenze" di ciascuna sfera. All'interno del tag <head>
, aggiungi la seguente configurazione di livello, :
<script> var orbs = [ {left: 1, right: 4}, {}, {on: true}, {}, {on: true} ]; </script>
Per ora, ogni sfera può avere solo una dipendenza a "destra" di essa e una a "sinistra" di essa. Immediatamente dopo aver dichiarato le orbs
sopra, aggiungi un gestore che verrà eseguito al caricamento della pagina. Questo gestore (1) duplicherà la sfera del modello e (2) rimuoverà la sfera del modello, utilizzando la configurazione di livello fornita:
$(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) {}
Quindi, compila la funzione di remove
, che rimuove semplicemente un elemento dalla scena VR, dato un selettore. Fortunatamente, A-Frame osserva le modifiche al DOM, quindi rimuovere l'elemento dal DOM è sufficiente per rimuoverlo dalla scena VR. Popolare la funzione di remove
come segue.
function remove(selector) { var el = document.querySelector(selector); el.parentNode.removeChild(el); }
Compila la funzione clickOrb
, che attiva semplicemente l'azione del clic su una sfera.
function clickOrb(i) { document.querySelector("#container-orb" + i).click(); }
Quindi, inizia a scrivere la funzione populateTemplate
. In questa funzione, inizia ottenendo il .container
. Questo contenitore per la sfera contiene inoltre gli indicatori visivi che abbiamo aggiunto nel passaggio precedente. Inoltre, dovremo modificare il comportamento al onclick
della sfera, in base alle sue dipendenze. Se esiste una dipendenza da sinistra, modifica sia l'indicatore visivo che il comportamento al onclick
per riflettere ciò; lo stesso vale per una dipendenza da destra:
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(); } }
Sempre nella funzione populateTemplate
, imposta correttamente l'ID dell'orb in tutti gli elementi dell'orb e del relativo contenitore.
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);
Sempre nella funzione populateTemplate
, imposta il comportamento al onclick
, imposta il seme casuale in modo che ogni globo sia visivamente diverso e, infine, imposta la posizione di rotazione del globo in base al suo ID.
container.attr('onclick', onclick); container.find('lp-sphere').attr('seed', i); template.attr('rotation', '0 ' + (360 / total * i) + ' 0');
A conclusione della funzione, restituire il template
con tutte le configurazioni di cui sopra.
return template;
All'interno del gestore del caricamento del documento e dopo aver rimosso il modello con remove('#template')
, attiva le sfere che erano inizialmente configurate per essere attive.
$(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); });
Questo conclude le modifiche Javascript. Successivamente, cambieremo le impostazioni predefinite del modello in quelle di una sfera "disattivata". Modificare la posizione e la scala per #container-orb0
come segue:
position="8 0.5 0" scale="0.5 0.5 0.5"
Quindi, cambia l'intensità per #light-orb0
su 0.
intensity="0"
Verifica che il tuo codice sorgente corrisponda al nostro codice sorgente per il passaggio 2.
La tua scena VR dovrebbe ora presentare 5 sfere, popolate dinamicamente. Una delle sfere dovrebbe inoltre avere indicatori visivi delle dipendenze, come di seguito:

Questo conclude la prima sezione nell'aggiunta dinamica di sfere. Nella prossima sezione, passeremo tre passaggi per aggiungere le meccaniche di gioco. In particolare, il giocatore sarà in grado di attivare o disattivare sfere specifiche a seconda dell'ID giocatore.
3. Aggiungi Stato terminale
In questo passaggio, aggiungeremo uno stato terminale. Se tutte le sfere vengono attivate correttamente, il giocatore vede una pagina di "vittoria". Per fare ciò, dovrai tenere traccia dello stato di tutte le sfere. Ogni volta che una sfera viene attivata o disattivata, dovremo aggiornare il nostro stato interno. Supponiamo che una funzione di supporto toggleOrb
aggiorni lo stato per noi. Richiama la funzione toggleOrb
ogni volta che un globo cambia stato: (1) aggiungi un listener di clic al gestore onload e (2) aggiungi un toggleOrb(i);
chiamata a clickOrb
. Infine, (3) definisci un toggleOrb
vuoto.
$(document).ready(function() { ... $('.orb').on('click', function() { var id = $(this).attr('data-id') toggleOrb(id); }); }); function toggleOrb(i) {} function clickOrb(i) { ... toggleOrb(i); }
Per semplicità, useremo la nostra configurazione di livello per indicare lo stato del gioco. Usa toggleOrb
per attivare lo stato on
per l'i-esimo globo. toggleOrb
può inoltre attivare uno stato terminale se tutte le sfere sono attive.
function toggleOrb(i) { orbs[i].on = !orbs[i].on; if (orbs.every(orb => orb.on)) console.log('Victory!'); }
Ricontrolla che il tuo codice corrisponda al nostro codice sorgente per il passaggio 3.
Questo conclude la modalità "giocatore singolo" per il gioco. A questo punto, hai un gioco di realtà virtuale completamente funzionante. Tuttavia, ora dovrai scrivere la componente multiplayer e incoraggiare la collaborazione tramite le meccaniche di gioco.

4. Crea oggetto giocatore
In questo passaggio creeremo un'astrazione per un giocatore con un ID giocatore. Questo ID giocatore verrà assegnato dal server in seguito.
Per ora, questa sarà semplicemente una variabile globale. Subito dopo aver definito le orbs
, definisci un ID giocatore:
var orbs = ... var current_player_id = 1;
Ricontrolla che il tuo codice corrisponda al nostro codice sorgente per il passaggio 4. Nel passaggio successivo, questo ID giocatore verrà quindi utilizzato per determinare quali sfere il giocatore può controllare.
5. Attiva/disattiva condizionalmente le sfere
In questo passaggio, modificheremo il comportamento di attivazione/disattivazione della sfera. In particolare, il giocatore 1 può controllare le sfere di numero dispari e il giocatore 2 può controllare le sfere di numero pari. Innanzitutto, implementa questa logica in entrambi i punti in cui le sfere cambiano stato:
$('.orb').on('click', function() { var id = ... if (!allowedToToggle(id)) return false; ... } ... function clickOrb(i) { if (!allowedToToggle(id)) return; ... }
In secondo luogo, definisci la funzione allowedToToggle
, subito dopo clickOrb
. Se il giocatore attuale è il giocatore 1, gli ID dispari restituiranno un valore di verità e quindi il giocatore 1 potrà controllare sfere dispari. Il contrario è vero per il giocatore 2. Tutti gli altri giocatori non possono controllare le sfere.
function allowedToToggle(id) { if (current_player_id == 1) { return id % 2; } else if (current_player_id == 2) { return !(id % 2); } return false; }
Ricontrolla che il tuo codice corrisponda al nostro codice sorgente per il passaggio 5. Per impostazione predefinita, il giocatore è il giocatore 1. Ciò significa che tu come giocatore 1 puoi controllare solo sfere con numero dispari nella tua anteprima. Questo conclude la sezione sulle meccaniche di gioco.
Nella prossima sezione, faciliteremo la comunicazione tra entrambi i giocatori tramite un server.
6. Imposta il server con WebSocket
In questo passaggio, configurerai un semplice server per (1) tenere traccia degli ID giocatore e (2) inoltrare i messaggi. Questi messaggi includeranno lo stato del gioco, in modo che i giocatori possano essere certi che ognuno veda ciò che vede l'altro.
Faremo riferimento al tuo precedente index.html
come codice sorgente lato client. Faremo riferimento al codice in questo passaggio come al codice sorgente lato server. Passa a glitch.com, fai clic su "nuovo progetto" in alto a destra e, nel menu a discesa, fai clic su "ciao-express".
Dal pannello di sinistra, seleziona "package.json" e aggiungi socket-io
alle dependencies
. Il tuo dizionario dependencies
dovrebbe ora corrispondere a quanto segue.
"dependencies": { "express": "^4.16.4", "socketio": "^1.0.0" },
Dal pannello di sinistra, seleziona "index.js" e sostituisci il contenuto di quel file con il seguente socket minimo.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); });
Quanto sopra imposta socket.io sulla porta 3000 per un'applicazione express di base. Quindi, definisci due variabili globali, una per mantenere l'elenco dei giocatori attivi e un'altra per mantenere l'ID giocatore non assegnato più piccolo.
/** * Maintain player IDs */ var playerIds = []; var smallestPlayerId = 1;
Quindi, definisci la funzione getPlayerId
, che genera un nuovo ID giocatore e contrassegna il nuovo ID giocatore come "preso" aggiungendolo all'array playerIds
. In particolare, la funzione contrassegna semplicemente il smallestPlayerId
e quindi aggiorna il smallestPlayerId
piccoloPlayerId cercando il successivo numero intero più piccolo non preso.
function getPlayerId() { var playerId = smallestPlayerId; playerIds.push(playerId); while (playerIds.includes(smallestPlayerId)) { smallestPlayerId++; } return playerId; }
Definisci la funzione removePlayer
, che aggiorna il smallestPlayerId
di conseguenza e libera il playerId
fornito in modo che un altro giocatore possa prendere quell'ID.
function removePlayer(playerId) { if (playerId < smallestPlayerId) { smallestPlayerId = playerId; } var index = playerIds.indexOf(playerId); playerIds.splice(index, 1); }
Infine, definisci una coppia di gestori di eventi socket che registrano nuovi giocatori e annullano la registrazione di giocatori disconnessi, usando la coppia di metodi sopra.
/** * 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); }); });
Ricontrolla che il tuo codice corrisponda al nostro codice sorgente per il passaggio 6. Questo conclude la registrazione e la cancellazione del giocatore di base. Ogni client può ora utilizzare l'ID giocatore generato dal server.
Nel passaggio successivo, modificheremo il client per ricevere e utilizzare l'ID giocatore emesso dal server.
7. Applicare l'ID giocatore
In questi prossimi due passaggi, completeremo una versione rudimentale dell'esperienza multiplayer. Per iniziare, integra l'assegnazione dell'ID giocatore lato client. In particolare, ogni client chiederà al server un ID giocatore. Torna al client-side index.html
su cui stavamo lavorando nei passaggi 4 e precedenti.
Importa socket.io
nella head
su L7:
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
Dopo il gestore del caricamento del documento, istanziare il socket ed emettere un evento newPlayer
. In risposta, il lato server genererà un nuovo ID giocatore utilizzando l'evento playerId
. Di seguito, usa l'URL per l'anteprima del tuo progetto Glitch invece di lightful.glitch.me
. Puoi utilizzare l'URL demo di seguito, ma le modifiche al codice che apporti ovviamente non verranno riflesse.
$(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); });
Verifica che il tuo codice corrisponda al nostro codice sorgente per il passaggio 7. Ora puoi caricare il tuo gioco su due diversi browser o schede per giocare su due lati di un gioco multiplayer. Il giocatore 1 sarà in grado di controllare le sfere di numero dispari e il giocatore 2 sarà in grado di controllare le sfere di numero pari.
Tuttavia, tieni presente che l'attivazione delle sfere per il giocatore 1 non influirà sullo stato delle sfere per il giocatore 2. Successivamente, dobbiamo sincronizzare gli stati di gioco.
8. Sincronizza lo stato del gioco
In questo passaggio, sincronizzeremo gli stati del gioco in modo che i giocatori 1 e 2 vedano gli stessi stati delle sfere. Se la sfera 1 è attiva per il giocatore 1, dovrebbe essere attiva anche per il giocatore 2. Sul lato client, annunceremo e ascolteremo gli interruttori delle sfere. Per annunciare, passeremo semplicemente l'ID della sfera che è stata attivata.
Prima di entrambe le chiamate toggleOrb
, aggiungi la seguente chiamata socket.emit
.
$(document).ready(function() { ... $('.orb').on('click', function() { ... socket.emit('toggleOrb', id); toggleOrb(id); }); }); ... function clickOrb(i) { ... socket.emit('toggleOrb', i); toggleOrb(i); }
Quindi, ascolta gli interruttori della sfera e attiva la sfera corrispondente. Direttamente sotto il listener di eventi socket playerId
, aggiungi un altro listener per l'evento toggleOrb
.
socket.on('toggleOrb', function(i) { document.querySelector("#container-orb" + i).click(); toggleOrb(i); });
Questo conclude le modifiche al codice lato client. Ricontrolla che il tuo codice corrisponda al nostro codice sorgente per il passaggio 8.
Il lato server ora deve ricevere e trasmettere l'ID orb attivato. In index.js
lato server, aggiungi il listener seguente. Questo listener deve essere posizionato direttamente sotto il listener di disconnect
della presa.
socket.on('toggleOrb', function(i) { socket.broadcast.emit('toggleOrb', i); });
Ricontrolla che il tuo codice corrisponda al nostro codice sorgente per il passaggio 8. Ora, il giocatore 1 caricato in una finestra e il giocatore 2 caricato in una seconda finestra vedranno entrambi lo stesso stato di gioco. Con ciò, hai completato un gioco di realtà virtuale multiplayer. I due giocatori, inoltre, devono collaborare per portare a termine l'obiettivo. Il prodotto finale corrisponderà a quanto segue.

Conclusione
Questo conclude il nostro tutorial sulla creazione di un gioco di realtà virtuale multiplayer. Nel processo, hai toccato una serie di argomenti, tra cui la modellazione 3D in A-Frame VR e le esperienze multiplayer in tempo reale utilizzando WebSocket.
Basandosi sui concetti che abbiamo toccato, come garantiresti un'esperienza più fluida per i due giocatori? Ciò potrebbe includere il controllo che lo stato del gioco sia sincronizzato e l'avviso all'utente in caso contrario. Puoi anche creare semplici indicatori visivi per lo stato del terminale e lo stato della connessione del giocatore.
Data la struttura che abbiamo stabilito e i concetti che abbiamo introdotto, ora hai gli strumenti per rispondere a queste domande e costruire molto di più.
Puoi trovare il codice sorgente completo qui.