Jak zbudować niekończącą się grę biegacza w wirtualnej rzeczywistości (część 2)
Opublikowany: 2022-03-10W części 1 tej serii zobaczyliśmy, jak można stworzyć model rzeczywistości wirtualnej z efektami oświetlenia i animacji. W tej części zaimplementujemy podstawową logikę gry i wykorzystamy bardziej zaawansowane manipulacje środowiskiem A-Frame do zbudowania części „gry” tej aplikacji. Pod koniec będziesz mieć działającą grę w wirtualnej rzeczywistości z prawdziwym wyzwaniem.
Ten samouczek obejmuje szereg kroków, w tym (ale nie tylko) wykrywanie kolizji i więcej koncepcji A-Frame, takich jak mixiny.
- Demo produktu końcowego
Warunki wstępne
Podobnie jak w poprzednim samouczku, będziesz potrzebować:
- dostęp do Internetu (w szczególności do glitch.com);
- Projekt Glitch ukończony od części 1. (Możesz kontynuować od gotowego produktu, przechodząc do https://glitch.com/edit/#!/ergo-1 i klikając „Zremiksuj do edycji”;
- Gogle wirtualnej rzeczywistości (opcjonalne, zalecane). (Używam Google Cardboard, który jest oferowany w cenie 15 USD za sztukę.)
Krok 1: Projektowanie przeszkód
W tym kroku projektujesz drzewa, które wykorzystamy jako przeszkody. Następnie dodasz prostą animację, która przesuwa drzewa w kierunku gracza, na przykład:
Te drzewa będą służyć jako szablony przeszkód, które generujesz podczas gry. W końcowej części tego kroku usuniemy te „drzewa szablonów”.
Na początek dodaj kilka różnych mixinów A-Frame . Mieszanki to powszechnie używane zestawy właściwości komponentów. W naszym przypadku wszystkie nasze drzewa będą miały ten sam kolor, wysokość, szerokość, głębokość itp. Innymi słowy, wszystkie Twoje drzewa będą wyglądały tak samo i dlatego będą korzystać z kilku wspólnych mixinów.
Uwaga : w naszym samouczku jedynymi zasobami będą mixiny. Odwiedź stronę A-Frame Mixins, aby dowiedzieć się więcej.
W edytorze przejdź do index.html . Zaraz po niebie i przed światłami dodaj nowy element A-Frame, aby przechowywać swoje zasoby:
<a-sky...></a-sky> <!-- Mixins --> <a-assets> </a-assets> <!-- Lights --> ...
W swoim nowym elemencie a-assets
zacznij od dodania domieszki dla swoich liści. Te domieszki definiują wspólne właściwości listowia drzewa szablonu. Krótko mówiąc, jest to biała piramida o płaskim cieniu, zapewniająca efekt 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>
Tuż pod mieszanką liści dodaj mieszankę do pnia. Ten kufer będzie małym, białym prostokątnym pryzmatem.
<a-assets> ... <a-mixin geometry=" primitive: box; height:0.5; width:0.1; depth:0.1;" material="color:white;"></a-mixin> </a-assets>
Następnie dodaj obiekty drzewa szablonów, które będą korzystać z tych domieszek. Nadal w index.html , przewiń w dół do sekcji platform. Tuż przed sekcją gracza dodaj nową sekcję drzewa z trzema pustymi elementami drzewa:
<a-entity ...> <!-- Trees --> <a-entity></a-entity> <a-entity></a-entity> <a-entity></a-entity> <!-- Player --> ...
Następnie zmień położenie, przeskaluj i dodaj cienie do elementów drzewa.
<!-- 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>
Teraz zapełnij elementy drzewa pniem i listowiem, używając wcześniej zdefiniowanych domieszek.
<!-- 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>
Przejdź do podglądu i powinieneś teraz zobaczyć następujące drzewa szablonów.
Teraz animuj drzewa z odległego miejsca na platformie w kierunku użytkownika. Tak jak poprzednio, użyj tagu 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>
Upewnij się, że Twój kod jest zgodny z poniższym.
<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 --> ...
Przejdź do podglądu, a zobaczysz teraz drzewa zbliżające się do Ciebie.
Wróć do swojego edytora. Tym razem wybierz asset/ergo.js . W sekcji gry skonfiguruj drzewa po załadowaniu okna.
/******** * GAME * ********/ ... window.onload = function() { setupTrees(); }
Pod kontrolkami, ale przed sekcją Gra, dodaj nową sekcję TREES
. W tej sekcji zdefiniuj nową funkcję setupTrees
.
/************ * CONTROLS * ************/ ... /********* * TREES * *********/ function setupTrees() { } /******** * GAME * ********/ ...
W nowej funkcji setupTrees
uzyskaj odniesienia do obiektów DOM drzewa szablonów i udostępnij je globalnie.
/********* * 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'); }
Następnie zdefiniuj nowe narzędzie removeTree
. Za pomocą tego narzędzia możesz następnie usunąć drzewa szablonów ze sceny. Pod funkcją setupTrees
zdefiniuj nowe narzędzie.
function setupTrees() { ... } function removeTree(tree) { tree.parentNode.removeChild(tree); }
Wróć do setupTrees
, użyj nowego narzędzia, aby usunąć drzewa szablonów.
function setupTrees() { ... removeTree(templateTreeLeft); removeTree(templateTreeRight); removeTree(templateTreeCenter); }
Upewnij się, że sekcje drzewa i gry odpowiadają następującym:
/********* * 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(); }
Otwórz ponownie podgląd, a twoje drzewa powinny być teraz nieobecne. Podgląd powinien pasować do naszej gry na początku tego samouczka.
Na tym kończy się projektowanie drzewa szablonów.
W tym kroku omówiliśmy i wykorzystaliśmy domieszki A-Frame, które pozwalają nam uprościć kod poprzez zdefiniowanie wspólnych właściwości. Ponadto wykorzystaliśmy integrację A-Frame z DOM, aby usunąć obiekty ze sceny A-Frame VR.
W następnym kroku stworzymy wiele przeszkód i zaprojektujemy prosty algorytm do rozmieszczania drzew na różnych pasach.
Krok 2: Przeszkody w tarle
W niekończącej się grze biegacza naszym celem jest unikanie przeszkód lecących w naszym kierunku. W tej konkretnej realizacji gry, jak zwykle, używamy trzech pasów.
W przeciwieństwie do większości niekończących się gier dla biegaczy, ta gra obsługuje tylko ruch w lewo i w prawo . Nakłada to ograniczenie na nasz algorytm odradzania przeszkód: nie możemy mieć trzech przeszkód na wszystkich trzech pasach jednocześnie lecących w naszym kierunku. Gdyby tak się stało, gracz miałby zerową szansę na przeżycie. W rezultacie nasz algorytm odradzania musi uwzględniać to ograniczenie.
Na tym etapie wszystkie nasze zmiany w kodzie zostaną wprowadzone w asset/ergo.js . Plik HTML pozostanie taki sam. Przejdź do sekcji TREES
w asset/ergo.js .
Na początek dodamy narzędzia do odradzania drzew. Każde drzewo będzie wymagało unikalnego identyfikatora, który naiwnie zdefiniujemy jako liczbę drzew, które istnieją, gdy drzewo się odradza. Zacznij od śledzenia liczby drzew w zmiennej globalnej.
/********* * TREES * *********/ ... var numberOfTrees = 0; function setupTrees() { ...
Następnie zainicjujemy referencję do elementu DOM kontenera drzewa, do którego nasza funkcja spawn doda drzewa. Nadal w sekcji TREES
dodaj zmienną globalną, a następnie wykonaj odwołanie.
... var treeContainer; var numberOfTrees ... function setupTrees() { ... templateTreeRight = ... treeContainer = document.getElementById('tree-container'); removeTree(...); ... }
Używając zarówno liczby drzew, jak i kontenera drzewa, napisz nową funkcję, która tworzy drzewa.
function removeTree(tree) { ... } function addTree(el) { numberOfTrees += 1; el.id = 'tree-' + numberOfTrees; treeContainer.appendChild(el); } ...
Aby ułatwić późniejsze użytkowanie, utworzysz drugą funkcję, która doda właściwe drzewo do właściwego pasa. Na początek zdefiniuj nową tablicę templates
w sekcji TREES
.
var templates; var treeContainer; ... function setupTrees() { ... templates = [templateTreeLeft, templateTreeCenter, templateTreeRight]; removeTree(...); ... }
Korzystając z tej tablicy szablonów, dodaj narzędzie, które tworzy drzewa na określonym pasie, mając identyfikator reprezentujący lewą, środkową lub prawą stronę.
function function addTree(el) { ... } function addTreeTo(position_index) { var template = templates[position_index]; addTree(template.cloneNode(true)); }
Przejdź do podglądu i otwórz konsolę programisty. W konsoli programisty wywołaj globalną funkcję addTreeTo
.
> addTreeTo(0); # spawns tree in left lane
Teraz napiszesz algorytm, który losowo odradza drzewa:
- Wybierz losowo pas (który nie został jeszcze wybrany dla tego przedziału czasowego);
- Stwórz drzewo z pewnym prawdopodobieństwem;
- Jeśli maksymalna liczba drzew została odradzana w tym przedziale czasowym, zatrzymaj się. W przeciwnym razie powtórz krok 1.
Aby zastosować ten algorytm, zamiast tego przetasujemy listę szablonów i będziemy przetwarzać je pojedynczo. Zacznij od zdefiniowania nowej funkcji addTreesRandomly
, która akceptuje wiele różnych argumentów słów kluczowych.
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 } = {}) { }
W nowej funkcji addTreesRandomly
zdefiniuj listę drzew szablonów i przetasuj listę.
function addTreesRandomly( ... ) { var trees = [ {probability: probTreeLeft, position_index: 0}, {probability: probTreeCenter, position_index: 1}, {probability: probTreeRight, position_index: 2}, ] shuffle(trees); }
Przewiń w dół pliku i utwórz nową sekcję narzędzi wraz z nowym narzędziem do odtwarzania shuffle
. To narzędzie przetasuje tablicę w miejscu.
/******** * 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; }
Wróć do funkcji addTreesRandomly
w sekcji Drzewa. Dodaj nową zmienną numberOfTreesAdded
i przeprowadź iterację listy drzew zdefiniowanych powyżej.
function addTreesRandomly( ... ) { ... var numberOfTreesAdded = 0; trees.forEach(function (tree) { }); }
W iteracji nad drzewami odradzaj drzewo tylko z pewnym prawdopodobieństwem i tylko wtedy, gdy liczba dodanych drzew nie przekracza 2
. Zaktualizuj pętlę for w następujący sposób.
function addTreesRandomly( ... ) { ... trees.forEach(function (tree) { if (Math.random() < tree.probability && numberOfTreesAdded < maxNumberTrees) { addTreeTo(tree.position_index); numberOfTreesAdded += 1; } }); }
Aby zakończyć funkcję, zwróć liczbę dodanych drzew.
function addTreesRandomly( ... ) { ... return numberOfTreesAdded; }
Dokładnie sprawdź, czy funkcja addTreesRandomly
odpowiada następującym.
/** * 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; }
Na koniec, aby automatycznie odradzać drzewa, skonfiguruj zegar, który uruchamia się w regularnych odstępach czasu. Zdefiniuj czasomierz globalnie i dodaj nową funkcję rozkładania dla tego czasomierza.
/********* * TREES * *********/ ... var treeTimer; function setupTrees() { ... } function teardownTrees() { clearInterval(treeTimer); }
Następnie zdefiniuj nową funkcję, która inicjuje zegar i zapisuje zegar we wcześniej zdefiniowanej zmiennej globalnej. Poniższy timer jest uruchamiany co pół sekundy.
function addTreesRandomlyLoop({intervalLength = 500} = {}) { treeTimer = setInterval(addTreesRandomly, intervalLength); }
Na koniec uruchom stoper po załadowaniu okna, z sekcji Gra.
/******** * GAME * ********/ ... window.onload = function() { ... addTreesRandomlyLoop(); }
Przejdź do podglądu, a zobaczysz losowo pojawiające się drzewa. Zauważ, że nigdy nie ma trzech drzew na raz.
To kończy etap przeszkód. Udało nam się zebrać wiele drzew szablonów i wygenerować nieskończoną liczbę przeszkód z szablonów. Nasz algorytm odradzania uwzględnia również naturalne ograniczenia w grze, dzięki czemu można w nią grać.
W następnym kroku dodajmy testy kolizyjne.
Krok 3: Testowanie kolizji
W tej sekcji zaimplementujemy testy kolizji między przeszkodami a graczem. Te testy kolizji są prostsze niż testy kolizji w większości innych gier; jednak gracz porusza się tylko wzdłuż osi x, więc za każdym razem, gdy drzewo przecina oś x, sprawdź, czy pas drzewa jest taki sam jak pas gracza. Zaimplementujemy ten prosty test w tej grze.
Przejdź do index.html , w dół do sekcji TREES
. Tutaj dodamy informacje o pasie do każdego z drzew. Dla każdego drzewa dodaj data-tree-position-index=
w następujący sposób. Dodatkowo dodaj class="tree"
, dzięki czemu możemy łatwo wybrać wszystkie drzewa w linii:
<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>
Przejdź do asset/ergo.js i wywołaj nową funkcję setupCollisions
w sekcji GAME
. Ponadto zdefiniuj nową zmienną globalną isGameRunning
, która wskazuje, czy istniejąca gra jest już uruchomiona.
/******** * GAME * ********/ var isGameRunning = false; setupControls(); setupCollision(); window.onload = function() { ...
Zdefiniuj nową sekcję COLLISIONS
zaraz za sekcją TREES
, ale przed sekcją Gra. W tej sekcji zdefiniuj funkcję 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 * ********/
Tak jak poprzednio, zarejestrujemy komponent AFRAME i użyjemy detektora zdarzeń tick
, aby uruchomić kod w każdym kroku czasowym. W takim przypadku zarejestrujemy komponent w player
i przeprowadzimy testy na wszystkich drzewach w tym odbiorniku:
function setupCollisions() { AFRAME.registerComponent('player', { tick: function() { document.querySelectorAll('.tree').forEach(function(tree) { } } } }
W pętli for
zacznij od uzyskania odpowiednich informacji o drzewie:
document.querySelectorAll('.tree').forEach(function(tree) { position = tree.getAttribute('position'); tree_position_index = tree.getAttribute('data-tree-position-index'); tree_id = tree.getAttribute('id'); }
Następnie, nadal w pętli for
, usuń drzewo, jeśli jest poza zasięgiem wzroku, zaraz po wyodrębnieniu właściwości drzewa:
document.querySelectorAll('.tree').forEach(function(tree) { ... if (position.z > POSITION_Z_OUT_OF_SIGHT) { removeTree(tree); } }
Następnie, jeśli nie ma uruchomionej gry, nie sprawdzaj, czy nie ma kolizji.
document.querySelectorAll('.tree').forEach(function(tree) { if (!isGameRunning) return; }
Na koniec (nadal w pętli for
) sprawdź, czy drzewo dzieli w tym samym czasie tę samą pozycję z graczem. Jeśli tak, wywołaj funkcję gameOver
, która nie została jeszcze zdefiniowana:
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(); } }
Sprawdź, czy funkcja setupCollisions
jest zgodna z następującymi parametrami:
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(); } }) } }) }
Na tym kończy się konfiguracja kolizji. Teraz dodamy kilka drobiazgów, aby oddzielić sekwencje startGame
i gameOver
. Przejdź do sekcji GAME
. Zaktualizuj blok window.onload
, aby pasował do poniższego, zastępując addTreesRandomlyLoop
startGame
której jeszcze nie zdefiniowano.
window.onload = function() { setupTrees(); startGame(); }
Pod wywołaniami funkcji konfiguracji utwórz nową funkcję startGame
. Ta funkcja zainicjuje odpowiednio zmienną isGameRunning
i zapobiegnie zbędnym wywołaniom.
window.onload = function() { ... } function startGame() { if (isGameRunning) return; isGameRunning = true; addTreesRandomlyLoop(); }
Na koniec zdefiniuj gameOver
, co spowoduje wyświetlenie komunikatu „Game Over!” wiadomość na razie.
function startGame() { ... } function gameOver() { isGameRunning = false; alert('Game Over!'); teardownTrees(); }
Na tym kończy się sekcja testowania kolizji w grze typu endless runner.
W tym kroku ponownie użyliśmy komponentów A-Frame i wielu innych narzędzi, które dodaliśmy wcześniej. Dodatkowo przeorganizowaliśmy i odpowiednio wyabstrahowaliśmy funkcje gry; będziemy następnie rozszerzać te funkcje gry, aby uzyskać pełniejsze wrażenia z gry.
Wniosek
W części 1 dodaliśmy sterowanie przyjazne dla gogli VR: patrz w lewo, aby poruszać się w lewo, i w prawo, aby poruszać się w prawo. W drugiej części serii pokazałem, jak łatwo można zbudować podstawową, działającą grę w wirtualnej rzeczywistości. Dodaliśmy logikę gry, aby niekończący się biegacz spełniał Twoje oczekiwania: biegnij w nieskończoność i miej niekończące się serie niebezpiecznych przeszkód lecących w kierunku gracza. Do tej pory zbudowałeś działającą grę z bezklawiaturową obsługą zestawów słuchawkowych rzeczywistości wirtualnej.
Oto dodatkowe zasoby dotyczące różnych elementów sterujących i gogli VR:
- Ramka A do gogli VR
Ankieta przeglądarek i zestawów słuchawkowych obsługiwanych przez A-Frame VR. - Ramka A do kontrolerów VR
Jak A-Frame obsługuje brak kontrolerów, kontrolery 3DoF i kontrolery 6DoF, oprócz innych alternatyw dla interakcji.
W kolejnej części dodamy kilka ostatnich szlifów i zsynchronizujemy stany gry , co przybliży nas o krok do rozgrywek wieloosobowych.