Jak zbudować niekończącą się grę biegacza w wirtualnej rzeczywistości (część 3)
Opublikowany: 2022-03-10I tak nasza podróż trwa. W ostatniej części mojej serii o tym, jak zbudować grę VR typu endless runner, pokażę Ci, jak zsynchronizować stan gry między dwoma urządzeniami, co zbliży Cię o krok do zbudowania gry wieloosobowej. W szczególności przedstawię MirrorVR, który jest odpowiedzialny za obsługę serwera pośredniczącego w komunikacji klient-klient.
Uwaga : w tę grę można grać z goglami VR lub bez nich. Demo produktu końcowego można obejrzeć na ergo-3.glitch.me.
Aby rozpocząć, będziesz potrzebować następujących elementów.
- dostęp do Internetu (w szczególności do glitch.com);
- Projekt Glitch ukończony z części 2 tego samouczka. Możesz zacząć od gotowego produktu części 2, przechodząc do https://glitch.com/edit/#!/ergo-2 i klikając „Remiksuj do edycji”;
- Gogle wirtualnej rzeczywistości (opcjonalne, zalecane). (Używam Google Cardboard, który jest oferowany w cenie 15 USD za sztukę.)
Krok 1: Wyświetl wynik
Gra taka, jaka jest, działa na minimalnym poziomie, w którym gracz otrzymuje wyzwanie: unikaj przeszkód. Jednak poza kolizjami obiektów gra nie przekazuje graczowi informacji zwrotnej na temat postępów w grze. Aby temu zaradzić, w tym kroku zaimplementujesz wyświetlanie partytury. Partyturą będzie duży obiekt tekstowy umieszczony w naszym świecie wirtualnej rzeczywistości, a nie interfejs przyklejony do pola widzenia użytkownika.
Ogólnie rzecz biorąc, w rzeczywistości wirtualnej interfejs użytkownika najlepiej integruje się ze światem, a nie przykleja do głowy użytkownika.
Zacznij od dodania obiektu do index.html . Dodaj domieszkę text
, która zostanie ponownie wykorzystana dla innych elementów tekstowych:
<a-assets> ... <a-mixin text=" font:exo2bold; anchor:center; align:center;"></a-mixin> ... </a-assets>
Następnie dodaj element text
do platformy, tuż przed odtwarzaczem:
<!-- Score --> <a-text value="" mixin="text" height="40" width="40" position="0 1.2 -3" opacity="0.75"></a-text> <!-- Player --> ...
Dodaje to element tekstowy do sceny rzeczywistości wirtualnej. Tekst nie jest obecnie widoczny, ponieważ jego wartość jest ustawiona na pustą. Jednak teraz będziesz wypełniać encję tekstową dynamicznie, używając JavaScript. Przejdź do asset/ergo.js . Po sekcji collisions
dodaj sekcję score
i zdefiniuj szereg zmiennych globalnych:
-
score
: aktualny wynik gry. -
countedTrees
: identyfikatory wszystkich drzew uwzględnionych w wyniku. (Dzieje się tak, ponieważ testy kolizji mogą być wyzwalane wielokrotnie dla tego samego drzewa). -
scoreDisplay
: odniesienie do obiektu DOM, odpowiadające obiektowi tekstowemu w świecie wirtualnej rzeczywistości.
/********* * SCORE * *********/ var score; var countedTrees; var scoreDisplay;
Następnie zdefiniuj funkcję konfiguracji, aby zainicjować nasze zmienne globalne. W tym samym duchu zdefiniuj funkcję teardown
.
... var scoreDisplay; function setupScore() { score = 0; countedTrees = new Set(); scoreDisplay = document.getElementById('score'); } function teardownScore() { scoreDisplay.setAttribute('value', ''); }
W sekcji Game
zaktualizuj gameOver
, startGame
i window.onload
, aby uwzględnić konfigurację wyników i usuwanie.
/******** * GAME * ********/ function gameOver() { ... teardownScore(); } function startGame() { ... setupScore(); addTreesRandomlyLoop(); } window.onload = function() { setupScore(); ... }
Zdefiniuj funkcję, która zwiększa wynik dla konkretnego drzewa. Ta countedTrees
sprawdza, czy drzewo nie jest liczone podwójnie.
function addScoreForTree(tree_id) { if (countedTrees.has(tree_id)) return; score += 1; countedTrees.add(tree_id); }
Dodatkowo dodaj narzędzie do aktualizowania wyświetlanego wyniku za pomocą zmiennej globalnej.
function updateScoreDisplay() { scoreDisplay.setAttribute('value', score); }
Odpowiednio zaktualizuj testy kolizji, aby wywołać tę funkcję zwiększania wyniku za każdym razem, gdy przeszkoda minie gracza. Nadal w assets/ergo.js
przejdź do sekcji collisions
. Dodaj następujące sprawdzenie i aktualizację.
AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_LINE_END) { addScoreForTree(tree_id); updateScoreDisplay(); } }) } })
Na koniec zaktualizuj wyświetlanie wyników, gdy tylko gra się rozpocznie. Przejdź do sekcji Game
i dodaj updateScoreDisplay();
startGame
:
function startGame() { ... setupScore(); updateScoreDisplay(); ... }
Upewnij się, że resources/ergo.js i index.html są zgodne z odpowiednimi plikami kodu źródłowego. Następnie przejdź do podglądu. Powinieneś zobaczyć następujące informacje:
Na tym kończy się wyświetlanie partytury. Następnie dodamy odpowiednie menu startowe i Game Over , aby gracz mógł odtworzyć grę według własnego uznania.
Krok 2: Dodaj menu Start
Teraz, gdy użytkownik może śledzić postępy, dodasz ostatnie szlify, aby ukończyć grę. W tym kroku dodasz menu Start i menu Game Over , umożliwiające użytkownikowi uruchamianie i ponowne uruchamianie gier.
Zacznijmy od menu Start , w którym gracz klika przycisk „Start”, aby rozpocząć grę. W drugiej połowie tego kroku dodasz menu Game Over z przyciskiem „Restart”:
Przejdź do index.html w swoim edytorze. Następnie znajdź sekcję Mixins
. Tutaj dołącz mixin title
, który definiuje style dla szczególnie dużego tekstu. Używamy tej samej czcionki co poprzednio, wyrównujemy tekst do środka i określamy rozmiar odpowiedni do rodzaju tekstu. (Zauważ, że poniżej anchor
to miejsce, w którym obiekt tekstowy jest zakotwiczony w swojej pozycji).
<a-assets> ... <a-mixin text=" font:exo2bold; height:40; width:40; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>
Następnie dodaj drugą mieszankę dla nagłówków drugorzędnych. Ten tekst jest nieco mniejszy, ale poza tym jest identyczny z tytułem.
<a-assets> ... <a-mixin text=" font:exo2bold; height:10; width:10; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>
W przypadku trzeciego i ostatniego domieszki zdefiniuj właściwości tekstu opisowego — nawet mniejsze niż nagłówki drugorzędne.
<a-assets> ... <a-mixin text=" font:exo2bold; height:5; width:5; opacity:0.75; anchor:center; align:center;"></a-mixin> </a-assets>
Po zdefiniowaniu wszystkich stylów tekstu, zdefiniujesz teraz obiekty tekstu w świecie. Dodaj nową sekcję Menus
pod sekcją Score
, z pustym kontenerem na menu Start :
<!-- Score --> ... <!-- Menus --> <a-entity> <a-entity position="0 1.1 -3"> </a-entity> </a-entity>
W kontenerze menu Start zdefiniuj tytuł i kontener dla całego tekstu niebędącego tytułem:
... <a-entity ...> <a-entity position="0 1 0"> </a-entity> <a-text value="ERGO" mixin="title"></a-text> </a-entity> </a-entity>
Wewnątrz kontenera na tekst niebędący tytułem dodaj instrukcje dotyczące gry:
<a-entity...> <a-text value="Turn left and right to move your player, and avoid the trees!" mixin="copy"></a-text> </a-entity>
Aby uzupełnić menu Start , dodaj przycisk z napisem „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>
Sprawdź dokładnie, czy kod HTML menu Start jest zgodny z następującym:
<!-- 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>
Przejdź do podglądu, a zobaczysz następujące menu Start :
Nadal w sekcji Menus
(bezpośrednio pod menu start
) dodaj menu zakończenia game-over
używając tych samych mixinów:
<!-- 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>
Przejdź do pliku JavaScript asset/ergo.js . Utwórz nową sekcję Menus
przed sekcją Game
. Dodatkowo zdefiniuj trzy puste funkcje: setupAllMenus
, hideAllMenus
i showGameOverMenu
.
/******** * MENU * ********/ function setupAllMenus() { } function hideAllMenus() { } function showGameOverMenu() { } /******** * GAME * ********/
Następnie zaktualizuj sekcję Game
w trzech miejscach. W gameOver
pokaż menu Game Over :
function gameOver() { ... showGameOverMenu(); } ``` In `startGame`, hide all menus: ``` function startGame() { ... hideAllMenus(); }
Następnie w window.onload
usuń bezpośrednie wywołanie startGame
i zamiast tego wywołaj setupAllMenus
. Zaktualizuj swój odbiornik, aby pasował do następujących elementów:
window.onload = function() { setupAllMenus(); setupScore(); setupTrees(); }
Wróć do sekcji Menu
. Zapisz odniesienia do różnych obiektów 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'); }
Następnie powiąż oba przyciski „Start” i „Uruchom ponownie” z startGame
:
function setupAllMenus() { ... startButton.addEventListener('click', startGame); restartButton.addEventListener('click', startGame); }
Zdefiniuj showStartMenu
i wywołaj je z setupAllMenus
:
function setupAllMenus() { ... showStartMenu(); } function hideAllMenus() { } function showGameOverMenu() { } function showStartMenu() { }
Aby wypełnić trzy puste funkcje, będziesz potrzebować kilku funkcji pomocniczych. Zdefiniuj następujące dwie funkcje, które akceptują element DOM reprezentujący element A-Frame VR i pokazują go lub ukrywają. Zdefiniuj obie funkcje powyżej showAllMenus
:
... var restartButton; function hideEntity(el) { el.setAttribute('visible', false); } function showEntity(el) { el.setAttribute('visible', true); } function showAllMenus() { ...
Najpierw hideAllMenus
. Usuniesz obiekty z pola widzenia, a następnie usuniesz detektory kliknięć dla obu menu:
function hideAllMenus() { hideEntity(menuContainer); startButton.classList.remove('clickable'); restartButton.classList.remove('clickable'); }
Po drugie, showGameOverMenu
. Tutaj przywróć kontener dla obu menu, a także menu Game Over i odbiornika kliknięć przycisku „Restart”. Usuń jednak odbiornik kliknięć przycisku „Start” i ukryj menu „Start”.
function showGameOverMenu() { showEntity(menuContainer); hideEntity(menuStart); showEntity(menuGameOver); startButton.classList.remove('clickable'); restartButton.classList.add('clickable'); }
Po trzecie, showStartMenu
. Tutaj odwróć wszystkie zmiany wprowadzone przez showGameOverMenu
.
function showStartMenu() { showEntity(menuContainer); hideEntity(menuGameOver); showEntity(menuStart); startButton.classList.add('clickable'); restartButton.classList.remove('clickable'); }
Sprawdź dokładnie, czy Twój kod pasuje do odpowiednich plików źródłowych. Następnie przejdź do podglądu i zaobserwujesz następujące zachowanie:
To kończy menu Start i Game Over .
Gratulacje! Masz teraz w pełni działającą grę z właściwym początkiem i właściwym zakończeniem. Jednak w tym samouczku pozostał jeszcze jeden krok: musimy zsynchronizować stan gry między różnymi urządzeniami graczy. To przybliży nas o krok do gier wieloosobowych.
Krok 3: Synchronizacja stanu gry z MirrorVR
W poprzednim samouczku dowiedziałeś się, jak przesyłać informacje w czasie rzeczywistym przez gniazda, aby ułatwić jednokierunkową komunikację między serwerem a klientem. W tym kroku zbudujesz pełnoprawny produkt z tego samouczka, MirrorVR, który obsługuje serwer pośredniczący w komunikacji klient-klient.
Uwaga : Możesz dowiedzieć się więcej o MirrorVR tutaj.
Przejdź do index.html . Tutaj załadujemy MirrorVR i dodamy komponent do kamery, wskazując, że powinien odzwierciedlać widok urządzenia mobilnego tam, gdzie ma to zastosowanie. Zaimportuj zależność 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>
Następnie dodaj do kamery komponent camera-listener
:
<a-camera camera-listener ...>
Przejdź do asset/ergo.js . Na tym etapie urządzenie mobilne będzie wysyłać polecenia, a urządzenie stacjonarne będzie odzwierciedlać tylko urządzenie mobilne.
Aby to ułatwić, potrzebujesz narzędzia do rozróżniania urządzeń stacjonarnych i mobilnych. Na końcu pliku dodaj funkcję mobileCheck
po 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; };
Najpierw zsynchronizujemy rozpoczęcie gry. W startGame
w sekcji Game dodaj na końcu powiadomienie mirrorVR
.
function startGame() { ... if (mobileCheck()) { mirrorVR.notify('startGame', {}) } }
Klient mobilny wysyła teraz powiadomienia o rozpoczęciu gry. Teraz zaimplementujesz odpowiedź pulpitu.
W oknie nasłuchu ładowania wywołaj funkcję setupMirrorVR
:
window.onload = function() { ... setupMirrorVR(); }
Zdefiniuj nową sekcję nad sekcją Game
dla konfiguracji MirrorVR:
/************ * MirrorVR * ************/ function setupMirrorVR() { mirrorVR.init(); }
Następnie dodaj argumenty słów kluczowych do funkcji inicjującej mirrorVR. W szczególności zdefiniujemy obsługę powiadomień o rozpoczęciu gry. Dodatkowo określimy identyfikator pokoju; zapewnia to natychmiastową synchronizację każdego, kto ładuje Twoją aplikację.
function setupMirrorVR() { mirrorVR.init({ roomId: 'ergo', state: { startGame: { onNotify: function(data) { hideAllMenus(); setupScore(); updateScoreDisplay(); } }, } }); }
Powtórz ten sam proces synchronizacji dla Game Over . W gameOver
w sekcji Game
dodaj czek na urządzenia mobilne i wyślij odpowiednie powiadomienie:
function gameOver() { ... if (mobileCheck()) { mirrorVR.notify('gameOver', {}); } }
Przejdź do sekcji MirrorVR
i zaktualizuj argumenty słów kluczowych za pomocą odbiornika gameOver
:
function setupMirrorVR() { mirrorVR.init({ state: { startGame: {... }, gameOver: { onNotify: function(data) { gameOver(); } }, } }) }
Następnie powtórz ten sam proces synchronizacji dla dodawania drzew. Przejdź do addTreesRandomly
w sekcji Trees
. Śledź, które pasy otrzymują nowe drzewa. Następnie bezpośrednio przed zarządzeniem return
i wyślij stosowne powiadomienie:
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 ... }
Przejdź do sekcji MirrorVR
i zaktualizuj argumenty słów kluczowych do mirrorVR.init
z nowym odbiornikiem drzew:
function setupMirrorVR() { mirrorVR.init({ state: { ... gameOver: {... }, addTrees: { onNotify: function(position_indices) { position_indices.forEach(addTreeTo) } }, } }) }
Na koniec synchronizujemy wynik gry. W updateScoreDisplay
z sekcji Score
wyślij powiadomienie, gdy ma to zastosowanie:
function updateScoreDisplay() { ... if (mobileCheck()) { mirrorVR.notify('score', score); } }
Zaktualizuj inicjalizację mirrorVR
po raz ostatni, z nasłuchiwaniem zmian punktacji:
function setupMirrorVR() { mirrorVR.init({ state: { addTrees: { }, score: { onNotify: function(data) { score = data; updateScoreDisplay(); } } } }); }
Sprawdź dokładnie, czy Twój kod pasuje do odpowiednich plików kodu źródłowego dla tego kroku. Następnie przejdź do podglądu na pulpicie. Dodatkowo otwórz ten sam adres URL na swoim urządzeniu mobilnym. Gdy tylko Twoje urządzenie mobilne załaduje stronę internetową, Twój pulpit powinien natychmiast rozpocząć tworzenie kopii lustrzanej gry urządzenia mobilnego.
Oto demo. Zauważ, że kursor na pulpicie nie porusza się, co oznacza, że urządzenie mobilne steruje podglądem na pulpicie.
To kończy Twój rozszerzony projekt z mirrorVR.
Ten trzeci krok wprowadził kilka podstawowych kroków synchronizacji stanu gry; aby uczynić to bardziej niezawodnym, możesz dodać więcej testów poprawności i więcej punktów synchronizacji.
Wniosek
W tym samouczku dodałeś ostatnie szlify do swojej gry typu endless runner i zaimplementowałeś synchronizację w czasie rzeczywistym klienta stacjonarnego z klientem mobilnym, skutecznie odzwierciedlając ekran urządzenia mobilnego na twoim komputerze. To kończy serię o budowaniu niekończącej się gry biegacza w wirtualnej rzeczywistości. Wraz z technikami A-Frame VR poznałeś modelowanie 3D, komunikację klient-klient i inne szeroko stosowane koncepcje.
Kolejne kroki mogą obejmować:
- Bardziej zaawansowane modelowanie
Oznacza to bardziej realistyczne modele 3D, potencjalnie tworzone w oprogramowaniu innej firmy i importowane. Na przykład (MagicaVoxel) ułatwia tworzenie grafiki wokselowej, a (Blender) to kompletne rozwiązanie do modelowania 3D. - Większa złożoność
Bardziej złożone gry, takie jak strategia czasu rzeczywistego, mogą wykorzystywać silnik innej firmy w celu zwiększenia wydajności. Może to oznaczać całkowite ominięcie A-Frame i webVR, zamiast opublikowania skompilowanej gry (Unity3d).
Inne możliwości obejmują obsługę trybu wieloosobowego i bogatszą grafikę. Po zakończeniu tej serii samouczków masz teraz ramy do dalszego zbadania.