GCM – Maj: Angular RPG. Poruszanie się po mapie.

Muszę przyznać, że póki co, tworzenie gry w Angularze sprawia mi ogromną frajdę. W ciągu zaledwie paru godzin udało mi się skonstruować całkiem solidne podstawy pod grę tekstową. W Angular RPG mogę już z łatwością tworzyć lokacje, a gracz, może bez trudu się po tych lokacjach poruszać.

Nie wydaje się żeby to było wiele, ale oprócz tego musiałem stworzyć też skromny ale, mam nadzieję, efektywny silnik gry.

Angulalr RPG

Jak zwykle aby przetestować aktualną wersję aplikacji, należy kliknąć na obrazek powyżej. Na moim gicie pojawiło się też nowe repo z całym kodem projektu.

Jakby co, wygląd aplikacji/gry to jeszcze wersja wczesno-robocza. Postaram się, żeby ostateczna forma była ładniejsza/czytelniejsza 🙂 . Nie ma co się rozdrabniać. Przejdę od razu do kodu, bo trochę go jest a chciałbym wszystko objaśnić 🙂 .

Angular RPG – kod

Na początek omówię zawartość pliku indeks.html, a dokładniej to treścią elementu body:

Dokument podzieliłem na dwie części. Jedna z nich podlega kontrolerowi mainController, druga ma przypisaną dyrektywę ng-view. Idea jest taka, że pierwszy div wyświetla komunikaty gry. Na przykład informacje o tym gdzie gracz się teraz znajduje, czy w pobliżu są jakieś przedmioty, czy coś go nie atakuje. Ten element aplikacji jest zawsze widoczny. Druga część będzie wyświetlała te dane, które gracz będzie aktualnie potrzebował. Mogą to przykładowo być: ekwipunek, statystyki postaci, mapa świata.

Póki co tylko pierwszy fragment jest dobrze rozwinięty. Zawiera on element p z dyrektywą ng-repeat. Dyrektywa ta będzie przechodzić przez zawartość tablicy przetrzymującej wszystkie wiadomości dla gracza. Dla każdej z nich powstanie nowy paragraf. W ten sposób gracz będzie miał dostęp do wszystkich wiadomości z danej rozgrywki.

Dodatkowo, pod divem zawierającym te informacje znajdują się cztery przyciski. Każdy z tych przycisków reprezentuje akcje jaką może podjąć gracz. Obecnie dostępne są tylko akcje ruchu w czterech kierunkach świata. Dyrektywy ng-disable powodują, że przyciski nie są aktywne, gdy funkcja sprawdzająca zwróci odpowiednią wartość. W tym wypadku, sprawdzane będzie, czy z aktualnej lokacji gracza istnieje ścieżka w danym kierunku. Jeżeli nie, przycisk nie jest aktywny.

Kliknięcie przycisku, oczywiście wywoła daną akcję.

Kolejny plik to module.js:

Jak widać nie zawiera on zbyt wiele. Jest to tylko definicja modułu. Dodatkowo w tablicy zależności, dołączam do projektu moduł ngRoute, potrzebny do zarządzania widokami.

Widoki definiuję w następnym pliku – config.js:

Zawartość zdefiniowanych tu widoków wyświetlana będzie w elemencie div oznaczonym dyrektywą ng-view. Póki co nie ma tu nic ciekawego. Tak naprawdę sprawdzałem tylko czy wszystko działa. Widoki nie są jeszcze wykorzystywane w grze. Stworzyłem tylko jeden widok, który będzie otwierany domyślnie. Dokładne działanie tych mechanizmów opiszę w przyszłych postach, gdy dodam już jakieś konkretne widoki 🙂 .

Czas na trochę więcej mięcha. Oto kod głównego kontrolera – controller.js:

Wow, co tam się dzieje w tej deklaracji? Używam metody controller modułu angularowego, aby stworzyć nową instancję kontrolera. Przekazuje mu dwa argumenty, pierwszy to łańcuch znaków zawierający nazwę elementu, drugi to tablica… I w tej tablicy wstrzykuje zależności, czyli te elementy aplikacji, które będę wewnątrz kontrolera wykorzystywał. Zależności dodaje wpisując jako łańcuchy znaków ich nazwy. Do tego, jako ostatni element tablicy wstawiam funkcję, to będzie główna funkcja mojego kontrolera. Po treści funkcji, na samym dole, zamykam funkcję, zamykam tablicę i zamykam listę parametrów metody controller. Czemu tak to wygląda? Nie wiem, ale działa 🙂 Taka składnia frameworka, może jakiś angularowy guru, mógłby to wytłumaczyć. Ja nie będę próbował bo nie chce nikomu pomieszać 🙂 .

Ale wracając do głównej funkcji kontrolera, ona też przyjmuje sporo parametrów. Muszę podać w nich jeszcze raz listę zależności, tym razem już nie jako łańcuchy znaków, a jako obiekty (?). Jak widać będę korzystał z angularwego $interval, czyli przystosowanej do frameworka wersji setInterval, oraz z trzech serwisów, które sam zdefiniowałem. Czym są te serwisy i po co je definiuję? To za chwilę się wyjaśni, póki co po prostu można wyobrazić je sobie jako „pod-moduły”, fragmenty kodu, które są zamknięte i dostępne tylko tam gdzie zostaną wstrzyknięte.

Po wstrzyknięciu, serwisy są dostępne w kontrolerze, tak jakby były obiektami w nim zdefiniowanymi. Wykorzystuję to od razu i tworzę sobie odnośniki do pól serwisów. Niektóre z nich wykorzystam w funkcjach poniżej a niektóre są odczytywane bezpośrednio w HTMLu. Warto zauważyć, że nie korzystam ze $scope. Zamiast tego stosuję składnię controller as.

W kontrolerze definiuję też trzy metody. Zacznę od ostatniej update, ponieważ jest zdecydowanie najważniejsza. Dlaczego? Ponieważ to jest mój game loop. Ta funkcja będzie wywoływana cyklicznie (docelowo) sześćdziesiąt razy na sekundę. Póki co jest dość mała i chciałbym aby tak zostało, chociaż znając życie pewnie się rozrośnie 😛 .

Na samym początku wywołuję wywołuję metodę update serwisu gracza, który przypisałem do pola kontrolera player. Ponieważ kod ten będzie odpalany wewnątrz setInterval, używam zmiennej self aby zachować odpowiedni scope.

Po aktualizacji gracza sprawdzam poprzez serwis lokacji (roomService), czy lokacja się zmieniła. Jeżeli tak, używam metody addEntry (poprzez pole add) serwisu aby dodać do tablicy wiadomości opis nowej lokacji. Gdy to jest gotowe, używam innej metody roomService, aby zmienić flagę sygnalizującą o zmianie pomieszczenia na false. Na koniec sprawdzam, czy postać gracza żyje, jeżeli nie, zatrzymuję pętle.

Zaraz pod definicją metody update, tworzę pole loop, do którego przypisuję angularowy $interval. Przyjmuje on dwa argumenty: funkcję oraz czas w milisekundach co jaki ta funkcja ma być wywoływana. Oczywiście przekazuję metodę update a czas to 1/60 sekundy.

W tym miejscu ktoś może zacząć się zastanawiać po co w ogóle tworzę pętle gry wewnątrz takiej aplikacji. W końcu mógłbym zaprojektować ją tak, że reagowałaby na akcje gracza. To znaczy, po każdym kliknięciu rozpatrywany byłby wynik działania gracza a następnie gra czekała by na kolejny input. Tak jak działają aplikacje webowe 🙂 Sęk w tym, że ja chcę aby świat gry „żył”, czyli jeśli, powiedzmy, gracz stoi w tej samej lokacji co goblin, to potwór będzie go atakował co jakiś określony odcinek czasu, nawet jeśli gracz będzie stał bezczynnie. Do tego, Wykonywanie akcji, nie będzie miało miejsca od razu po kliknięciu, każda czynność, taka jak przejście na inną lokacje, zajmie chwilę. Prędkość takiego działania ma to duże znaczenie. Na przykład gdy opuszczasz lokacje gdzie znajduje się wróg, który Cię atakuje 🙂 Magiczne przedmioty i statystyki gracza będą wpływać na czas akcji. Atak halabardą będzie trwał dłużej niż atak sztyletem. Dzięki pętli, mogę to wszystko zaimplementować bez większego problemu.

Inna sprawa, że może fajnie byłoby czasem wprowadzić takie ‚życie’ do standardowych aplikacji 🙂 ale to temat na kiedy indziej. Oczywiście ten system nie jest idealny. Po pierwsze setInterval nie zatrzyma się gdy gracz zminimalizuje grę lub zmieni zakładkę w przeglądarce. Ewentualny fix polegałby na dodaniu do gry jakiegoś rodzaju pauzy. Innym problemem z setInterval jest to, że nie mam gwarancji, że zostanie wywołana dokładnie co zdefiniowany czas. Normalnie byłoby to dość problematyczne, biorąc pod uwagę to, że w grach delta czasu między aktualizacjami świata ma znaczenie. Mógłbym dodać jakiegoś rodzaju buffer czasowy, ale w mojej grze nie będę przeprowadzał żadnych skomplikowanych symulacji, więc wątpię aby gracz odczuł faktyczną różnice czasów pomiędzy tickami.

Ale wracając do kontrolera. Zawiera on jeszcze dwie metody. Pierwsza z nich to walk, która powoduje, że gracz zmienia aktualną lokację. Jest ona Wywoływana przez kliknięcie jednego z przycisków w głównej części aplikacji. Przycisk przekazuje do metody jeden argument – kierunek, w którym ma udać się postać. Najpierw do serwisu wiadomości przekazywana jest informacja o podjętej przez akcji gracza (dzięki temu zostanie wyświetlona na ekranie). Następnie ustawiam zmienną direction w serwisie gracza, tak aby zawierała aktualny kierunek. Na koniec wywoływana jest metoda walk tego samego serwisu.

Ostatnia metoda to checkDirection. Zwraca ona wartość boolowską do dyrektywy ng-disable. Od jej wyniku zależy, czy przycisk w aplikacji będzie aktywny czy nie. Wartość true zdezaktywuje przycisk. Funkcja zwraca tę wartość w dwóch przypadkach: gdy gracz wykonuje już jakąś akcję (pole performingAction serwisu gracza jest true), lub gdy obiekt obecnej lokacji, pod kluczem kierunku zawiera wartość udnefined.

Pozostałe pliki to serwisy, przechowujące dane oraz logikę konkretnych elementów gry. Pierwszy z nich to mainService.js:

Ten serwis odpowiada, za wiadomości wyświetlane w głównym oknie gry. Na ten moment zawiera on tylko dwa elementy: tablicę z wiadomościami oraz metodę do dodawania do tej tablicy nowych elementów. Tablica log pobierana jest w kontrolerze i wyświetlana w całości na stronie, poprzez dyrektywę ng-repeat. Metoda addEntry, występowała już wcześniej pod aliasem add i służy do dodawania łańcucha znaków przekazanego w parametrze na początek tablicy log. Dlaczego na początek? Bo chcę aby nowe wiadomości wyświetlały się na samej górze okna a właśnie w takiej kolejności ng-repeat dodaje elementy.

Kolejny plik to playerService.js, zawierający serwis opisujący głównego bohatera gry:

Serwisy można wstrzykiwać nie tylko do kontrolerów ale też do innych serwisów. Robię to właśnie w tym przypadku, do serwisu playerService, wstrzykuję roomService. Korzystam z tej samej dziwacznej składni co wcześniej 🙂 . Teraz pola i metody wstrzykiwanego serwisu są dla mnie dostępne wewnątrz serwisu gracza.

Najpierw definiuję kilka pól określających stan bohatera. Następnie tworzę pole actions, zawierające definicje wszystkich akcji, które wykonać może postać. Póki co jest tam tylko chodzenie, czyli zmiana lokacji 🙂 Do tego fragmentu przejdę za moment. Najpierw w skrócie objaśnię dwie metody znajdujące się poniżej.

Pierwsza z nich walk, ustawia pola gracza performinaAction na true oraz currentAction na walking. Ta metoda wywoływana jest z głównego kontrolera, gdy gracz wciśnie przycisk wywołujący akcję zmiany lokacji.

Kolejna metoda to update, czyli aktualizacja wywoływana w pętli gry. W tej funkcji, najpierw sprawdzam czy pole performingAction równe jest true. Jeżeli tak, wywołuję metodę z obiektu actions, której klucz to aktualnie wykonywana przez gracza akcja.

Teraz mogę wrócić do obiektu actions i funkcji znajdującej się pod kluczem walking. Na początku przy pomocy pola przechowującego ilość miniętych ticków, sprawdzam, czy czas już na wykonanie akcji. To jest właśnie to o czym pisałem wcześniej, każda akcja trwa określony odcinek czas. Chodzenie w tej chwili trwa 100 obiegów pętli gry, ale to jest tylko testowa wartość. Gdy wprowadzę do gry odpowiednie mechanizmy, czas trwania tej akcji będzie zależał od paru czynników i na pewno nie będzie stały.

Gdy funkcja ustali, że odpowiednia ilość czasu minęła, wykonywana jest akcja. W tym wypadku jest to zmiana lokacji gracza, ustawienie sygnalizującej to flagi w serwisie lokacji oraz zresetowanie wszystkich pól związanych z wykonywaniem akcji. Nic skomplikowanego 🙂 .

Ostatni plik to serwis lokacji czyli roomService.js. Nie będę wklejał całości, ponieważ nie ma takiej potrzeby. Oryginał pliku można zawsze zobaczyć na githubie :). A oto fragment, który chcę omówić:

Najważniejszym elementem tego serwisu jest oczywiście obiekt rooms. Jego pola to obiekty opisujące lokacje w grze. Nazwa każdego obiektu zawarta jest w kluczu, natomiast sam obiekt przechowuje opis miejsca oraz pola wskazujące na to, w którą stronę można się z danej lokacji udać. Jeżeli pole kierunku posiada wartość undefined, oznacza to, że w tę stronę iść się nie da. Każda inna wartość to nazwa miejsca do którego można się z obecnej lokacji dostać idąc w danym kierunku.

Po za tym, w serwisie znajdują się jeszcze trzy elementy. Pole announceRoom, które przechowuje w informację o tym czy lokacja została zmieniona oraz dwie metody jedna służąca do pobrania wartości announceRoom i jedna służąco do ustawienia wartości tej samej zmiennej.

Póki co, to tyle, pracy jeszcze sporo ale tak jak pisałem na początku, przyjemnie jest popracować z jakąś inną technologią, więc się nie załamuję 🙂 . Nie wiem czy uda mi się skończyć tę grę do czerwca, ale nie jest to wykluczone. W najgorszym razie prace przeciągną się parę dni 🙂 .

A w tak zwanym między czasie, jeżeli chcesz być na bieżąco z postami na blogu zachęcam do polubienia mojej strony na facebooku. Zawsze zamieszczam tam informacje o wszystkich nowościach. Jest to też dobre miejsce na kontakt ze mną. Na wszystkie pytania zawsze odpowiem :).

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *