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

Publicat: 2022-03-10
Rezumat rapid ↬ Dacă v-ați întrebat vreodată cum sunt create jocurile cu suport fără tastatură pentru căști VR, atunci acest tutorial explică exact ceea ce căutați. Iată cum și tu poți da viață unui joc VR de bază, funcțional.

În partea 1 a acestei serii, am văzut cum poate fi creat un model de realitate virtuală cu efecte de iluminare și animație. În această parte, vom implementa logica de bază a jocului și vom folosi manipulări mai avansate ale mediului A-Frame pentru a construi partea „joc” a acestei aplicații. Până la sfârșit, vei avea un joc de realitate virtuală funcțional, cu o adevărată provocare.

Acest tutorial implică o serie de pași, inclusiv (dar fără a se limita la) detectarea coliziunilor și mai multe concepte A-Frame, cum ar fi mixin-urile.

  • Demo a produsului final

Cerințe preliminare

La fel ca în tutorialul anterior, veți avea nevoie de următoarele:

  • acces la internet (în special la glitch.com);
  • Un proiect Glitch finalizat din partea 1. (Puteți continua de la produsul finit navigând la https://glitch.com/edit/#!/ergo-1 ș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.)
Mai multe după săritură! Continuați să citiți mai jos ↓

Pasul 1: Proiectarea obstacolelor

În acest pas, proiectați copacii pe care îi vom folosi ca obstacole. Apoi, veți adăuga o animație simplă care mută copacii către player, ca următorul:

Arborele șablon se deplasează către jucător
Arborele șablon se deplasează către jucător (previzualizare mare)

Acești copaci vor servi drept șabloane pentru obstacolele pe care le generați în timpul jocului. Pentru ultima parte a acestui pas, vom elimina apoi acești „arbori șablon”.

Pentru a începe, adăugați un număr de mixuri A-Frame diferite. Mixinele sunt seturi de proprietăți ale componentelor utilizate în mod obișnuit. În cazul nostru, toți copacii noștri vor avea aceeași culoare, înălțime, lățime, adâncime etc. Cu alte cuvinte, toți copacii tăi vor arăta la fel și, prin urmare, vor folosi câteva mixuri comune.

Notă : în tutorialul nostru, singurele dvs. active vor fi mixin-urile. Vizitați pagina A-Frame Mixins pentru a afla mai multe.

În editorul dvs., navigați la index.html . Imediat după cer și înainte de lumini, adăugați o nouă entitate A-Frame pentru a vă păstra activele:

 <a-sky...></a-sky> <!-- Mixins --> <a-assets> </a-assets> <!-- Lights --> ...

În noua dvs. entitate a-assets , începeți prin a adăuga un mixin pentru frunziș. Acest mixin definește proprietăți comune pentru frunzișul arborelui șablon. Pe scurt, este o piramidă albă, cu umbrire plată, pentru un efect low poly.

 <a-assets> <a-mixin geometry=" primitive: cone; segments-height: 1; segments-radial:4; radius-bottom:0.3;" material="color:white;flat-shading: true;"></a-mixin> </a-assets>

Chiar sub amestecul de frunze, adăugați un mixin pentru trunchi. Acest trunchi va fi o prismă dreptunghiulară mică, albă.

 <a-assets> ... <a-mixin geometry=" primitive: box; height:0.5; width:0.1; depth:0.1;" material="color:white;"></a-mixin> </a-assets>

Apoi, adăugați obiectele arborelui șablon care vor folosi aceste mixuri. Încă în index.html , derulați în jos la secțiunea platforme. Chiar înainte de secțiunea de jucător, adăugați o nouă secțiune de arbore, cu trei entități de arbore goale:

 <a-entity ...> <!-- Trees --> <a-entity></a-entity> <a-entity></a-entity> <a-entity></a-entity> <!-- Player --> ...

Apoi, repoziționați, redimensionați și adăugați umbre entităților arborescente.

 <!-- Trees --> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"></a-entity>

Acum, populați entitățile arborelui cu un trunchi și frunziș, folosind mixin-urile pe care le-am definit anterior.

 <!-- Trees --> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity> <a-entity ...> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> </a-entity>

Navigați la previzualizare și acum ar trebui să vedeți următorii arbori șabloane.

Arbori șablon pentru obstacole
Arbori șablon pentru obstacole (Previzualizare mare)

Acum, animați copacii dintr-o locație îndepărtată de pe platformă către utilizator. Ca și înainte, utilizați eticheta a-animation :

 <!-- Trees --> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="0 0.6 -7" to="0 0.6 1.5" dur="5000"></a-animation> </a-entity> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="-0.5 0.55 -7" to="-0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <a-entity ...> ... <a-animation attribute="position" ease="linear" from="0.5 0.55 -7" to="0.5 0.55 1.5" dur="5000"></a-animation> </a-entity>

Asigurați-vă că codul dvs. se potrivește cu următoarele.

 <a-entity...> <!-- Trees --> <a-entity shadow scale="0.3 0.3 0.3" position="0 0.6 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="0 0.6 -7" to="0 0.6 1.5" dur="5000"></a-animation> </a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="-0.5 0.55 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="-0.5 0.55 -7" to="-0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <a-entity shadow scale="0.3 0.3 0.3" position="0.5 0.55 0"> <a-entity mixin="foliage"></a-entity> <a-entity mixin="trunk" position="0 -0.5 0"></a-entity> <a-animation attribute="position" ease="linear" from="0.5 0.55 -7" to="0.5 0.55 1.5" dur="5000"></a-animation> </a-entity> <!-- Player --> ...

Navigați la previzualizare și veți vedea acum copacii mișcându-se spre dvs.

Arborele șablon se deplasează către jucător
Arbore șabloane se deplasează către jucătorArborii șabloane se deplasează către jucător (previzualizare mare)

Navigați înapoi la editorul dvs. De data aceasta, selectați assets/ergo.js . În secțiunea de joc, configurați arbori după ce fereastra s-a încărcat.

 /******** * GAME * ********/ ... window.onload = function() { setupTrees(); }

Sub comenzi, dar înainte de secțiunea Joc, adăugați o nouă secțiune TREES . În această secțiune, definiți o nouă funcție setupTrees .

 /************ * CONTROLS * ************/ ... /********* * TREES * *********/ function setupTrees() { } /******** * GAME * ********/ ...

În noua funcție setupTrees , obțineți referințe la obiectele DOM ale arborelui șablon și faceți referințele disponibile la nivel global.

 /********* * TREES * *********/ var templateTreeLeft; var templateTreeCenter; var templateTreeRight; function setupTrees() { templateTreeLeft = document.getElementById('template-tree-left'); templateTreeCenter = document.getElementById('template-tree-center'); templateTreeRight = document.getElementById('template-tree-right'); }

Apoi, definiți un nou utilitar removeTree . Cu acest utilitar, puteți elimina apoi arborii șablon din scenă. Sub funcția setupTrees , definiți noul utilitar.

 function setupTrees() { ... } function removeTree(tree) { tree.parentNode.removeChild(tree); }

Înapoi în setupTrees , utilizați noul utilitar pentru a elimina arborii șablon.

 function setupTrees() { ... removeTree(templateTreeLeft); removeTree(templateTreeRight); removeTree(templateTreeCenter); }

Asigurați-vă că secțiunile arborelui și jocului dvs. se potrivesc cu următoarele:

 /********* * TREES * *********/ var templateTreeLeft; var templateTreeCenter; var templateTreeRight; function setupTrees() { templateTreeLeft = document.getElementById('template-tree-left'); templateTreeCenter = document.getElementById('template-tree-center'); templateTreeRight = document.getElementById('template-tree-right'); removeTree(templateTreeLeft); removeTree(templateTreeRight); removeTree(templateTreeCenter); } function removeTree(tree) { tree.parentNode.removeChild(tree); } /******** * GAME * ********/ setupControls(); // TODO: AFRAME.registerComponent has to occur before window.onload? window.onload = function() { setupTrees(); }

Redeschideți previzualizarea, iar copacii ar trebui să lipsească. Previzualizarea ar trebui să se potrivească cu jocul nostru la începutul acestui tutorial.

Partea 1 produs finit
Produsul finit partea 1 (previzualizare mare)

Aceasta încheie designul arborelui șablon.

În acest pas, am acoperit și am folosit mixuri A-Frame, care ne permit să simplificăm codul prin definirea proprietăților comune. În plus, am folosit integrarea A-Frame cu DOM pentru a elimina obiectele din scena A-Frame VR.

În pasul următor, vom genera mai multe obstacole și vom proiecta un algoritm simplu pentru a distribui copacii pe diferite benzi.

Pasul 2: Obstacole de generare

Într-un joc de alergători fără sfârșit, scopul nostru este să evităm obstacolele care zboară spre noi. În această implementare specială a jocului, folosim trei benzi, așa cum este cel mai obișnuit.

Spre deosebire de majoritatea jocurilor de alergători fără sfârșit, acest joc va suporta doar mișcarea la stânga și la dreapta . Acest lucru impune o constrângere algoritmului nostru de apariție a obstacolelor: nu putem avea trei obstacole pe toate cele trei benzi, în același timp, zburând spre noi. Dacă se întâmplă acest lucru, jucătorul ar avea șanse zero de supraviețuire. Ca rezultat, algoritmul nostru de generare trebuie să se adapteze la această constrângere.

În acest pas, toate editările noastre de cod vor fi făcute în assets/ergo.js . Fișierul HTML va rămâne același. Navigați la secțiunea TREES din assets/ergo.js .

Pentru început, vom adăuga utilități pentru a genera copaci. Fiecare copac va avea nevoie de un ID unic, pe care îl vom defini naiv ca fiind numărul de copaci care există atunci când arborele este generat. Începeți prin a urmări numărul de arbori dintr-o variabilă globală.

 /********* * TREES * *********/ ... var numberOfTrees = 0; function setupTrees() { ...

În continuare, vom inițializa o referință la elementul DOM al containerului arborelui, la care funcția noastră de spawn îi va adăuga arbori. Încă în secțiunea TREES , adăugați o variabilă globală și apoi faceți referința.

 ... var treeContainer; var numberOfTrees ... function setupTrees() { ... templateTreeRight = ... treeContainer = document.getElementById('tree-container'); removeTree(...); ... }

Folosind atât numărul de copaci, cât și containerul de copaci, scrieți o nouă funcție care generează copaci.

 function removeTree(tree) { ... } function addTree(el) { numberOfTrees += 1; el.id = 'tree-' + numberOfTrees; treeContainer.appendChild(el); } ...

Pentru ușurință de utilizare mai târziu, veți crea o a doua funcție care adaugă arborele corect pe banda corectă. Pentru a începe, definiți o nouă matrice de templates în secțiunea TREES .

 var templates; var treeContainer; ... function setupTrees() { ... templates = [templateTreeLeft, templateTreeCenter, templateTreeRight]; removeTree(...); ... }

Folosind această matrice de șabloane, adăugați un utilitar care generează copaci pe o bandă specifică, având un ID care reprezintă stânga, mijlocul sau dreapta.

 function function addTree(el) { ... } function addTreeTo(position_index) { var template = templates[position_index]; addTree(template.cloneNode(true)); }

Navigați la previzualizare și deschideți consola pentru dezvoltatori. În consola pentru dezvoltatori, invocați funcția globală addTreeTo .

 > addTreeTo(0); # spawns tree in left lane 
Invocarea addTreeTo manual
Invocați addTreeTo manual (previzualizare mare)

Acum, veți scrie un algoritm care generează copaci aleatoriu:

  1. Alegeți o bandă la întâmplare (care nu a fost încă aleasă, pentru acest interval de timp);
  2. Generați un copac cu o oarecare probabilitate;
  3. Dacă numărul maxim de copaci a fost generat pentru acest interval de timp, opriți-vă. În caz contrar, repetați pasul 1.

Pentru a aplica acest algoritm, vom amesteca lista de șabloane și vom procesa unul câte unul. Începeți prin a defini o nouă funcție, addTreesRandomly , care acceptă o serie de argumente de cuvinte cheie diferite.

 function addTreeTo(position_index) { ... } /** * Add any number of trees across different lanes, randomly. **/ function addTreesRandomly( { probTreeLeft = 0.5, probTreeCenter = 0.5, probTreeRight = 0.5, maxNumberTrees = 2 } = {}) { }

În noua funcție addTreesRandomly , definiți o listă de arbori șablon și amestecați lista.

 function addTreesRandomly( ... ) { var trees = [ {probability: probTreeLeft, position_index: 0}, {probability: probTreeCenter, position_index: 1}, {probability: probTreeRight, position_index: 2}, ] shuffle(trees); }

Derulați în jos până în partea de jos a fișierului și creați o nouă secțiune de utilități, împreună cu un nou utilitar de shuffle . Acest utilitar va amesteca o matrice în loc.

 /******** * GAME * ********/ ... /************* * UTILITIES * *************/ /** * Shuffles array in place. * @param {Array} a items An array containing the items. */ function shuffle(a) { var j, x, i; for (i = a.length - 1; i > 0; i--) { j = Math.floor(Math.random() * (i + 1)); x = a[i]; a[i] = a[j]; a[j] = x; } return a; }

Navigați înapoi la funcția addTreesRandomly din secțiunea Trees. Adăugați o nouă variabilă numberOfTreesAdded și parcurgeți lista de arbori definită mai sus.

 function addTreesRandomly( ... ) { ... var numberOfTreesAdded = 0; trees.forEach(function (tree) { }); }

În iterația peste copaci, generați un copac numai cu o anumită probabilitate și numai dacă numărul de arbori adăugați nu depășește 2 . Actualizați bucla for după cum urmează.

 function addTreesRandomly( ... ) { ... trees.forEach(function (tree) { if (Math.random() < tree.probability && numberOfTreesAdded < maxNumberTrees) { addTreeTo(tree.position_index); numberOfTreesAdded += 1; } }); }

Pentru a încheia funcția, returnați numărul de arbori adăugați.

 function addTreesRandomly( ... ) { ... return numberOfTreesAdded; }

Verificați de două ori dacă funcția addTreesRandomly se potrivește cu următoarele.

 /** * Add any number of trees across different lanes, randomly. **/ function addTreesRandomly( { probTreeLeft = 0.5, probTreeCenter = 0.5, probTreeRight = 0.5, maxNumberTrees = 2 } = {}) { var trees = [ {probability: probTreeLeft, position_index: 0}, {probability: probTreeCenter, position_index: 1}, {probability: probTreeRight, position_index: 2}, ] shuffle(trees); var numberOfTreesAdded = 0; trees.forEach(function (tree) { if (Math.random() < tree.probability && numberOfTreesAdded < maxNumberTrees) { addTreeTo(tree.position_index); numberOfTreesAdded += 1; } }); return numberOfTreesAdded; }

În cele din urmă, pentru a genera copaci automat, configurați un cronometru care rulează declanșează generarea copacilor la intervale regulate. Definiți cronometrul la nivel global și adăugați o nouă funcție de demontare pentru acest cronometru.

 /********* * TREES * *********/ ... var treeTimer; function setupTrees() { ... } function teardownTrees() { clearInterval(treeTimer); }

Apoi, definiți o nouă funcție care inițializează cronometrul și salvează cronometrul în variabila globală definită anterior. Cronometrul de mai jos rulează la fiecare jumătate de secundă.

 function addTreesRandomlyLoop({intervalLength = 500} = {}) { treeTimer = setInterval(addTreesRandomly, intervalLength); }

În cele din urmă, porniți cronometrul după ce fereastra s-a încărcat, din secțiunea Joc.

 /******** * GAME * ********/ ... window.onload = function() { ... addTreesRandomlyLoop(); }

Navigați la previzualizare și veți vedea copaci care apar la întâmplare. Rețineți că nu există niciodată trei copaci deodată.

Arborele care depun icre la întâmplare
Arborele care se generează aleatoriu (previzualizare mare)

Aceasta încheie pasul obstacolelor. Am luat cu succes o serie de arbori șabloane și am generat un număr infinit de obstacole din șabloane. Algoritmul nostru de generare respectă, de asemenea, constrângerile naturale din joc pentru a-l face jucabil.

În pasul următor, să adăugăm testarea de coliziune.

Pasul 3: Testarea coliziunii

În această secțiune, vom implementa testele de coliziune între obstacole și jucător. Aceste teste de coliziune sunt mai simple decât testele de coliziune din majoritatea celorlalte jocuri; totuși, jucătorul se mișcă doar de-a lungul axei x, așa că ori de câte ori un copac traversează axa x, verificați dacă banda copacului este aceeași cu banda jucătorului. Vom implementa această verificare simplă pentru acest joc.

Navigați la index.html , în jos la secțiunea TREES . Aici, vom adăuga informații despre bandă la fiecare dintre copaci. Pentru fiecare dintre arbori, adăugați data-tree-position-index= , după cum urmează. În plus, adăugați class="tree" , astfel încât să putem selecta cu ușurință toți copacii de pe linie:

 <a-entity data-tree-position-index="1" class="tree" ...> </a-entity> <a-entity data-tree-position-index="0" class="tree" ...> </a-entity> <a-entity data-tree-position-index="2" class="tree" ...> </a-entity>

Navigați la assets/ergo.js și invocați o nouă funcție setupCollisions în secțiunea GAME . În plus, definiți o nouă variabilă globală isGameRunning care indică dacă un joc existent rulează sau nu.

 /******** * GAME * ********/ var isGameRunning = false; setupControls(); setupCollision(); window.onload = function() { ...

Definiți o nouă secțiune COLLISIONS imediat după secțiunea TREES , dar înaintea secțiunii Joc. În această secțiune, definiți funcția setupCollisions.

 /********* * TREES * *********/ ... /************** * COLLISIONS * **************/ const POSITION_Z_OUT_OF_SIGHT = 1; const POSITION_Z_LINE_START = 0.6; const POSITION_Z_LINE_END = 0.7; function setupCollision() { } /******** * GAME * ********/

Ca și înainte, vom înregistra o componentă AFRAME și vom folosi ascultătorul de evenimente tick pentru a rula codul la fiecare pas de timp. În acest caz, vom înregistra o componentă cu player și vom efectua verificări împotriva tuturor arborilor din acel ascultător:

 function setupCollisions() { AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { } } } }

În bucla for , începeți prin a obține informațiile relevante ale arborelui:

 document.querySelectorAll('.tree').forEach(function(tree) { position = tree.getAttribute('position'); tree_position_index = tree.getAttribute('data-tree-position-index'); tree_id = tree.getAttribute('id'); }

Apoi, încă în bucla for , eliminați arborele dacă nu este vizibil, imediat după extragerea proprietăților arborelui:

 document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_OUT_OF_SIGHT) { removeTree(tree); } }

Apoi, dacă nu rulează niciun joc, nu verificați dacă există o coliziune.

 document.querySelectorAll('.tree').forEach(function(tree) { if (!isGameRunning) return; }

În cele din urmă (încă în bucla for ), verificați dacă arborele împarte aceeași poziție în același timp cu jucătorul. Dacă da, apelați o funcție gameOver care nu a fost încă definită:

 document.querySelectorAll('.tree').forEach(function(tree) { ... if (POSITION_Z_LINE_START < position.z && position.z < POSITION_Z_LINE_END && tree_position_index == player_position_index) { gameOver(); } }

Verificați dacă funcția setupCollisions corespunde următoarelor:

 function setupCollisions() { AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { position = tree.getAttribute('position'); tree_position_index = tree.getAttribute('data-tree-position-index'); tree_id = tree.getAttribute('id'); if (position.z > POSITION_Z_OUT_OF_SIGHT) { removeTree(tree); } if (!isGameRunning) return; if (POSITION_Z_LINE_START < position.z && position.z < POSITION_Z_LINE_END && tree_position_index == player_position_index) { gameOver(); } }) } }) }

Aceasta încheie configurarea coliziunii. Acum, vom adăuga câteva detalii pentru a abstrage secvențele startGame și gameOver . Navigați la secțiunea GAME . Actualizați blocul window.onload pentru a se potrivi cu următoarele, înlocuind addTreesRandomlyLoop cu o funcție startGame care nu a fost încă definită.

 window.onload = function() { setupTrees(); startGame(); }

Sub invocările funcției de configurare, creați o nouă funcție startGame . Această funcție va inițializa variabila isGameRunning în consecință și va preveni apelurile redundante.

 window.onload = function() { ... } function startGame() { if (isGameRunning) return; isGameRunning = true; addTreesRandomlyLoop(); }

În cele din urmă, definiți gameOver , care va alerta un „Game Over!” mesaj pentru moment.

 function startGame() { ... } function gameOver() { isGameRunning = false; alert('Game Over!'); teardownTrees(); }

Aceasta se încheie secțiunea de testare a coliziunilor din jocul alergătorului fără sfârșit.

În acest pas, am folosit din nou componente A-Frame și o serie de alte utilități pe care le-am adăugat anterior. În plus, am reorganizat și extras corect funcțiile jocului; Vom spori ulterior aceste funcții de joc pentru a obține o experiență de joc mai completă.

Concluzie

În partea 1, am adăugat comenzi compatibile cu căștile VR: Privește la stânga pentru a te deplasa la stânga și la dreapta pentru a te deplasa la dreapta. În această a doua parte a seriei, v-am arătat cât de ușor poate fi să construiți un joc de realitate virtuală de bază, funcțional. Am adăugat logica jocului, astfel încât alergătorul fără sfârșit să se potrivească așteptărilor tale: aleargă pentru totdeauna și zboară către jucător o serie nesfârșită de obstacole periculoase. Până acum, ați creat un joc funcțional cu suport fără tastatură pentru căștile de realitate virtuală.

Iată resurse suplimentare pentru diferite comenzi VR și căști:

  • A-Frame pentru căști VR
    Un studiu asupra browserelor și căștilor pe care le acceptă A-Frame VR.
  • A-Frame pentru controlere VR
    Cum A-Frame nu acceptă controlere, controlere 3DoF și controlere 6DoF, pe lângă alte alternative de interacțiune.

În partea următoare, vom adăuga câteva retușuri finale și vom sincroniza stările jocului , ceea ce ne apropie cu un pas de jocurile multiplayer.