Come costruire un gioco di corridori senza fine nella realtà virtuale (parte 3)

Pubblicato: 2022-03-10
Riepilogo rapido ↬ Nella parte 1, Alvin ha spiegato le basi di come progettare un modello di realtà virtuale. Nella parte 2, ha mostrato come implementare la logica di base del gioco. In questa parte finale del suo tutorial, verranno aggiunti gli ultimi ritocchi come i menu "Start" e "Game Over", nonché una sincronizzazione degli stati di gioco tra client mobili e desktop. Questo apre la strada a concetti nella creazione di giochi multiplayer.

E così il nostro viaggio continua. In questa parte finale della mia serie su come costruire un gioco VR di corsa senza fine, ti mostrerò come sincronizzare lo stato del gioco tra due dispositivi che ti avvicineranno di un passo alla creazione di un gioco multiplayer. Introdurrò specificamente MirrorVR che è responsabile della gestione del server di mediazione nella comunicazione da client a client.

Nota : questo gioco può essere giocato con o senza un visore VR. È possibile visualizzare una demo del prodotto finale su ergo-3.glitch.me.

Per iniziare, avrai bisogno di quanto segue.

  • Accesso a Internet (in particolare a glitch.com);
  • Un progetto Glitch completato dalla parte 2 di questo tutorial. Puoi iniziare dal prodotto finito della parte 2 navigando su https://glitch.com/edit/#!/ergo-2 e facendo clic su "Remix per modificare";
  • Un visore per realtà virtuale (opzionale, consigliato). (Uso Google Cardboard, che viene offerto a $ 15 al pezzo.)

Passaggio 1: visualizza il punteggio

Il gioco così com'è funziona come minimo, in cui al giocatore viene data una sfida: evitare gli ostacoli. Tuttavia, al di fuori delle collisioni di oggetti, il gioco non fornisce feedback al giocatore in merito ai progressi nel gioco. Per rimediare, in questo passaggio implementerai la visualizzazione del punteggio. La partitura sarà un grande oggetto di testo posizionato nel nostro mondo di realtà virtuale, al contrario di un'interfaccia incollata al campo visivo dell'utente.

Altro dopo il salto! Continua a leggere sotto ↓

Nella realtà virtuale in generale, l'interfaccia utente è meglio integrata nel mondo piuttosto che attaccata alla testa dell'utente.

Visualizzazione del punteggio
Visualizzazione del punteggio (anteprima grande)

Inizia aggiungendo l'oggetto a index.html . Aggiungi un mixin di text , che verrà riutilizzato per altri elementi di testo:

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

Quindi, aggiungi un elemento di text alla piattaforma, subito prima del giocatore:

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

Questo aggiunge un'entità di testo alla scena della realtà virtuale. Il testo non è attualmente visibile, perché il suo valore è impostato su vuoto. Tuttavia, ora compilerai l'entità di testo in modo dinamico, utilizzando JavaScript. Passa a asset/ergo.js . Dopo la sezione delle collisions , aggiungi una sezione del score e definisci una serie di variabili globali:

  • score : il punteggio del gioco corrente.
  • countedTrees : ID di tutti gli alberi inclusi nel punteggio. (Questo perché i test di collisione possono attivarsi più volte per lo stesso albero.)
  • scoreDisplay : riferimento all'oggetto DOM, corrispondente a un oggetto di testo nel mondo della realtà virtuale.
 /********* * SCORE * *********/ var score; var countedTrees; var scoreDisplay;

Quindi, definisci una funzione di configurazione per inizializzare le nostre variabili globali. Allo stesso modo, definire una funzione di teardown .

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

Nella sezione Game , aggiorna gameOver , startGame e window.onload per includere l'impostazione del punteggio e lo smontaggio.

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

Definire una funzione che incrementi il ​​punteggio per un particolare albero. Questa funzione verificherà countedTrees per assicurarsi che l'albero non venga contato due volte.

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

Inoltre, aggiungi un'utilità per aggiornare la visualizzazione del punteggio utilizzando la variabile globale.

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

Aggiorna il test di collisione di conseguenza per invocare questa funzione di incremento del punteggio ogni volta che un ostacolo ha superato il giocatore. Sempre in assets/ergo.js , vai alla sezione collisions . Aggiungi il seguente controllo e aggiorna.

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

Infine, aggiorna la visualizzazione del punteggio non appena il gioco inizia. Passa alla sezione Game e aggiungi updateScoreDisplay(); per startGame il gioco:

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

Assicurati che asset/ergo.js e index.html corrispondano ai file del codice sorgente corrispondenti. Quindi, vai alla tua anteprima. Dovresti vedere quanto segue:

Visualizzazione del punteggio
Visualizzazione del punteggio (anteprima grande)

Questo conclude la visualizzazione del punteggio. Successivamente, aggiungeremo i menu di avvio e Game Over adeguati, in modo che il giocatore possa ripetere il gioco come desidera.

Passaggio 2: aggiungi il menu Start

Ora che l'utente può tenere traccia dei progressi, aggiungerai ritocchi finali per completare l'esperienza di gioco. In questo passaggio, aggiungerai un menu Start e un menu Game Over , consentendo all'utente di avviare e riavviare i giochi.

Iniziamo con il menu Start in cui il giocatore fa clic su un pulsante "Start" per iniziare il gioco. Per la seconda metà di questo passaggio, aggiungerai un menu Game Over , con un pulsante "Riavvia":

Menu di avvio e di gioco
Menu Start e Game Over (Anteprima grande)

Passa a index.html nel tuo editor. Quindi, trova la sezione Mixins . Qui aggiungi il title mixin, che definisce gli stili per testi particolarmente grandi. Usiamo lo stesso font di prima, allineiamo il testo al centro e definiamo una dimensione appropriata per il tipo di testo. (Nota sotto quell'ancora è dove un oggetto di testo è anchor alla sua posizione.)

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

Quindi, aggiungi un secondo mixin per le intestazioni secondarie. Questo testo è leggermente più piccolo ma per il resto è identico al titolo.

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

Per il terzo e ultimo mixin, definisci le proprietà per il testo descrittivo, anche più piccole delle intestazioni secondarie.

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

Con tutti gli stili di testo definiti, ora definirai gli oggetti di testo nel mondo. Aggiungi una nuova sezione Menus sotto la sezione Score , con un contenitore vuoto per il menu Start :

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

All'interno del contenitore del menu di avvio, definisci il titolo e un contenitore per tutto il testo senza titolo:

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

All'interno del contenitore per il testo senza titolo, aggiungi le istruzioni per il gioco:

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

Per completare il menu Start , aggiungi un pulsante che dice "Start":

 <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>

Verifica che il codice HTML del menu Start corrisponda a quanto segue:

 <!-- 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>

Passa alla tua anteprima e vedrai il seguente menu Start :

Immagine del menu Start
Menu Start (Anteprima grande)

Sempre nella sezione Menus (direttamente sotto il menu di start ), aggiungi il menu game-over usando gli stessi mixin:

 <!-- 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>

Passa al tuo file JavaScript, assets/ergo.js . Crea una nuova sezione Menus prima della sezione Game . Inoltre, definisci tre funzioni vuote: setupAllMenus , hideAllMenus e showGameOverMenu .

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

Quindi, aggiorna la sezione Game in tre punti. In gameOver , mostra il menu Game Over :

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

Quindi, in window.onload , rimuovi la chiamata diretta a startGame e chiama invece setupAllMenus . Aggiorna il tuo listener in modo che corrisponda a quanto segue:

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

Torna alla sezione Menu . Salva i riferimenti a vari oggetti 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'); }

Quindi, associa entrambi i pulsanti "Avvia" e "Riavvia" per startGame il gioco:

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

Definisci showStartMenu e richiamalo da setupAllMenus :

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

Per popolare le tre funzioni vuote, avrai bisogno di alcune funzioni di supporto. Definisci le due funzioni seguenti, che accetta un elemento DOM che rappresenta un'entità A-Frame VR e lo mostra o lo nasconde. Definisci entrambe le funzioni sopra showAllMenus :

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

Prima popola hideAllMenus . Rimuoverai gli oggetti dalla vista, quindi rimuoverai i listener di clic per entrambi i menu:

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

In secondo luogo, compila showGameOverMenu . Qui, ripristina il contenitore per entrambi i menu, così come il menu Game Over e l'ascoltatore di clic del pulsante "Riavvia". Tuttavia, rimuovi l'ascoltatore di clic del pulsante "Start" e nascondi il menu "Start".

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

Terzo, popolare showStartMenu . Qui, annulla tutte le modifiche apportate da showGameOverMenu .

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

Ricontrolla che il tuo codice corrisponda ai file di origine corrispondenti. Quindi, vai alla tua anteprima e osserverai il seguente comportamento:

Menu di avvio e di gioco
Menu Start e Game Over (Anteprima grande)

Questo conclude i menu Start e Game Over .

Congratulazioni! Ora hai un gioco perfettamente funzionante con un inizio e una fine corretti. Tuttavia, abbiamo ancora un passaggio in questo tutorial: dobbiamo sincronizzare lo stato del gioco tra diversi dispositivi del giocatore. Questo ci avvicinerà di un passo ai giochi multiplayer.

Passaggio 3: sincronizzazione dello stato del gioco con MirrorVR

In un tutorial precedente, hai imparato come inviare informazioni in tempo reale attraverso i socket, per facilitare la comunicazione unidirezionale tra un server e un client. In questo passaggio, costruirai un prodotto completo di quel tutorial, MirrorVR, che gestisce il server di mediazione nella comunicazione da client a client.

Nota : puoi saperne di più su MirrorVR qui.

Passa a index.html . Qui caricheremo MirrorVR e aggiungeremo un componente alla videocamera, indicando che dovrebbe rispecchiare la vista di un dispositivo mobile, ove applicabile. Importa la dipendenza 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>

Quindi, aggiungi un componente, camera-listener , alla fotocamera:

 <a-camera camera-listener ...>

Passa a asset/ergo.js . In questo passaggio, il dispositivo mobile invierà i comandi e il dispositivo desktop eseguirà il mirroring solo del dispositivo mobile.

Per facilitare ciò, è necessaria un'utilità per distinguere tra dispositivi desktop e mobili. Alla fine del file, aggiungi una funzione mobileCheck dopo la 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; };

Per prima cosa sincronizzeremo l'inizio del gioco. In startGame , della sezione Gioco , aggiungi una notifica mirrorVR alla fine.

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

Il client mobile ora invia notifiche sull'avvio di un gioco. A questo punto implementerai la risposta del desktop.

Nel listener di caricamento della finestra, invoca una funzione setupMirrorVR :

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

Definisci una nuova sezione sopra la sezione Game per la configurazione di MirrorVR:

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

Quindi, aggiungi gli argomenti delle parole chiave alla funzione di inizializzazione per mirrorVR. Nello specifico, definiremo il gestore per le notifiche di inizio del gioco. Indicheremo inoltre un ID camera; questo assicura che chiunque carichi la tua applicazione sia immediatamente sincronizzato.

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

Ripeti lo stesso processo di sincronizzazione per Game Over . In gameOver nella sezione Game , aggiungi un segno di spunta per i dispositivi mobili e invia una notifica di conseguenza:

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

Passa alla sezione MirrorVR e aggiorna gli argomenti delle parole chiave con un listener di gameOver :

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

Quindi, ripeti lo stesso processo di sincronizzazione per l'aggiunta di alberi. Passa a addTreesRandomly nella sezione Trees . Tieni traccia di quali corsie ricevono nuovi alberi. Quindi, direttamente prima della direttiva sul return , e invia una notifica di conseguenza:

 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 ... }

Passa alla sezione MirrorVR e aggiorna gli argomenti della parola chiave su mirrorVR.init con un nuovo listener per gli alberi:

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

Infine, sincronizziamo il punteggio del gioco. In updateScoreDisplay dalla sezione Score , invia una notifica quando applicabile:

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

Aggiorna l'inizializzazione mirrorVR per l'ultima volta, con un listener per le modifiche al punteggio:

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

Ricontrolla che il tuo codice corrisponda ai file di codice sorgente appropriati per questo passaggio. Quindi, vai all'anteprima del desktop. Inoltre, apri lo stesso URL sul tuo dispositivo mobile. Non appena il tuo dispositivo mobile carica la pagina web, il tuo desktop dovrebbe iniziare immediatamente a eseguire il mirroring del gioco del dispositivo mobile.

Ecco una demo. Si noti che il cursore del desktop non si sposta, a indicare che il dispositivo mobile sta controllando l'anteprima del desktop.

Final Endless Runner Game con sincronizzazione dello stato del gioco MirrorVR
Risultato finale del gioco del corridore senza fine con la sincronizzazione dello stato del gioco MirrorVR (anteprima grande)

Questo conclude il tuo progetto aumentato con mirrorVR.

Questo terzo passaggio ha introdotto alcuni passaggi di base per la sincronizzazione dello stato del gioco; per renderlo più robusto, potresti aggiungere più controlli di integrità e più punti di sincronizzazione.

Conclusione

In questo tutorial, hai aggiunto gli ultimi ritocchi al tuo gioco di corsa infinita e implementato la sincronizzazione in tempo reale di un client desktop con un client mobile, rispecchiando efficacemente lo schermo del dispositivo mobile sul desktop. Questo conclude la serie sulla costruzione di un gioco di corsa senza fine nella realtà virtuale. Insieme alle tecniche A-Frame VR, hai acquisito la modellazione 3D, la comunicazione da cliente a cliente e altri concetti ampiamente applicabili.

I passaggi successivi possono includere:

  • Modellazione più avanzata
    Ciò significa modelli 3D più realistici, potenzialmente creati in un software di terze parti e importati. Ad esempio, (MagicaVoxel) semplifica la creazione di voxel art e (Blender) è una soluzione di modellazione 3D completa.
  • Più complessità
    Giochi più complessi, come un gioco di strategia in tempo reale, potrebbero sfruttare un motore di terze parti per una maggiore efficienza. Ciò potrebbe significare aggirare completamente A-Frame e webVR, invece di pubblicare un gioco compilato (Unity3d).

Altre strade includono il supporto multiplayer e una grafica più ricca. Con la conclusione di questa serie di tutorial, ora hai un framework da esplorare ulteriormente.