Come costruire un gioco di corridori senza fine nella realtà virtuale (parte 2)
Pubblicato: 2022-03-10Nella parte 1 di questa serie, abbiamo visto come è possibile creare un modello di realtà virtuale con effetti di illuminazione e animazione. In questa parte, implementeremo la logica di base del gioco e utilizzeremo manipolazioni dell'ambiente A-Frame più avanzate per creare la parte "gioco" di questa applicazione. Alla fine, avrai un gioco di realtà virtuale funzionante con una vera sfida.
Questo tutorial prevede una serie di passaggi, incluso (ma non limitato a) il rilevamento delle collisioni e altri concetti di A-Frame come i mixin.
- Demo del prodotto finale
Prerequisiti
Proprio come nel tutorial precedente, avrai bisogno di quanto segue:
- Accesso a Internet (in particolare a glitch.com);
- Un progetto Glitch completato dalla parte 1. (Puoi continuare dal prodotto finito navigando su https://glitch.com/edit/#!/ergo-1 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: progettare gli ostacoli
In questo passaggio, progetti gli alberi che useremo come ostacoli. Quindi, aggiungerai una semplice animazione che sposta gli alberi verso il giocatore, come la seguente:

Questi alberi serviranno da modello per gli ostacoli che genererai durante il gioco. Per la parte finale di questo passaggio, rimuoveremo quindi questi "alberi modello".
Per iniziare, aggiungi diversi mixin A-Frame . I mixin sono insiemi di proprietà dei componenti comunemente usati. Nel nostro caso, tutti i nostri alberi avranno lo stesso colore, altezza, larghezza, profondità ecc. In altre parole, tutti i tuoi alberi avranno lo stesso aspetto e quindi utilizzeranno alcuni mixin condivisi.
Nota : nel nostro tutorial, le tue uniche risorse saranno i mixin. Visita la pagina A-Frame Mixin per saperne di più.
Nel tuo editor, vai a index.html . Subito dopo il tuo cielo e prima delle tue luci, aggiungi una nuova entità A-Frame per contenere le tue risorse:
<a-sky...></a-sky> <!-- Mixins --> <a-assets> </a-assets> <!-- Lights --> ...
Nella tua nuova entità a-assets
, inizia aggiungendo un mixin per il tuo fogliame. Questo mixins definisce le proprietà comuni per il fogliame dell'albero modello. In breve, è una piramide bianca, piatta, per un effetto 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>
Appena sotto il tuo fogliame mixin, aggiungi un mixin per il tronco. Questo tronco sarà un piccolo prisma rettangolare bianco.
<a-assets> ... <a-mixin geometry=" primitive: box; height:0.5; width:0.1; depth:0.1;" material="color:white;"></a-mixin> </a-assets>
Successivamente, aggiungi gli oggetti dell'albero del modello che utilizzeranno questi mixin. Sempre in index.html , scorri verso il basso fino alla sezione piattaforme. Subito prima della sezione giocatore, aggiungi una nuova sezione albero, con tre entità albero vuote:
<a-entity ...> <!-- Trees --> <a-entity></a-entity> <a-entity></a-entity> <a-entity></a-entity> <!-- Player --> ...
Quindi, riposiziona, ridimensiona e aggiungi ombre alle entità albero.
<!-- 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>
Ora, popola le entità albero con un tronco e fogliame, usando i mixin che abbiamo definito in precedenza.
<!-- 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>
Passa alla tua anteprima e ora dovresti vedere i seguenti alberi di modello.

Ora, anima gli alberi da una posizione distante sulla piattaforma verso l'utente. Come prima, usa il tag 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>
Assicurati che il tuo codice corrisponda a quanto segue.
<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 --> ...
Passa alla tua anteprima e ora vedrai gli alberi muoversi verso di te.

Torna al tuo editor. Questa volta, seleziona asset/ergo.js . Nella sezione del gioco, imposta gli alberi dopo che la finestra è stata caricata.
/******** * GAME * ********/ ... window.onload = function() { setupTrees(); }
Sotto i controlli ma prima della sezione Gioco, aggiungi una nuova sezione TREES
. In questa sezione, definisci una nuova funzione setupTrees
.
/************ * CONTROLS * ************/ ... /********* * TREES * *********/ function setupTrees() { } /******** * GAME * ********/ ...
Nella nuova funzione setupTrees
, ottieni i riferimenti agli oggetti DOM dell'albero del modello e rendi i riferimenti disponibili a livello globale.
/********* * 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'); }
Quindi, definisci una nuova utilità removeTree
. Con questa utilità, puoi quindi rimuovere gli alberi dei modelli dalla scena. Sotto la funzione setupTrees
, definisci la tua nuova utilità.
function setupTrees() { ... } function removeTree(tree) { tree.parentNode.removeChild(tree); }
Di nuovo in setupTrees
, usa la nuova utilità per rimuovere gli alberi dei modelli.
function setupTrees() { ... removeTree(templateTreeLeft); removeTree(templateTreeRight); removeTree(templateTreeCenter); }
Assicurati che le sezioni dell'albero e del gioco corrispondano a quanto segue:
/********* * 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(); }
Riapri l'anteprima e ora i tuoi alberi dovrebbero essere assenti. L'anteprima dovrebbe corrispondere al nostro gioco all'inizio di questo tutorial.

Questo conclude il design dell'albero del modello.
In questo passaggio, abbiamo trattato e utilizzato i mixin A-Frame, che ci consentono di semplificare il codice definendo proprietà comuni. Inoltre, abbiamo sfruttato l'integrazione di A-Frame con il DOM per rimuovere gli oggetti dalla scena VR di A-Frame.
Nella fase successiva, genereremo più ostacoli e progetteremo un semplice algoritmo per distribuire gli alberi tra diverse corsie.
Passaggio 2: la generazione di ostacoli
In un gioco di corsa senza fine, il nostro obiettivo è evitare gli ostacoli che volano verso di noi. In questa particolare implementazione del gioco, utilizziamo tre corsie come è più comune.
A differenza della maggior parte dei giochi di corridori senza fine, questo gioco supporterà solo il movimento a sinistra e a destra . Questo impone un vincolo al nostro algoritmo per generare ostacoli: non possiamo avere tre ostacoli in tutte e tre le corsie, allo stesso tempo, che volano verso di noi. In tal caso, il giocatore avrebbe zero possibilità di sopravvivenza. Di conseguenza, il nostro algoritmo di spawn deve soddisfare questo vincolo.
In questo passaggio, tutte le nostre modifiche al codice verranno apportate in assets/ergo.js . Il file HTML rimarrà lo stesso. Passa alla sezione TREES
di asset/ergo.js .
Per iniziare, aggiungeremo utilità per generare alberi. Ogni albero avrà bisogno di un ID univoco, che definiremo ingenuamente come il numero di alberi che esistono quando l'albero viene generato. Inizia monitorando il numero di alberi in una variabile globale.
/********* * TREES * *********/ ... var numberOfTrees = 0; function setupTrees() { ...
Successivamente, inizializzeremo un riferimento all'elemento DOM del contenitore dell'albero, a cui la nostra funzione di spawn aggiungerà alberi. Sempre nella sezione TREES
, aggiungi una variabile globale e poi fai il riferimento.
... var treeContainer; var numberOfTrees ... function setupTrees() { ... templateTreeRight = ... treeContainer = document.getElementById('tree-container'); removeTree(...); ... }
Usando sia il numero di alberi che il contenitore dell'albero, scrivi una nuova funzione che genera gli alberi.

function removeTree(tree) { ... } function addTree(el) { numberOfTrees += 1; el.id = 'tree-' + numberOfTrees; treeContainer.appendChild(el); } ...
Per facilità d'uso in seguito, creerai una seconda funzione che aggiunge l'albero corretto alla corsia corretta. Per iniziare, definisci un nuovo array di templates
nella sezione TREES
.
var templates; var treeContainer; ... function setupTrees() { ... templates = [templateTreeLeft, templateTreeCenter, templateTreeRight]; removeTree(...); ... }
Usando questo array di modelli, aggiungi un'utilità che genera alberi in una corsia specifica, dato un ID che rappresenta sinistra, centro o destra.
function function addTree(el) { ... } function addTreeTo(position_index) { var template = templates[position_index]; addTree(template.cloneNode(true)); }
Passa alla tua anteprima e apri la tua console per sviluppatori. Nella tua console per sviluppatori, richiama la funzione globale addTreeTo
.
> addTreeTo(0); # spawns tree in left lane

addTreeTo
manualmente (anteprima grande)Ora scriverai un algoritmo che genera alberi in modo casuale:
- Scegli una corsia a caso (che non è stata ancora scelta, per questo passaggio temporale);
- Genera un albero con una certa probabilità;
- Se è stato generato il numero massimo di alberi per questo passaggio temporale, fermati. In caso contrario, ripetere il passaggio 1.
Per effettuare questo algoritmo, mescoleremo invece l'elenco dei modelli ed elaboreremo uno alla volta. Inizia definendo una nuova funzione, addTreesRandomly
che accetta un numero di diversi argomenti di parole chiave.
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 } = {}) { }
Nella tua nuova funzione addTreesRandomly
, definisci un elenco di alberi modello e mescola l'elenco.
function addTreesRandomly( ... ) { var trees = [ {probability: probTreeLeft, position_index: 0}, {probability: probTreeCenter, position_index: 1}, {probability: probTreeRight, position_index: 2}, ] shuffle(trees); }
Scorri verso il basso fino alla fine del file e crea una nuova sezione utilità, insieme a una nuova utilità shuffle
. Questa utilità mescolerà un array sul posto.
/******** * 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; }
Torna alla funzione addTreesRandomly
nella sezione Alberi. Aggiungere una nuova variabile numberOfTreesAdded
e scorrere l'elenco di alberi definito sopra.
function addTreesRandomly( ... ) { ... var numberOfTreesAdded = 0; trees.forEach(function (tree) { }); }
Nell'iterazione sugli alberi, genera un albero solo con una certa probabilità e solo se il numero di alberi aggiunti non supera 2
. Aggiorna il ciclo for come segue.
function addTreesRandomly( ... ) { ... trees.forEach(function (tree) { if (Math.random() < tree.probability && numberOfTreesAdded < maxNumberTrees) { addTreeTo(tree.position_index); numberOfTreesAdded += 1; } }); }
Per concludere la funzione, restituire il numero di alberi aggiunti.
function addTreesRandomly( ... ) { ... return numberOfTreesAdded; }
Verifica che la tua funzione addTreesRandomly
corrisponda a quanto segue.
/** * 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; }
Infine, per generare automaticamente gli alberi, impostare un timer che viene eseguito attiva la generazione degli alberi a intervalli regolari. Definisci il timer a livello globale e aggiungi una nuova funzione di smontaggio per questo timer.
/********* * TREES * *********/ ... var treeTimer; function setupTrees() { ... } function teardownTrees() { clearInterval(treeTimer); }
Quindi, definire una nuova funzione che inizializza il timer e salva il timer nella variabile globale precedentemente definita. Il timer seguente viene eseguito ogni mezzo secondo.
function addTreesRandomlyLoop({intervalLength = 500} = {}) { treeTimer = setInterval(addTreesRandomly, intervalLength); }
Infine, avvia il timer dopo che la finestra è stata caricata, dalla sezione Gioco.
/******** * GAME * ********/ ... window.onload = function() { ... addTreesRandomlyLoop(); }
Passa alla tua anteprima e vedrai gli alberi che si generano in modo casuale. Nota che non ci sono mai tre alberi contemporaneamente.

Questo conclude la fase degli ostacoli. Abbiamo preso con successo un certo numero di alberi di modelli e generato un numero infinito di ostacoli dai modelli. Il nostro algoritmo di spawn rispetta anche i vincoli naturali nel gioco per renderlo giocabile.
Nel passaggio successivo, aggiungiamo il test di collisione.
Passaggio 3: test di collisione
In questa sezione implementeremo i test di collisione tra gli ostacoli e il giocatore. Questi test di collisione sono più semplici dei test di collisione nella maggior parte degli altri giochi; tuttavia, il giocatore si muove solo lungo l'asse x, quindi ogni volta che un albero attraversa l'asse x, controlla se la corsia dell'albero è la stessa della corsia del giocatore. Implementeremo questo semplice controllo per questo gioco.
Passa a index.html , fino alla sezione TREES
. Qui aggiungeremo le informazioni sulla corsia a ciascuno degli alberi. Per ciascuno degli alberi, aggiungi data-tree-position-index=
, come segue. Aggiungi inoltre class="tree"
, in modo da poter selezionare facilmente tutti gli alberi lungo la linea:
<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>
Passa a assets/ergo.js e invoca una nuova funzione setupCollisions
nella sezione GAME
. Inoltre, definisci una nuova variabile globale isGameRunning
che denoti se un gioco esistente è già in esecuzione o meno.
/******** * GAME * ********/ var isGameRunning = false; setupControls(); setupCollision(); window.onload = function() { ...
Definisci una nuova sezione COLLISIONS
subito dopo la sezione TREES
ma prima della sezione Gioco. In questa sezione, definire la funzione 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 * ********/
Come prima, registreremo un componente AFRAME e utilizzeremo il listener di eventi tick
per eseguire il codice in ogni timestep. In questo caso, registreremo un componente con il player
ed eseguiremo controlli su tutti gli alberi in quel listener:
function setupCollisions() { AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { } } } }
Nel ciclo for
, inizia ottenendo le informazioni rilevanti dell'albero:
document.querySelectorAll('.tree').forEach(function(tree) { position = tree.getAttribute('position'); tree_position_index = tree.getAttribute('data-tree-position-index'); tree_id = tree.getAttribute('id'); }
Quindi, sempre all'interno del ciclo for
, rimuovi l'albero se non è visibile, subito dopo aver estratto le proprietà dell'albero:
document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_OUT_OF_SIGHT) { removeTree(tree); } }
Quindi, se non c'è alcun gioco in esecuzione, non controllare se c'è una collisione.
document.querySelectorAll('.tree').forEach(function(tree) { if (!isGameRunning) return; }
Infine (sempre nel ciclo for
), controlla se l'albero condivide la stessa posizione contemporaneamente con il giocatore. In tal caso, chiama una funzione gameOver
ancora da definire:
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 che la tua funzione di setupCollisions
corrisponda a quanto segue:
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(); } }) } }) }
Questo conclude la configurazione della collisione. Ora aggiungeremo alcune sottigliezze per astrarre le sequenze startGame
e gameOver
. Vai alla sezione GAME
. Aggiorna il blocco window.onload
in modo che corrisponda a quanto segue, sostituendo addTreesRandomlyLoop
con una funzione startGame
ancora da definire.
window.onload = function() { setupTrees(); startGame(); }
Sotto le invocazioni della funzione di configurazione, crea una nuova funzione startGame
. Questa funzione inizializzerà la variabile isGameRunning
di conseguenza e impedirà le chiamate ridondanti.
window.onload = function() { ... } function startGame() { if (isGameRunning) return; isGameRunning = true; addTreesRandomlyLoop(); }
Infine, definisci gameOver
, che avviserà un "Game Over!" messaggio per ora.
function startGame() { ... } function gameOver() { isGameRunning = false; alert('Game Over!'); teardownTrees(); }
Questo conclude la sezione del test di collisione del gioco di corsa infinita.
In questo passaggio, abbiamo nuovamente utilizzato i componenti A-Frame e una serie di altre utilità che abbiamo aggiunto in precedenza. Abbiamo inoltre riorganizzato e opportunamente astratto le funzioni di gioco; successivamente aumenteremo queste funzioni di gioco per ottenere un'esperienza di gioco più completa.
Conclusione
Nella parte 1, abbiamo aggiunto controlli compatibili con le cuffie VR: guarda a sinistra per spostarti a sinistra e a destra per spostarti a destra. In questa seconda parte della serie, ti ho mostrato quanto può essere facile creare un gioco di realtà virtuale di base e funzionante. Abbiamo aggiunto la logica di gioco, in modo che l'infinity runner soddisfi le tue aspettative: corri per sempre e fai volare una serie infinita di pericolosi ostacoli verso il giocatore. Finora, hai creato un gioco funzionante con supporto senza tastiera per cuffie per realtà virtuale.
Di seguito sono riportate risorse aggiuntive per diversi controlli e visori VR:
- A-Frame per visori VR
Un'indagine su browser e visori supportati da A-Frame VR. - A-Frame per controller VR
In che modo A-Frame non supporta controller, controller 3DoF e controller 6DoF, oltre ad altre alternative per l'interazione.
Nella parte successiva, aggiungeremo alcuni ritocchi finali e sincronizzeremo gli stati di gioco , che ci avvicinano di un passo ai giochi multiplayer.