Cum să construiești un joc de alergători fără sfârșit în realitate virtuală (partea a 3-a)

Publicat: 2022-03-10
Rezumat rapid ↬ În partea 1, Alvin a explicat elementele de bază ale modului de proiectare a unui model de realitate virtuală. În partea 2, el a arătat cum să implementeze logica de bază a jocului. În această parte finală a tutorialului său, vor fi adăugate ultimele, cum ar fi meniurile „Start” și „Game Over”, precum și o sincronizare a stărilor jocului între clienții de pe mobil și desktop. Acest lucru deschide calea pentru concepte în construirea de jocuri multiplayer.

Și astfel călătoria noastră continuă. În această parte finală a seriei mele despre cum să construiești un joc VR cu alergători fără sfârșit, îți voi arăta cum poți sincroniza starea jocului între două dispozitive, ceea ce te va aduce cu un pas mai aproape de construirea unui joc multiplayer. Voi prezenta în mod special MirrorVR, care este responsabil pentru gestionarea serverului de mediere în comunicarea client-la-client.

Notă : Acest joc poate fi jucat cu sau fără căști VR. Puteți vizualiza o demonstrație a produsului final la ergo-3.glitch.me.

Pentru a începe, veți avea nevoie de următoarele.

  • acces la internet (în special la glitch.com);
  • Un proiect Glitch finalizat din partea 2 a acestui tutorial. Puteți începe de la produsul finit din partea 2 navigând la https://glitch.com/edit/#!/ergo-2 și făcând clic pe „Remix pentru a edita”;
  • Un set cu cască de realitate virtuală (opțional, recomandat). (Folosesc Google Cardboard, care este oferit la 15 USD bucata.)

Pasul 1: Afișează scorul

Jocul așa cum este funcționează la minimum, în care jucătorului i se oferă o provocare: evita obstacolele. Cu toate acestea, în afara coliziunilor de obiecte, jocul nu oferă feedback jucătorului cu privire la progresul în joc. Pentru a remedia acest lucru, veți implementa afișarea scorului în acest pas. Scorul va fi un obiect text mare plasat în lumea noastră de realitate virtuală, spre deosebire de o interfață lipită de câmpul vizual al utilizatorului.

Mai multe după săritură! Continuați să citiți mai jos ↓

În realitate virtuală, în general, interfața cu utilizatorul este cel mai bine integrată în lume, mai degrabă decât lipită de capul utilizatorului.

Afișare scor
Afișare scor (previzualizare mare)

Începeți prin a adăuga obiectul la index.html . Adăugați un mix de text , care va fi reutilizat pentru alte elemente de text:

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

Apoi, adăugați un element de text pe platformă, chiar înaintea jucătorului:

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

Aceasta adaugă o entitate text scenei de realitate virtuală. Textul nu este vizibil momentan, deoarece valoarea sa este setată la gol. Cu toate acestea, acum veți popula entitatea de text în mod dinamic, folosind JavaScript. Navigați la assets/ergo.js . După secțiunea de collisions , adăugați o secțiune de score și definiți un număr de variabile globale:

  • score : scorul curent al jocului.
  • countedTrees : ID-urile tuturor arborilor care sunt incluși în scor. (Acest lucru se datorează faptului că testele de coliziune pot declanșa de mai multe ori pentru același arbore.)
  • scoreDisplay : referință la obiectul DOM, corespunzătoare unui obiect text din lumea realității virtuale.
 /********* * SCORE * *********/ var score; var countedTrees; var scoreDisplay;

Apoi, definiți o funcție de configurare pentru a inițializa variabilele noastre globale. În același sens, definiți o funcție de teardown .

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

În secțiunea Game , actualizați gameOver , startGame și window.onload pentru a include configurarea scorului și demontarea.

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

Definiți o funcție care crește scorul pentru un anumit arbore. Această funcție va verifica față de countedTrees pentru a se asigura că arborele nu este dublu numărat.

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

În plus, adăugați un utilitar pentru a actualiza afișarea scorului folosind variabila globală.

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

Actualizați testarea de coliziune în consecință pentru a invoca această funcție de creștere a scorului ori de câte ori un obstacol a trecut de jucător. Încă în assets/ergo.js , navigați la secțiunea collisions . Adăugați următoarea verificare și actualizare.

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

În cele din urmă, actualizați afișarea scorului de îndată ce începe jocul. Navigați la secțiunea Game și adăugați updateScoreDisplay(); pentru a startGame :

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

Asigurați-vă că assets/ergo.js și index.html se potrivesc cu fișierele de cod sursă corespunzătoare. Apoi, navigați la previzualizare. Ar trebui să vedeți următoarele:

Afișare scor
Afișare scor (previzualizare mare)

Aceasta încheie afișarea scorului. În continuare, vom adăuga meniurile de start și Game Over adecvate, astfel încât jucătorul să poată reda jocul după cum dorește.

Pasul 2: Adăugați meniul Start

Acum că utilizatorul poate urmări progresul, veți adăuga retușuri finale pentru a finaliza experiența de joc. În acest pas, veți adăuga un meniu Start și un meniu Game Over , permițând utilizatorului să înceapă și să repornească jocurile.

Să începem cu meniul Start , unde jucătorul dă clic pe butonul „Start” pentru a începe jocul. Pentru a doua jumătate a acestui pas, veți adăuga un meniu Game Over , cu un buton „Repornire”:

Start și joc peste meniuri
Meniuri Start și joc peste (previzualizare mare)

Navigați la index.html în editorul dvs. Apoi, găsiți secțiunea Mixins . Aici, adăugați mixul de title , care definește stilurile pentru text deosebit de mare. Folosim același font ca înainte, aliniem textul la centru și definim o dimensiune adecvată tipului de text. (Rețineți de mai jos că anchor este locul în care un obiect text este ancorat la poziția sa.)

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

Apoi, adăugați un al doilea mixin pentru titlurile secundare. Acest text este puțin mai mic, dar este identic cu titlul.

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

Pentru a treia și ultima combinație, definiți proprietățile textului descriptiv - chiar mai mici decât titlurile secundare.

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

Cu toate stilurile de text definite, acum veți defini obiectele text din lume. Adăugați o nouă secțiune Menus sub secțiunea Score , cu un container gol pentru meniul Start :

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

În interiorul containerului meniului de pornire, definiți titlul și un container pentru tot textul fără titlu:

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

În interiorul containerului pentru text fără titlu, adăugați instrucțiuni pentru a juca jocul:

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

Pentru a finaliza meniul Start , adăugați un buton care scrie „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ți de două ori dacă codul HTML al meniului Start se potrivește cu următoarele:

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

Navigați la previzualizare și veți vedea următorul meniu Start :

Imagine a meniului Start
Meniul Start (Previzualizare mare)

Încă în secțiunea Menus (direct sub meniul de start ), adăugați meniul game-over folosind aceleași mixuri:

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

Navigați la fișierul JavaScript, assets/ergo.js . Creați o nouă secțiune Menus înainte de secțiunea Game . În plus, definiți trei funcții goale: setupAllMenus , hideAllMenus și showGameOverMenu .

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

Apoi, actualizați secțiunea Game în trei locuri. În gameOver , afișați meniul Game Over :

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

Apoi, în window.onload , eliminați invocarea directă pentru startGame și, în schimb, apelați setupAllMenus . Actualizați-vă ascultătorul pentru a se potrivi cu următoarele:

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

Navigați înapoi la secțiunea Menu . Salvați referințe la diferite obiecte 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'); }

Apoi, legați ambele butoane „Start” și „Repornire” la startGame :

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

Definiți showStartMenu și invocați-l din setupAllMenus :

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

Pentru a popula cele trei funcții goale, veți avea nevoie de câteva funcții de ajutor. Definiți următoarele două funcții, care acceptă un element DOM reprezentând o entitate VR A-Frame și îl arată sau ascunde. Definiți ambele funcții deasupra showAllMenus :

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

Mai întâi populați hideAllMenus . Veți elimina obiectele din vedere, apoi veți elimina ascultătorii de clic pentru ambele meniuri:

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

În al doilea rând, showGameOverMenu . Aici, restaurați containerul pentru ambele meniuri, precum și meniul Game Over și ascultatorul de clic al butonului „Repornire”. Cu toate acestea, eliminați ascultătorul de clic al butonului „Start” și ascundeți meniul „Start”.

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

În al treilea rând, showStartMenu . Aici, inversează toate modificările efectuate de showGameOverMenu .

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

Verificați de două ori dacă codul dvs. se potrivește cu fișierele sursă corespunzătoare. Apoi, navigați la previzualizare și veți observa următorul comportament:

Start și joc peste meniuri
Meniurile Start și Game Over (Previzualizare mare)

Astfel se încheie meniurile Start și Game Over .

Felicitări! Acum aveți un joc complet funcțional, cu un început și un sfârșit adecvat. Cu toate acestea, mai rămâne un pas în acest tutorial: trebuie să sincronizăm starea jocului între diferite dispozitive de jucător. Acest lucru ne va apropia cu un pas de jocurile multiplayer.

Pasul 3: Sincronizarea stării jocului cu MirrorVR

Într-un tutorial anterior, ați învățat cum să trimiteți informații în timp real peste socketuri, pentru a facilita comunicarea unidirecțională între un server și un client. În acest pas, veți construi pe un produs cu drepturi depline al acelui tutorial, MirrorVR, care se ocupă de serverul de mediere în comunicarea client-la-client.

Notă : Puteți afla mai multe despre MirrorVR aici.

Navigați la index.html . Aici, vom încărca MirrorVR și vom adăuga o componentă la cameră, indicând că aceasta ar trebui să oglindească vizualizarea unui dispozitiv mobil, acolo unde este cazul. Importați dependența socket.io și 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>

Apoi, adăugați o componentă, camera-listener , la cameră:

 <a-camera camera-listener ...>

Navigați la assets/ergo.js . În acest pas, dispozitivul mobil va trimite comenzi, iar dispozitivul desktop va oglindi doar dispozitivul mobil.

Pentru a facilita acest lucru, aveți nevoie de un utilitar care să facă distincția între dispozitivele desktop și cele mobile. La sfârșitul fișierului, adăugați o funcție mobileCheck după 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; };

Mai întâi, vom sincroniza începutul jocului. În startGame , din secțiunea Joc , adăugați o notificare mirrorVR la sfârșit.

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

Clientul mobil trimite acum notificări despre începerea unui joc. Acum veți implementa răspunsul desktopului.

În ascultatorul de încărcare a ferestrei, invocați o funcție setupMirrorVR :

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

Definiți o nouă secțiune deasupra secțiunii Game pentru configurarea MirrorVR:

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

Apoi, adăugați argumente de cuvinte cheie la funcția de inițializare pentru mirrorVR. Mai exact, vom defini handlerul pentru notificările de pornire a jocului. În plus, vom specifica un ID de cameră; acest lucru asigură că oricine vă încarcă aplicația este imediat sincronizat.

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

Repetați același proces de sincronizare pentru Game Over . În gameOver , în secțiunea Game , adăugați o verificare pentru dispozitivele mobile și trimiteți o notificare în consecință:

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

Navigați la secțiunea MirrorVR și actualizați argumentele cuvintelor cheie cu un ascultător gameOver :

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

Apoi, repetați același proces de sincronizare pentru adăugarea arborilor. Navigați la addTreesRandomly în secțiunea Trees . Urmăriți ce benzi primesc copaci noi. Apoi, direct înainte de directiva de return și trimiteți o notificare în consecință:

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

Navigați la secțiunea MirrorVR și actualizați argumentele cuvintelor cheie la mirrorVR.init cu un nou ascultător pentru arbori:

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

În cele din urmă, sincronizăm scorul jocului. În updateScoreDisplay din secțiunea Score , trimiteți o notificare atunci când este cazul:

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

Actualizați inițializarea mirrorVR pentru ultima dată, cu un ascultător pentru modificările scorului:

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

Verificați de două ori dacă codul dvs. se potrivește cu fișierele de cod sursă adecvate pentru acest pas. Apoi, navigați la previzualizarea desktopului. În plus, deschideți aceeași adresă URL pe dispozitivul dvs. mobil. De îndată ce dispozitivul dvs. mobil încarcă pagina web, desktopul ar trebui să înceapă imediat să reflecte jocul dispozitivului mobil.

Iată un demo. Observați că cursorul de pe desktop nu se mișcă, ceea ce indică faptul că dispozitivul mobil controlează previzualizarea desktopului.

Joc final Endless Runner cu sincronizarea stării jocului MirrorVR
Rezultatul final al jocului de alergători fără sfârșit cu sincronizarea stării jocului MirrorVR (previzualizare mare)

Aceasta încheie proiectul dvs. augmentat cu mirrorVR.

Acest al treilea pas a introdus câțiva pași de bază de sincronizare a stării jocului; pentru a face acest lucru mai robust, ați putea adăuga mai multe verificări de sănătate și mai multe puncte de sincronizare.

Concluzie

În acest tutorial, ați adăugat retușuri de ultimă oră jocului dvs. de alergători fără sfârșit și ați implementat sincronizarea în timp real a unui client desktop cu un client mobil, oglindind în mod eficient ecranul dispozitivului mobil pe desktop. Aceasta încheie seria despre construirea unui joc de alergători fără sfârșit în realitate virtuală. Alături de tehnicile A-Frame VR, ați preluat modelarea 3D, comunicarea de la client la client și alte concepte aplicabile pe scară largă.

Următorii pași pot include:

  • Modelare mai avansată
    Aceasta înseamnă modele 3D mai realiste, potențial create într-un software terță parte și importate. De exemplu, (MagicaVoxel) simplifică crearea artei voxel, iar (Blender) este o soluție completă de modelare 3D.
  • Mai multă complexitate
    Jocuri mai complexe, cum ar fi un joc de strategie în timp real, ar putea folosi un motor terță parte pentru o eficiență sporită. Acest lucru poate însemna ocolirea completă a A-Frame și webVR, în schimb publicarea unui joc compilat (Unity3d).

Alte căi includ suport multiplayer și grafică mai bogată. Odată cu încheierea acestei serii de tutoriale, aveți acum un cadru de explorat în continuare.