jsHopper – gra co miesiąc: marzec. Kompletna gra w Phaser.js – część druga.

W poprzednim poście pokazałem jak działają podstawy mojej nowej gry stworoznej przy pomocy frameworka Phaser.js. Były to dwa pierwsze stany oraz sposób przechodzenia pomiędzy nimi. Stan load wczytuje pliki potrzebne do gry a stan menu prezentuje ekran menu. Trzeci stan, game obsługuje czyli właściwą grę. Wyjaśniłem jak działają dwa pierwsze stany, oraz jak zainicjalizować je wraz z grą Phaser.

Dziś przyszła pora na objaśnienie działania ostatniego stanu gry. Jest on znacznie większy niż dwa poprzednie, dlatego umieściłem jego opis w osobnym poście.

Tworzenie gier phaser dla początkujących

We wcześniejszym poście wspomniałem już, że najważniejsze funkcje każdego stanu w Phaser to preload, create oraz update. Główny stan nie posiada funkcji preload ponieważ wszystkie dane zostały wczytane wcześniej, posiada za to dwie pozostałe. Pomimo tego, że znajdują się one na końcu stanu, zacznę opis właśnie od nich. Oto kod:

metoda create, odpalana jest jako pierwsza, za każdym razem gdy stan startuje (normalnie byłaby to preload, ale w tym wypadku jej nie ma). Gdy metoda ta się wykona, program zacznie wywoływać update 60 razy na sekundę.

W metodzie create definiuję wszystkie zmienne oraz obiekty potrzebne do poprawnego działania stanu. Najpierw inicjalizuję tablicę baskets. Zawiera ona współrzędną x wszystkich norek królika (początkowo miały to być koszyki na jajka wielkanocne, ale porzuciłem ten pomysł, nazwa zmiennej pozostała 🙂 ).

Kolejna zmienna to tablica rows. Poziom gry podzielony jest na rzędy, każdy o wysokości 32 piksele. Królik porusza się tylko po rzędach. Tablica zawierające progi każdego rzędu przyda się. Wypełniam ją w pętli for, która znajduje się poniżej. Teraz każdy element to współrzędna y, rzędu o numerze równym indeksowi elementu. Rząd zerowy znajduje się na samej górze, rząd pierwszy 32 piksele poniżej, rząd drugi 64 i tak dalej.

Ostatnia zmienna to score i jak łatwo się domyślić, przechowuje ona punkty zebrane przez gracza. Wykorzystam ją do sprawdzenia czy gracz już wypełnił wszystkie norki czy jeszcze nie. Gdy to jest już gotowe, wywołuje funkcję setUpLogs, która zajmuje się dodaniem do gry obiektów drzew płynących strumieniem. Kolejna linijka to dodanie obrazka nakładającego się na ostatni rząd z kłodami. Dlaczego to robię? Z powodu głupiego błędu, z którym nie mogłem poradzić sobie w inny sposób, opiszę go gdy dojdę do funkcji aktualizującej odpowiedni fragment gry.

Teraz dochodzę do najciekawszego elementu tej metody czyli do tworzenia obiektu gracza. Robię to za pomocą metody sprite, pola add obiektu game. Tworzony obiekt zachowuję w polu stanu: player. Konstruktor nowego sprite’a, przyjmuje trzy zmienne, współrzędne x i y oraz klucz do obrazka, który będzie go reprezentował. Sprite automatycznie otrzymuje wymiary obrazka. To już gotowy obiekt, który ma wszystkie potrzebne dane do funkcjonowania w środowisku 2d! W kolejnej linijce ustawiam anchor obiektu gracza. Co to znaczy? Chcę aby punkt rysowania tego obiektu znajdował się w centrum obrazka, a nie w jego lewym, górnym rogu, jak ma to zazwyczaj miejsce. Robię to ponieważ podczas ruchu, obracam ten obiekt tak aby królik patrzył w stronę, w którą skacze. Obrót normalnie liczony byłby od lewego górnego rogu, co przesunęłoby obrazek o cały rząd. Chcę aby obracał się względem swojego środka, stąd anchor.setTo. Metoda ta przyjmuje jeden lub dwa argumenty. Ich wartość to liczba pomiędzy 1 a 0. Pierwszy argument to zmiana wartości X punktu rysowania a drugi to Y. Wartość 1 oznacza standardowy punkt, czyli w przypadku współrzędnej X lewą stronę obrazka. 0 to przeciwległa wartość, czyli prawa strona. 0.5 to dokładny środek. Podanie jednego argumentu spowoduje, ze obie współrzędne zostaną zmienione na tę samą wartość.

Kolejna linijka, odpowiada za dodanie do obiektu gracza metod phasera odpowiedzialnych za fizykę. Nie będę dużo z tego korzystał w mojej grze, ale przyda mi się do wykrywania kolizji pomiędzy autami oraz krawędziami gry. Kolejna linijka kodu powoduje właśnie to drugie. Sprite gracza nie może teraz wyjść poza granice płótna. Fizyka w phaser to dość duży temat i wyjaśnię dokładnie to zagadnienie, przy okazji jakiejś gry, które je wykorzystuje.

Gdy obiekt gracza jest już gotowy. Odpalam metodę setUpCars, która ustawia obiekty samochodów. Skąd taka kolejność? Najpierw ustawiam kłody, ponieważ chce aby zajączek był rysowany na nich. Samochody ustawiam po króliku, ponieważ w razie czego chcę aby znalazł się pod samochodem (to znaczy, wcale tego nie chce… każdy wie chyba o co chodzi).

Ostatnia linijka kodu aktywuje klawisze kursora, czyli strzałki na klawiaturze. Tak, wystarczy ta jedna linijka, i gra będzie reagować na przyciskanie strzałek.

Czas na metodę update. Jeszcze raz zaznaczam, metoda ta będzie wywoływana 60 razy na sekundę gdy stan ruszy. Najpierw wywołuje całą masę metod odpowiedzialnych za aktualizacje obiektów. Je opiszę później. zacznę od tej linijki:

ta jedna linijka odpowiada za kolizje pomiędzy graczem a samochodami. Metoda overlap pola arcade, modułu physics głównego obiektu gry, przyjmuje pięć argumentów. Dla mnie najważniejsze są pierwsze trzy. Dla uproszczenia ustalę na tę chwilę, że czwarty oraz piąty argument zawsze są równe null oraz this. Pierwsze dwa argumenty to obiekty lub grupy obiektów (o grupach za chwilkę). Trzeci argument to funkcja, która zostanie wywołana, gdy pomiędzy obiektami z pierwszych argumentów nastąpi kolizja. W moim wypadku wywołana zostanie metoda bunnyDie. I kolizje załatwione 🙂 .

Ostatnia część metody create zostanie odpalona, gdy wartość score równa będzie cztery. Za każdą zaliczoną norkę gracz otrzymuje jeden punkt. score równe cztery oznacza, że gracz zakończył grę. W takim wypadku wykona się kod obsługujący tę sytuację.

Najpierw zwiększam score o jeden. Nie chcę aby zawartość ifa wykonywała się więcej niż raz. W ten sposób sobie to gwarantuję. Kolejny krok to usunięcie zajączka z planszy. Moja metoda resetBunny, resetuje jego współrzędne, a phaserowa metoda kill usuwa go z planszy. Następnie, tworzę tekst zawierający informację gratulująca graczowi. Ustawiam anchor tekstu na jego środek a jego x na punkt znajdujący się w połowie szerokości gry (wygodne pole dostępne dzięki phaserowi: game.world.centerX). Współrzędna y napisu równa jest -50, więc napis nie jest początkowo widoczny. Gdy tekst jest gotowy uruchamiam Tweena, który opuszcza napis. Nowością jest użycie metody add pola onComplete tweena. Przyjmuje ona funkcję zwrotną, która wykona się, gdy tween zakończy się animować. W moim wypadku metoda ta po prostu rozpoczyna nowy stan – menu.

I tak działa cały stan 🙂 .

Przejdę teraz do opisu tych jego metod, które sam dodałem. Pierwsza to bunnyHop, dość obszerna ale bardzo prostolinijna metoda:

Metoda ta odpowiada za poruszanie się królika i jest wywoływana wewnątrz update. Aktywując klawiaturę w metodzie create uzyskałem dostęp do obiektu keys, który zawiera w sobie obiekty klawiszy strzałek. Mogę odwołać się do każdego z nich poprzez pola left, right, down oraz up. Jeżeli ich pole isDown równe jest true, oznacza to, że dany klawisz jest przyciśnięty. Reszta to już prosta sprawa. Naciśnięcie każdego z przycisków powoduje, ze zając skacze o 32 piksele w odpowiednią stroną> obracam również jego obrazek, manipulując pole obiektu gracza angle. Aby każde przyciśnięcie równe było tylko jednemu skokowi, tworzę też zmienne logiczne kontrolujące to czy dany przycisk jest naciśnięty. Nic nowego.

Kolejne trzy metody sprawdzają gdzie jest królik i sprawiają, że gra odpowiednio na to reaguje. Oto one:

Tak naprawdę nie ma tutaj, dużo metod samego phasera. Pierwsza, bunnyUpdate, sprawdza po prostu, czy królik nie przeszedł na drugi przedostatni rząd (licząc od dołu), jeżeli tak jest cofany na trzeci.

Druga metoda – checkBunny, jest już trochę obszerniejsza. Sprawdza ona, czy królik nie trafił do którejś z norek (wywołują ją przed bunnyUpdate, więc dzieje się to przed jego cofnięciem). Jeżeli x królika znajduje się w tablicy baskets zastępuję wartość pod tym indeksem wartością false i dodaje nowego królika, który umieszczony zostaje w odpowiedniej norce. Następnie zwiększam wartość score o jeden i resetuję królika gracza.

Jeżeli natomiast, królik znajduje się na którymś z rzędów na wodzie ale wartość zmiennej onLog jest fałszywa, wywoływana jest metoda bunnyDie. Wejście do wody to drugi sposób w jaki może zginąć królik.

Ostatnia metoda to checkIfOnLog, sprawdza ona czy krówlik znajduje się na którejś z kłód. Jeżeli tak, to onLog ustawiane jest na true. Wszystkie kłody, znajdują się w grupie logs i mogę między nimi iterować tak jak po normalnej tablicy, używając pętli for. Odnośniki do każdego elementu grupy znajdują się w jej polu children.

W pętli sprawdzam każdą kłodę z królikiem, używam do tego metody intesects pola Rectangle obiektu Phaser. Ta metoda przyjmuje dwa argumenty, granice sprawdzanych elementów. Jeżeli granice na siebie nachodzą, metoda zwraca true. Granice pobieram używając metody getBounds, która posiada każdy sprite w Phaser.

Jeżeli królik koliduje z którąś z kłód, przerywam pętle i onLog równe jest true. Jeżeli nie nastąpi kolizja z żadnym z obiektów w grupie logs, onLogs ustawiane jest na false.

Miałem w tym miejscu drobny problem. Za każdym razem, gdy królik wchodził na pierwszy rząd z kłodami, onLog było przez chwilę równe false, niezależnie od tego czy królik trafiał na kłodę, czy nie. Przez co zawsze ginął gdy wchodził na pierwszy (od dołu) rząd rzeki. Załatałem to przykrywając pierwszy rząd kłód obrazkiem ziemi, a badanie kolizji z woda i kłodami zacząłem od kolejnego rzędu. Głupi fix, przyznaje, ale działa a ja dopiero uczę się Phasera i szczerze mówiąc średnio wiem co robię 😛 🙂 .

Dwie ostatnie metody dotyczące królika to resetBunny oraz bunnyDie:

Pierwsza metoda jest bardzo prosta. Po prostu przenosi królika w lewy dolny róg planszy. Nie ma tu żadnych czarów.

Druga metoda posiada same znane już rozwiązania. Najpierw resetuje i zabijam królika a następnie tworzę dwa obiekty tekstu, które pokazuje jeden po drugim za pomocą tween’ów. Gdy oba już się pokażą, odpalam stan ‚menu’.

Kolejne dwie metody obsługują obiekty samochodów. Pierwsza setUpCars uruchamiana jest w metodzie create i inicjalizuje one obiekty aut. Drugą checkCars, która aktualizuje auta, odpala metoda update. Oto kod tych metod:

Pierwsza linijka w metodzie setUpCars pokazuje jak stworzyc jedną z najlepszych według mnie rzeczy w Phaser – grupe spriteów.

Przy pomocy metody group, modułu add głównego obiektu gry, tworzę nową grupę, którą przypisuję do zmiennej cars. Czym są grupy? Jest to zbiór spriteów, które będą dzieliły wiele podobnych zachowań. To nie jest tylko lista, ale też sposób na wpływanie na wiele spriteów na raz. W drugiej linijce widać przykład tego. Ustawiając pole enableBody grupy na true, aktywuje fizykę dla wszystkich członków grupy na raz.

Póki co grupa jest pusta. W parę następnych linijkach dodaje do niej obiekty. Działa to tak samo jak dodawanie jednego srprite do gry, czyli używając metody sprite modułu add obiektu gry. Tym razem jednak po współrzędnych i kluczu obrazku, dodaje jeszcze argument równy zero, oraz odnośnik do grupy, do której nowy sprite ma być dodany. Zero oznacza miejsce w którym ma być dodany nowy sprite w grupie (czyli na początku).

Każda grupa ma też pole children, które przechowuje tablice odnośników do wszystkich obiektów w grupie. Przechodzę przez każdy dodany do niej obiekt samochodu i ustawiam trzy jego właściwości. Pierwsze to velocity.x modułu body. Jest to prędkość obiektu, Wystarczy dodać tę jedną linijkę i obiekt będzie się poruszał, niezależnie od jakichkolwiek innych metod. Minusowa wartość x oznacza oczywiście ruch w lewo a dodatnia w prawo. Jak widać kierunek ruchu samochodów zmienia sie w każdym rzędzie. Kolejne dwie właściwości ustawiam na true. Są to checkWorldBounds oraz outOfBoundsKill. Pierwsza oznacza, że program będzie sprawdzał czy dany obiekt znajduje się w granicach gry, druga natomiast usuwa obiekt (zabija – kill), jeżeli wyjdzie poza nie.

Kolejna metoda stanu to checkCars. Na jej początku tworzę zmienna tymczasową, do której przypisuje wartość zwracaną przez metodę getFirstDead grupy cars. Jest to metoda Phaserowa i myślę że jej nazwa jest jednoznaczna. Zwraca ona obiekt grupy, który najdłużej jest martwy (po potraktowaniu metoda kill). Jeżeli obiekt taki istnieje (może być tak ze aktualnie na ekranie są wszystkie auta i żadne nie jest ‚martwe’), metoda przywraca go do gry na jego początkowym. W ten prosty sposób sprawiam, że po wirtualnej ulicy cały czas jeżdżą samochody. Gdy tylko jakiś wyjdzie po za ekran gry, wraca z drugiej strony.

Metody obsługujące kłody są identyczne jak te obsługujące auta. Oto ich kod:

Jedyna różnica to to, ze przesuwam obiekty kłód o parę pikseli w dół na osi Y. Wysokość obrazków jest mniejsza niż wysokość rzędu a chciałem aby były wycentrowane. Do tego, funkcja sprawdzająca czy kłody nachodzą na gracza logowała również true jeśli obiekty się stykały, czyli jak zajączek stał na wodzie a kłoda płynęła rząd wyżej lub niżej, gra traktowała to jako kolizje. Reszta kodu działa tak samo jak kod obsługujący obiekty samochodów.

I to cała gra. Jestem z niej zadowolony. Pomimo tego, że to moja pierwsza styczność z phaserem i grę tworzyłem trochę ‚na czuja’, uważam, że wyszła naprawdę nieźle. Bardzo spodobało mi się używanie Phasera. Na pewno znacznie usprawni tworzenie prototypów do bardziej złożonych gier. A wy co o tym sądzicie? Używaliście tego frameworka? Jeżeli tak podzielcie się wrażeniami w komentarzach.

Na koniec, jak zwykle, zachęcam do polubienia mojej strony na facebooku. Zamieszczam tam informacje o wszystkich nowościach. Polubienie strony gwarantuje bycie na bieżąco zamieszczanymi na blogu 🙂 .

Dodaj komentarz

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