Cum să construiești un joc de realitate virtuală multiplayer în timp real (partea a 2-a)

Publicat: 2022-03-10
Rezumat rapid ↬ În acest tutorial, veți scrie mecanica jocului pentru un joc de realitate virtuală, care este strâns cuplat cu elementele multiplayer în timp real ale jocului.

În această serie de tutoriale, vom construi un joc de realitate virtuală multiplayer bazat pe web, în ​​care jucătorii vor trebui să colaboreze pentru a rezolva un puzzle. În prima parte a acestei serii, am proiectat globurile prezentate în joc. În această parte a seriei, vom adăuga mecanisme de joc și vom configura protocoale de comunicare între perechi de jucători.

Descrierea jocului de aici este extrasă din prima parte a seriei: Fiecare pereche de jucători primește un inel de globuri. Scopul este de a „porni” toate globurile, unde un glob este „pornit” dacă este ridicat și luminos. Un orb este „off” dacă este mai jos și slab. Cu toate acestea, anumite globuri „dominante” își afectează vecinii: dacă schimbă starea, vecinii săi schimbă și starea. Jucătorul 2 poate controla sfere cu numere pare, iar jucătorul 1 poate controla sfere cu numere impare. Acest lucru îi obligă pe ambii jucători să colaboreze pentru a rezolva puzzle-ul.

Cei 8 pași din acest tutorial sunt grupați în 3 secțiuni:

  1. Popularea interfeței utilizator (Pașii 1 și 2)
  2. Adăugați mecanisme de joc (Pașii de la 3 la 5)
  3. Configurați comunicarea (Pașii de la 6 la 8)

Această parte se va încheia cu o demonstrație online complet funcțională, pe care să o poată juca oricine. Veți folosi A-Frame VR și mai multe extensii A-Frame.

Puteți găsi codul sursă finalizat aici.

Jocul multiplayer finalizat, sincronizat între mai mulți clienți
Jocul multiplayer finalizat, sincronizat între mai mulți clienți. (Previzualizare mare)

1. Adăugați indicatori vizuali

Pentru a începe, vom adăuga indicatori vizuali ai ID-ului unui glob. Introduceți un nou element VR a-text ca primul copil al #container-orb0 , pe 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>

„Dependențele” unui orb sunt orb-urile pe care le va comuta atunci când este comutată: de exemplu, să spunem că orb 1 are ca dependențe orb-urile 2 și 3. Aceasta înseamnă că dacă orb 1 este comutat, orb-urile 2 și 3 vor fi și ele comutate. Vom adăuga indicatori vizuali ai dependențelor, după cum urmează, imediat după .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ți dacă codul dvs. se potrivește cu codul nostru sursă pentru Pasul 1. Orb-ul dvs. ar trebui să se potrivească acum cu următoarele:

Orb cu indicatori vizuali pentru ID-ul globului și ID-urile globurilor pe care le va declanșa
Orb cu indicatori vizuali pentru ID-ul globului și ID-urile globurilor pe care le va declanșa (Previzualizare mare)

Aceasta încheie indicatorii vizuali suplimentari de care vom avea nevoie. În continuare, vom adăuga dinamic sfere la scena VR, folosind acest șablon sfere.

2. Adăugați dinamic globuri

În acest pas, vom adăuga sfere conform unei specificații JSON-esque a unui nivel. Acest lucru ne permite să specificăm și să generăm cu ușurință noi niveluri. Vom folosi globul de la ultimul pas din partea 1 ca șablon.

Pentru a începe, importați jQuery, deoarece acest lucru va face modificările DOM și, astfel, modificările scenei VR, mai ușoare. Direct după importul A-Frame, adăugați următoarele la L8:

 <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>

Specificați un nivel folosind o matrice. Matricea va conține literali obiect care codifică „dependențele” fiecărei sfere. În interiorul etichetei <head> , adăugați următoarea configurație de nivel:

 <script> var orbs = [ {left: 1, right: 4}, {}, {on: true}, {}, {on: true} ]; </script>

Pentru moment, fiecare orb poate avea o singură dependență la „dreapta” și una la „stânga”. Imediat după declararea orbs -urilor de mai sus, adăugați un handler care va rula la încărcarea paginii. Acest handler va (1) duplica globul șablon și (2) va elimina globul șablon, folosind configurația de nivel furnizată:

 $(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) {}

Apoi, populați funcția de remove , care elimină pur și simplu un element din scena VR, având în vedere un selector. Din fericire, A-Frame observă modificări ale DOM-ului și, prin urmare, eliminarea elementului din DOM este suficientă pentru a-l elimina din scena VR. Completați funcția de remove după cum urmează.

 function remove(selector) { var el = document.querySelector(selector); el.parentNode.removeChild(el); }

Populați funcția clickOrb , care pur și simplu declanșează acțiunea de clic pe un orb.

 function clickOrb(i) { document.querySelector("#container-orb" + i).click(); }

Apoi, începeți să scrieți funcția populateTemplate . În această funcție, începeți prin a obține .container . Acest container pentru orb conține în plus indicatorii vizuali pe care i-am adăugat în pasul anterior. Mai mult, va trebui să modificăm comportamentul la clic al onclick , în funcție de dependențele sale. Dacă există o dependență de stânga, modificați atât indicatorul vizual, cât și comportamentul onclick pentru a reflecta acest lucru; același lucru este valabil și pentru o dependență de drept:

 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(); } }

Încă în funcția populateTemplate , setați corect ID-ul orb în toate elementele orb și ale containerului său.

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

Încă în funcția populateTemplate , setați comportamentul onclick , setați sămânța aleatoare astfel încât fiecare orb să fie diferit vizual și, în final, setați poziția de rotație a globului pe baza ID-ului său.

 container.attr('onclick', onclick); container.find('lp-sphere').attr('seed', i); template.attr('rotation', '0 ' + (360 / total * i) + ' 0');

La încheierea funcției, returnați template cu toate configurațiile de mai sus.

 return template;

În interiorul gestionarului de încărcare a documentelor și după eliminarea șablonului cu remove('#template') , activați orb-urile care au fost configurate să fie activate inițial.

 $(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); });

Aceasta încheie modificările Javascript. În continuare, vom schimba setările implicite ale șablonului cu cele ale unui glob „dezactivat”. Schimbați poziția și scala pentru #container-orb0 la următoarele:

 position="8 0.5 0" scale="0.5 0.5 0.5"

Apoi, modificați intensitatea pentru #light-orb0 la 0.

 intensity="0"

Verificați dacă codul dvs. sursă se potrivește cu codul nostru sursă pentru Pasul 2.

Scena dvs. VR ar trebui să prezinte acum 5 globuri, populate dinamic. În plus, unul dintre globuri ar trebui să aibă indicatori vizuali ai dependențelor, ca mai jos:

Toate globurile sunt populate dinamic, folosind orb șablon
Toate globurile sunt populate dinamic, folosind șablonul orb (previzualizare mare)

Aceasta încheie prima secțiune în adăugarea dinamică a globurilor. În secțiunea următoare, vom petrece trei pași adăugând mecanisme de joc. Mai exact, jucătorul va putea comuta doar anumite sfere în funcție de ID-ul jucătorului.

3. Adăugați starea terminalului

În acest pas, vom adăuga o stare terminală. Dacă toate globurile sunt activate cu succes, jucătorul vede o pagină de „victorie”. Pentru a face acest lucru, va trebui să urmăriți starea tuturor globurilor. De fiecare dată când un orb este activat sau dezactivat, va trebui să ne actualizăm starea internă. Să spunem că o funcție de ajutor toggleOrb actualizează starea pentru noi. Invocați funcția toggleOrb de fiecare dată când un orb își schimbă starea: (1) adăugați un ascultător de clic la handlerul onload și (2) adăugați un toggleOrb(i); invocarea la clickOrb . În cele din urmă, (3) definiți un toggleOrb gol.

 $(document).ready(function() { ... $('.orb').on('click', function() { var id = $(this).attr('data-id') toggleOrb(id); }); }); function toggleOrb(i) {} function clickOrb(i) { ... toggleOrb(i); }

Pentru simplitate, vom folosi configurația noastră de nivel pentru a indica starea jocului. Utilizați toggleOrb pentru a comuta starea on pornire pentru i-lea orb. toggleOrb poate declanșa în plus o stare terminală dacă toate globurile sunt activate.

 function toggleOrb(i) { orbs[i].on = !orbs[i].on; if (orbs.every(orb => orb.on)) console.log('Victory!'); }

Verificați de două ori dacă codul dvs. se potrivește cu codul nostru sursă pentru Pasul 3.

Aceasta încheie modul „single-player” pentru joc. În acest moment, aveți un joc de realitate virtuală complet funcțional. Cu toate acestea, acum va trebui să scrieți componenta multiplayer și să încurajați colaborarea prin mecanica jocului.

4. Creați obiectul jucător

În acest pas, vom crea o abstractizare pentru un jucător cu un ID de jucător. Acest ID de jucător va fi atribuit de server mai târziu.

Pentru moment, aceasta va fi pur și simplu o variabilă globală. Imediat după definirea orbs , definiți un ID de jucător:

 var orbs = ... var current_player_id = 1;

Verificați de două ori dacă codul dvs. se potrivește cu codul nostru sursă pentru Pasul 4. În pasul următor, acest ID de jucător va fi apoi utilizat pentru a determina ce sfere poate controla jucătorul.

5. Comutați Condițional Orbs

În acest pas, vom modifica comportamentul de comutare a orbului. Mai exact, jucătorul 1 poate controla sfere cu numere impare, iar jucătorul 2 poate controla sfere cu numere pare. Mai întâi, implementați această logică în ambele locuri în care orb-urile își schimbă starea:

 $('.orb').on('click', function() { var id = ... if (!allowedToToggle(id)) return false; ... } ... function clickOrb(i) { if (!allowedToToggle(id)) return; ... }

În al doilea rând, definiți funcția allowedToToggle , imediat după clickOrb . Dacă jucătorul actual este jucătorul 1, ID-urile cu numere impare vor returna o valoare de adevăr și, astfel, jucătorului 1 i se va permite să controleze globurile cu numere impare. Reversul este valabil pentru jucătorul 2. Tuturor celorlalți jucători nu li se permite să controleze orburile.

 function allowedToToggle(id) { if (current_player_id == 1) { return id % 2; } else if (current_player_id == 2) { return !(id % 2); } return false; }

Verificați de două ori dacă codul dvs. se potrivește cu codul nostru sursă pentru Pasul 5. În mod implicit, jucătorul este jucătorul 1. Aceasta înseamnă că dvs., în calitate de jucător 1, puteți controla doar globurile cu numere impare în previzualizare. Aceasta se încheie secțiunea despre mecanica jocului.

În secțiunea următoare, vom facilita comunicarea între ambii jucători prin intermediul unui server.

6. Configurați serverul cu WebSocket

În acest pas, veți configura un server simplu pentru (1) a ține evidența ID-urilor jucătorilor și (2) a transmite mesaje. Aceste mesaje vor include starea jocului, astfel încât jucătorii pot fi siguri că fiecare vede ceea ce vede celălalt.

Ne vom referi la index.html dvs. anterior ca cod sursă la nivelul clientului. Ne vom referi la cod în acest pas ca fiind codul sursă de pe partea serverului. Navigați la glitch.com, faceți clic pe „proiect nou” în dreapta sus, iar în meniul drop-down, faceți clic pe „hello-express”.

Din panoul din stânga, selectați „package.json” și adăugați socket-io la dependencies . Dicționarul dvs. dependencies ar trebui să se potrivească acum cu următoarele.

 "dependencies": { "express": "^4.16.4", "socketio": "^1.0.0" },

Din panoul din stânga, selectați „index.js” și înlocuiți conținutul acelui fișier cu următorul minim socket.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); });

Cele de mai sus configurează socket.io pe portul 3000 pentru o aplicație expres de bază. Apoi, definiți două variabile globale, una pentru menținerea listei de jucători activi și alta pentru menținerea celui mai mic ID de jucător nealocat.

 /** * Maintain player IDs */ var playerIds = []; var smallestPlayerId = 1;

Apoi, definiți funcția getPlayerId , care generează un nou ID de jucător și marchează noul ID de jucător ca „luat” adăugându-l la matricea playerIds . În special, funcția marchează pur și simplu smallestPlayerId și apoi actualizează smallestPlayerId căutând următorul cel mai mic număr întreg neluat.

 function getPlayerId() { var playerId = smallestPlayerId; playerIds.push(playerId); while (playerIds.includes(smallestPlayerId)) { smallestPlayerId++; } return playerId; }

Definiți funcția removePlayer , care actualizează smallestPlayerId în consecință și eliberează playerId -ul furnizat, astfel încât un alt jucător să poată lua acel ID.

 function removePlayer(playerId) { if (playerId < smallestPlayerId) { smallestPlayerId = playerId; } var index = playerIds.indexOf(playerId); playerIds.splice(index, 1); }

În cele din urmă, definiți o pereche de handlere de evenimente socket care înregistrează jucători noi și anulează înregistrarea jucătorilor deconectați, folosind perechea de metode de mai sus.

 /** * 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); }); });

Verificați de două ori dacă codul dvs. se potrivește cu codul nostru sursă pentru Pasul 6. Aceasta încheie înregistrarea și anularea de bază a jucătorului. Fiecare client poate folosi acum ID-ul jucătorului generat de server.

În pasul următor, vom modifica clientul pentru a primi și utiliza ID-ul jucătorului emis de server.

7. Aplicați ID-ul jucătorului

În următorii doi pași, vom finaliza o versiune rudimentară a experienței multiplayer. Pentru a începe, integrați atribuirea ID-ului jucătorului din partea clientului. În special, fiecare client va cere serverului un ID de jucător. Navigați înapoi la index.html din partea clientului la care lucram în pașii 4 și anterior.

Importă socket.io în head la L7:

 <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>

După manipulatorul de încărcare a documentului, instanțiază socket-ul și emite un eveniment newPlayer . Ca răspuns, partea de server va genera un nou ID de jucător folosind evenimentul playerId . Mai jos, utilizați adresa URL pentru previzualizarea proiectului Glitch în loc de lightful.glitch.me . Sunteți binevenit să utilizați adresa URL demonstrativă de mai jos, dar orice modificări de cod pe care le faceți nu vor fi, desigur, reflectate.

 $(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ți dacă codul dvs. se potrivește cu codul nostru sursă pentru Pasul 7. Acum, vă puteți încărca jocul în două browsere sau file diferite pentru a juca două părți ale unui joc multiplayer. Jucătorul 1 va putea controla sfere cu numere impare, iar jucătorul 2 va putea controla sfere cu numere pare.

Cu toate acestea, rețineți că comutarea orb-urilor pentru jucătorul 1 nu va afecta starea orb-ului pentru jucătorul 2. În continuare, trebuie să sincronizăm stările jocului.

8. Sincronizați starea jocului

În acest pas, vom sincroniza stările jocului, astfel încât jucătorii 1 și 2 să vadă aceleași stări de glob. Dacă orb 1 este activat pentru jucătorul 1, ar trebui să fie activat și pentru jucătorul 2. Pe partea clientului, vom anunța și vom asculta comutarile orb. Pentru a anunța, vom transmite pur și simplu ID-ul globului care este comutat.

Înainte de ambele invocări toggleOrb , adăugați următorul apel socket.emit .

 $(document).ready(function() { ... $('.orb').on('click', function() { ... socket.emit('toggleOrb', id); toggleOrb(id); }); }); ... function clickOrb(i) { ... socket.emit('toggleOrb', i); toggleOrb(i); }

Apoi, ascultați comutarile orb și comutați orb-ul corespunzător. Direct sub ascultatorul evenimentului socket playerId , adăugați un alt ascultător pentru evenimentul toggleOrb .

 socket.on('toggleOrb', function(i) { document.querySelector("#container-orb" + i).click(); toggleOrb(i); });

Aceasta încheie modificările aduse codului clientului. Verificați de două ori dacă codul dvs. se potrivește cu codul nostru sursă pentru Pasul 8.

Partea serverului trebuie acum să primească și să difuzeze ID-ul orb comutat. În index.js de pe partea serverului, adăugați următorul ascultător. Acest ascultător ar trebui să fie plasat direct sub receptorul de disconnect a prizei.

 socket.on('toggleOrb', function(i) { socket.broadcast.emit('toggleOrb', i); });

Verificați de două ori dacă codul dvs. se potrivește cu codul nostru sursă pentru Pasul 8. Acum, jucătorul 1 încărcat într-o fereastră și jucătorul 2 încărcat într-o a doua fereastră vor vedea ambele aceeași stare de joc. Cu asta, ai finalizat un joc de realitate virtuală multiplayer. În plus, cei doi jucători trebuie să colaboreze pentru a îndeplini obiectivul. Produsul final se va potrivi cu următoarele.

Jocul multiplayer finalizat, sincronizat între mai mulți clienți
Jocul multiplayer finalizat, sincronizat între mai mulți clienți. (Previzualizare mare)

Concluzie

Aceasta încheie tutorialul nostru despre crearea unui joc de realitate virtuală multiplayer. În acest proces, ați atins o serie de subiecte, inclusiv modelarea 3-D în A-Frame VR și experiențe multiplayer în timp real folosind WebSockets.

Pornind de la conceptele pe care le-am atins, cum ați asigura o experiență mai fluidă pentru cei doi jucători? Aceasta ar putea include verificarea că starea jocului este sincronizată și alertarea utilizatorului în caz contrar. De asemenea, puteți face indicatori vizuali simpli pentru starea terminalului și starea conexiunii jucătorului.

Având în vedere cadrul pe care l-am stabilit și conceptele pe care le-am introdus, acum aveți instrumentele pentru a răspunde la aceste întrebări și pentru a construi mult mai multe.

Puteți găsi codul sursă finalizat aici.

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