Jak zbudować wieloosobową grę wirtualną w czasie rzeczywistym (część 2)
Opublikowany: 2022-03-10W tej serii samouczków zbudujemy internetową grę wieloosobową w wirtualnej rzeczywistości, w której gracze będą musieli współpracować, aby rozwiązać zagadkę. W pierwszej części tej serii zaprojektowaliśmy kule występujące w grze. W tej części serii dodamy mechanikę gry i ustawimy protokoły komunikacji między parami graczy.
Opis gry pochodzi z pierwszej części serii: Każda para graczy otrzymuje pierścień kul. Celem jest „włączenie” wszystkich kul, przy czym kula jest „włączona”, jeśli jest uniesiona i jasna. Kula jest „wyłączona”, jeśli jest niższa i słaba. Jednak niektóre „dominujące” kule wpływają na swoich sąsiadów: jeśli zmienią stan, ich sąsiedzi również zmienią stan. Gracz 2 może kontrolować kule o numerach parzystych, a gracz 1 może kontrolować kule o numerach nieparzystych. Zmusza to obu graczy do współpracy w celu rozwiązania zagadki.
8 kroków w tym samouczku podzielono na 3 sekcje:
- Wypełnianie interfejsu użytkownika (kroki 1 i 2)
- Dodaj mechanikę gry (kroki 3 do 5)
- Konfiguracja komunikacji (kroki 6 do 8)
Ta część zakończy się w pełni funkcjonalnym demo online, w którym każdy może zagrać. Będziesz używać A-Frame VR i kilku rozszerzeń A-Frame.
Gotowy kod źródłowy znajdziesz tutaj.
1. Dodaj wskaźniki wizualne
Na początek dodamy wizualne wskaźniki identyfikatora kuli. Wstaw nowy element a-text
VR jako pierwszy element podrzędny #container-orb0
na 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>
„Zależności” kuli to kule, które będą się przełączać po przełączeniu: na przykład, powiedzmy, że kula 1 ma zależności kule 2 i 3. Oznacza to, że jeśli kula 1 jest przełączana, kule 2 i 3 również zostaną przełączone. Dodamy wizualne wskaźniki zależności, jak następuje, bezpośrednio po .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>
Sprawdź, czy Twój kod pasuje do naszego kodu źródłowego dla kroku 1. Twoja kula powinna teraz odpowiadać następującym:
To kończy dodatkowe wizualne wskaźniki, których będziemy potrzebować. Następnie dynamicznie dodamy kule do sceny VR, używając tej kuli szablonu.
2. Dynamicznie dodawaj kule
W tym kroku dodamy kule zgodnie ze specyfikacją poziomu w formacie JSON. Pozwala nam to w łatwy sposób określać i generować nowe poziomy. Użyjemy kuli z ostatniego kroku w części 1 jako szablonu.
Na początek zaimportuj jQuery, ponieważ ułatwi to modyfikacje DOM, a tym samym modyfikacje sceny VR. Bezpośrednio po imporcie A-Frame dodaj następujące elementy do L8:
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
Określ poziom za pomocą szyku. Tablica będzie zawierać literały obiektów, które kodują „zależności” każdej kuli. Wewnątrz tagu <head>
dodaj następującą konfigurację poziomu:
<script> var orbs = [ {left: 1, right: 4}, {}, {on: true}, {}, {on: true} ]; </script>
Na razie każda kula może mieć tylko jedną zależność po „prawej” stronie i jedną po „lewej”. Natychmiast po zadeklarowaniu orbs
powyżej, dodaj procedurę obsługi, która będzie działać podczas ładowania strony. Ten program obsługi (1) zduplikuje kulę szablonu i (2) usunie kulę szablonu, korzystając z dostarczonej konfiguracji poziomu:
$(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) {}
Następnie wypełnij funkcję remove
, która po prostu usuwa element ze sceny VR, mając selektor. Na szczęście A-Frame obserwuje zmiany w DOM i dlatego usunięcie elementu z DOM wystarczy, aby usunąć go ze sceny VR. Wypełnij funkcję remove
w następujący sposób.
function remove(selector) { var el = document.querySelector(selector); el.parentNode.removeChild(el); }
Wypełnij funkcję clickOrb
, która po prostu uruchamia akcję kliknięcia na kuli.
function clickOrb(i) { document.querySelector("#container-orb" + i).click(); }
Następnie zacznij pisać funkcję populateTemplate
. W tej funkcji zacznij od .container
. Ten pojemnik na kulę dodatkowo zawiera wizualne wskaźniki, które dodaliśmy w poprzednim kroku. Co więcej, będziemy musieli zmodyfikować zachowanie onclick
kuli w oparciu o jej zależności. Jeśli istnieje zależność od lewej, zmodyfikuj zarówno wskaźnik wizualny, jak i zachowanie przy onclick
, aby to odzwierciedlić; to samo dotyczy prawo-zależności:
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(); } }
Nadal w funkcji populateTemplate
ustaw poprawnie identyfikator kuli we wszystkich elementach kuli i jej kontenera.
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);
Nadal w funkcji populateTemplate
ustaw zachowanie onclick
, ustaw losowe ziarno, aby każda kula była wizualnie inna, a na koniec ustaw pozycję obrotową kuli na podstawie jej identyfikatora.
container.attr('onclick', onclick); container.find('lp-sphere').attr('seed', i); template.attr('rotation', '0 ' + (360 / total * i) + ' 0');
Na zakończenie funkcji zwróć template
ze wszystkimi powyższymi konfiguracjami.
return template;
Wewnątrz modułu obsługi ładowania dokumentu i po usunięciu szablonu za pomocą remove('#template')
włącz kule, które zostały skonfigurowane jako włączone początkowo.
$(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); });
Na tym kończy się modyfikacja Javascript. Następnie zmienimy domyślne ustawienia szablonu na ustawienia „wyłączonej” kuli. Zmień położenie i skalę #container-orb0
na następujące:
position="8 0.5 0" scale="0.5 0.5 0.5"
Następnie zmień intensywność #light-orb0
na 0.
intensity="0"
Sprawdź, czy Twój kod źródłowy jest zgodny z naszym kodem źródłowym w kroku 2.
Twoja scena VR powinna teraz zawierać 5 dynamicznie wypełnianych kul. Jedna z kul powinna ponadto posiadać wizualne wskaźniki zależności, jak poniżej:
Na tym kończy się pierwsza sekcja dotycząca dynamicznego dodawania kul. W następnej sekcji poświęcimy trzy kroki na dodawanie mechaniki gry. W szczególności gracz będzie mógł przełączać tylko określone kule w zależności od identyfikatora gracza.
3. Dodaj stan terminala
W tym kroku dodamy stan terminala. Jeśli wszystkie kule zostaną pomyślnie włączone, gracz zobaczy stronę „zwycięstwa”. Aby to zrobić, musisz śledzić stan wszystkich kul. Za każdym razem, gdy kula jest włączana lub wyłączana, będziemy musieli zaktualizować nasz stan wewnętrzny. Powiedzmy, że funkcja pomocnicza toggleOrb
stan aktualizacji dla nas. Wywołaj funkcję toggleOrb
za każdym razem, gdy kula zmienia stan: (1) dodaj detektor kliknięć do modułu obsługi onload i (2) dodaj toggleOrb(i);
wywołanie clickOrb
. Na koniec (3) zdefiniuj pusty toggleOrb
.
$(document).ready(function() { ... $('.orb').on('click', function() { var id = $(this).attr('data-id') toggleOrb(id); }); }); function toggleOrb(i) {} function clickOrb(i) { ... toggleOrb(i); }
Dla uproszczenia użyjemy naszej konfiguracji poziomów do wskazania stanu gry. Użyj toggleOrb
, aby przełączyć stan on
dla kuli ith. toggleOrb
może dodatkowo wywołać stan terminala, jeśli wszystkie kule są włączone.
function toggleOrb(i) { orbs[i].on = !orbs[i].on; if (orbs.every(orb => orb.on)) console.log('Victory!'); }
Sprawdź dokładnie, czy Twój kod jest zgodny z naszym kodem źródłowym w kroku 3.
Na tym kończy się tryb „dla jednego gracza” w grze. W tym momencie masz w pełni funkcjonalną grę w wirtualnej rzeczywistości. Jednak teraz będziesz musiał napisać komponent dla wielu graczy i zachęcić do współpracy za pomocą mechaniki gry.
4. Utwórz obiekt gracza
W tym kroku stworzymy abstrakcję dla gracza z identyfikatorem gracza. Ten identyfikator gracza zostanie później przydzielony przez serwer.
Na razie będzie to po prostu zmienna globalna. Bezpośrednio po zdefiniowaniu orbs
, zdefiniuj ID gracza:
var orbs = ... var current_player_id = 1;
Sprawdź dokładnie, czy Twój kod pasuje do naszego kodu źródłowego dla kroku 4. W następnym kroku ten identyfikator gracza zostanie użyty do określenia, które kule gracz może kontrolować.
5. Warunkowo przełącz kule
W tym kroku zmodyfikujemy zachowanie przełączania kul. W szczególności gracz 1 może kontrolować kule o numerach nieparzystych, a gracz 2 może kontrolować kule o numerach parzystych. Najpierw zaimplementuj tę logikę w obu miejscach, w których kule zmieniają stan:
$('.orb').on('click', function() { var id = ... if (!allowedToToggle(id)) return false; ... } ... function clickOrb(i) { if (!allowedToToggle(id)) return; ... }
Po drugie, zdefiniuj funkcję allowedToToggle
, zaraz po clickOrb
. Jeśli aktualnym graczem jest gracz 1, identyfikatory o nieparzystych numerach zwrócą wartość true-y, a zatem gracz 1 będzie mógł kontrolować nieparzyste kule. Odwrotna sytuacja dotyczy gracza 2. Wszyscy pozostali gracze nie mogą kontrolować kul.
function allowedToToggle(id) { if (current_player_id == 1) { return id % 2; } else if (current_player_id == 2) { return !(id % 2); } return false; }
Sprawdź dokładnie, czy Twój kod pasuje do naszego kodu źródłowego dla Kroku 5. Domyślnie graczem jest gracz 1. Oznacza to, że jako gracz 1 możesz kontrolować tylko nieparzyste kule w podglądzie. To kończy sekcję dotyczącą mechaniki gry.
W kolejnej sekcji ułatwimy komunikację między obydwoma graczami za pośrednictwem serwera.
6. Konfiguracja serwera z WebSocket
W tym kroku skonfigurujesz prosty serwer, aby (1) śledzić identyfikatory graczy i (2) przekazywać wiadomości. Wiadomości te będą zawierać stan gry, dzięki czemu gracze będą mieć pewność, że każdy widzi to, co widzi drugi.
Twój poprzedni index.html
będziemy odnosić się do kodu źródłowego po stronie klienta. W tym kroku będziemy odnosić się do kodu jako do kodu źródłowego po stronie serwera. Przejdź do glitch.com, kliknij „nowy projekt” w prawym górnym rogu, a na liście rozwijanej kliknij „hello-express”.
Z panelu po lewej stronie wybierz „package.json” i dodaj socket-io
do dependencies
. Twój słownik dependencies
powinien teraz odpowiadać następującym.
"dependencies": { "express": "^4.16.4", "socketio": "^1.0.0" },
Z panelu po lewej stronie wybierz „index.js” i zastąp zawartość tego pliku następującym minimalnym 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); });
Powyższe konfiguruje socket.io na porcie 3000 dla podstawowej aplikacji ekspresowej. Następnie zdefiniuj dwie zmienne globalne, jedną do utrzymywania listy aktywnych graczy, a drugą do utrzymywania najmniejszego nieprzypisanego identyfikatora gracza.
/** * Maintain player IDs */ var playerIds = []; var smallestPlayerId = 1;
Następnie zdefiniuj funkcję getPlayerId
, która generuje nowy identyfikator gracza i oznacza nowy identyfikator gracza jako „zabrany”, dodając go do tablicy playerIds
. W szczególności funkcja po prostu oznacza smallestPlayerId
, a następnie aktualizuje smallestPlayerId
, wyszukując następną najmniejszą niepobraną liczbę całkowitą.
function getPlayerId() { var playerId = smallestPlayerId; playerIds.push(playerId); while (playerIds.includes(smallestPlayerId)) { smallestPlayerId++; } return playerId; }
Zdefiniuj funkcję removePlayer
, która odpowiednio aktualizuje smallestPlayerId
i zwalnia podany playerId
, aby inny gracz mógł go pobrać.
function removePlayer(playerId) { if (playerId < smallestPlayerId) { smallestPlayerId = playerId; } var index = playerIds.indexOf(playerId); playerIds.splice(index, 1); }
Na koniec zdefiniuj parę programów obsługi zdarzeń gniazda, które rejestrują nowe odtwarzacze i wyrejestrowują odłączone odtwarzacze, używając powyższej pary metod.
/** * 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); }); });
Sprawdź dokładnie, czy Twój kod jest zgodny z naszym kodem źródłowym w kroku 6. To kończy podstawową rejestrację i wyrejestrowanie gracza. Każdy klient może teraz używać identyfikatora gracza wygenerowanego przez serwer.
W następnym kroku zmodyfikujemy klienta, aby otrzymywał i używał identyfikatora gracza emitowanego przez serwer.
7. Zastosuj identyfikator gracza
W tych dwóch kolejnych krokach ukończymy podstawową wersję rozgrywki wieloosobowej. Aby rozpocząć, zintegruj przypisanie identyfikatora gracza po stronie klienta. W szczególności każdy klient poprosi serwer o identyfikator gracza. Wróć do index.html
po stronie klienta, nad którym pracowaliśmy w krokach 4 i wcześniejszych.
Importuj socket.io
w head
w L7:
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
Po procedurze obsługi ładowania dokumentu utwórz wystąpienie gniazda i wyemituj zdarzenie newPlayer
. W odpowiedzi strona serwera wygeneruje nowy identyfikator gracza przy użyciu zdarzenia playerId
. Poniżej użyj adresu URL do podglądu projektu Glitch zamiast lightful.glitch.me
. Zachęcamy do korzystania z poniższego demonstracyjnego adresu URL, ale wszelkie wprowadzone przez Ciebie zmiany w kodzie nie zostaną oczywiście odzwierciedlone.
$(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); });
Sprawdź, czy Twój kod jest zgodny z naszym kodem źródłowym dla kroku 7. Teraz możesz załadować swoją grę w dwóch różnych przeglądarkach lub na dwóch różnych kartach, aby grać w dwie strony gry wieloosobowej. Gracz 1 będzie mógł kontrolować kule o numerach nieparzystych, a gracz 2 będzie mógł kontrolować kule o numerach parzystych.
Pamiętaj jednak, że przełączanie kul dla gracza 1 nie wpłynie na stan kul dla gracza 2. Następnie musimy zsynchronizować stany gry.
8. Synchronizuj stan gry
W tym kroku zsynchronizujemy stany gry, aby gracze 1 i 2 widzieli te same stany kul. Jeśli kula 1 jest włączona dla gracza 1, powinna być również włączona dla gracza 2. Po stronie klienta będziemy zarówno ogłaszać, jak i nasłuchiwać przełączania kul. Aby ogłosić, po prostu przekażemy ID przełączanej kuli.
Przed obydwoma wywołaniami toggleOrb
dodaj następujące wywołanie socket.emit
.
$(document).ready(function() { ... $('.orb').on('click', function() { ... socket.emit('toggleOrb', id); toggleOrb(id); }); }); ... function clickOrb(i) { ... socket.emit('toggleOrb', i); toggleOrb(i); }
Następnie posłuchaj przełączników kuli i przełącz odpowiednią kulę. Bezpośrednio pod detektorem zdarzeń gniazda playerId
dodaj kolejny detektor dla zdarzenia toggleOrb
.
socket.on('toggleOrb', function(i) { document.querySelector("#container-orb" + i).click(); toggleOrb(i); });
Na tym kończy się modyfikacja kodu po stronie klienta. Sprawdź dokładnie, czy Twój kod jest zgodny z naszym kodem źródłowym w kroku 8.
Po stronie serwera musi teraz odbierać i rozgłaszać przełączony identyfikator kuli. W index.js
po stronie serwera dodaj następujący odbiornik. Ten odbiornik powinien być umieszczony bezpośrednio pod słuchaczem disconnect
gniazda.
socket.on('toggleOrb', function(i) { socket.broadcast.emit('toggleOrb', i); });
Sprawdź dokładnie, czy twój kod pasuje do naszego kodu źródłowego dla kroku 8. Teraz, gracz 1 załadowany w jednym oknie i gracz 2 załadowany w drugim oknie, zobaczą ten sam stan gry. Dzięki temu ukończyłeś wieloosobową grę w wirtualnej rzeczywistości. Co więcej, obaj gracze muszą współpracować, aby osiągnąć cel. Ostateczny produkt będzie pasował do następujących.
Wniosek
To kończy nasz samouczek dotyczący tworzenia wieloosobowej gry w wirtualnej rzeczywistości. W trakcie tego poruszyłeś wiele tematów, w tym modelowanie 3D w A-Frame VR i gry wieloosobowe w czasie rzeczywistym przy użyciu WebSockets.
Opierając się na koncepcjach, które poruszyliśmy, w jaki sposób zapewnilibyście płynniejszą rozgrywkę dla dwóch graczy? Może to obejmować sprawdzenie, czy stan gry jest zsynchronizowany, i powiadomienie użytkownika, jeśli jest inaczej. Możesz także tworzyć proste wizualne wskaźniki stanu terminala i stanu połączenia odtwarzacza.
Biorąc pod uwagę ramy, które stworzyliśmy i wprowadzone przez nas koncepcje, masz teraz narzędzia, które pozwolą Ci odpowiedzieć na te pytania i stworzyć znacznie więcej.
Gotowy kod źródłowy znajdziesz tutaj.