(WIP) Space Attack Gra w JavaScript. Nowy silnik.

Minął już ponad tydzień od wpisu o moim nowym projekcie, grze Space Attack. Nie zapomniałem jednak o niej, wręcz przeciwnie, w wolnych chwilach praca szła pełną parą. Dziś mogę pokazać pierwszą aktualizację stanu projektu Space Attack gra w JavaScript.

Na pierwszy rzut oka, może wydawać się, że niewiele zostało zmienione. To nieprawda, prawie całkowicie przepisałem kod projektu. Powstał silnik gry, który obsługuje stany programu, oraz sprawia, że wszystko działa znacznie płynniej niż wcześniej.

Aktualną wersję gry można wypróbować klikając w ten link. Tak jak ostatnio dodałem również paczkę z kodem do ściągnięcia.

Space Attack - gra napisana w HTML5 canvas

Gra wciąż nie jest skończona, ale dzięki wprowadzonym zmianom dalsze prace będą znacznie łatwiejsze. Szacuję, że ukończoną grę czytelnicy bloga zobaczą najpóźniej w ten weekend 🙂

Nie byłem zadowolony z tego jak działała poprzednia wersja. Statek przemieszczał się bardzo topornie. Nie można było też na raz poruszać statkiem i strzelać. Nie była to gra, w którą chciałbym zagrać 🙁 Do tego wciąż miałem wrażenie, że w kodzie jest bajzel (ale takie wrażenie mam chyba, odnośnie każdego swojego projektu :P).

Dlatego zamiast rozwijać grę dalej, postanowiłem pomyśleć jak usprawnić to co mam teraz. Siedziałem, myślałem, dłubałem w kodzie, czytałem internety, podpatrywałem innych. Połączyłem zgromadzoną wiedzę z doświadczeniem i przelałem na kod. Myślę że wynik jest zadowalający 🙂

Space Attack Gra w JavaScript – kod

Już na pierwszy rzut oka widać, że w kodzie zmieniło się prawie wszystko. Po pierwsze wyrzuciłem kod obsługujący tło do osobnego pliku. Nie było to konieczne, ale trochę przeszkadzał mi w kodzie. Teraz jest dodawany jako argument do konstruktora głównej klasy gry.

Skoro już mowa o głównej klasie, SpaceAttack, oprócz standardowych pól i metod posiada teraz wewnętrzne podklasy. Najważniejsza z nich to klasa GameEngine, czyli silnik gry, obsługujący całą logikę. Trzy kolejne podklasy to MenuState, GameState oraz PauseState. Jak nazwy wskazują są to obiekty opisujące działanie konkretnych stanów gry. Menu startowe, sama gra, oraz pauza.

W skrócie działa to w następujący sposób. Główny obiekt aktywuje silnik gry, który obsługuje game loop, uaktualnia i rysuje grę. Również reaguje na zdarzenia naciśnięcia przycisków. Myk polega na tym, że to jak uaktualnia, rysuje grę i reaguje na przyciski zależy od aktualnego stanu. Chyba najważniejszym zadaniem silnika gry jest obsługa stanów. Silnik wie jaki stan gry jest obecnie, potrafi zakończyć stan, rozpocząć nowy lub ‚przykryć’ obecny innym (np. pauzą). Jeżeli to wszystko wydaje się proste, to dobrze. Tak naprawdę nie ma tu nic skomplikowanego.

Przejdę do kodu aby móc te idee zilustrować przykładami. Na początek główna klasa gry i jej podstawowe metody oraz pola.

Konstruktor klasy przyjmuje trzy parametry: wskaźnik na canvas mający zawierać grę, wskaźnik na canvas mający wyświetlać tło oraz obiekt obsługujący tło. Dwa pierwsze przypisywane są do odpowiednich pól, a z trzeciego program tworzy instancje i przypisuje ją do kolejnego pola. Ostatnie pole to gameStates, pusty obiekt. Nie będzie to klasa, a jedynie struktura pełniąca funkcje tablicy asocjacyjnej, przechowująca instancje stanów, dostępnych po ich nazwach.

Następnie pojawia się metoda initSpaceAttack której zadaniem jest główny rozruch gry. Wewnątrz metody tworzona jest instancja obiektu GameEngine, za chwilę pojawi się dokładny opis tego obiektu, póki co warto zwrócić uwagę, że konstruktor przyjmuje 3 parametry: wskaźnik na canvas gry, referencje do metody gameLoop obiektu SpaceAttack oraz obiekt gameStates. Kolejne trzy linijki to tworzenie instancji stanów oraz dodawanie ich w odpowiednie pola obiektu gameStates. Konstruktory stanów przyjmują tylko jeden parametr, instancje silnika gry. Gdy to jest już gotowe, uruchamiane jest tło gry. Następnie program odpala metodę start, utworzonej przed chwilą instancji GameEngine. Na koniec uruchamiana jest metoda activateKeyboard

activateKeyboard powoduje, że okno przeglądarki nasłuchuje wciśnięcia klawisza oraz zwolnienia klawisza. W razie gdy wystąpi któreś z tych wydarzeń wywoływana jest odpowiednia metoda silnika gry (keyDown lub keyUp). Blokowane jest również domyślne zachowanie przycisków wykorzystywanych w grze, a które mogłyby wpłynąć na okno przeglądarki (spacja, strzałka w prawo oraz strzałka w lewo).

Ostatnia metoda to gameLoop, która jako parametr przyjmuje (a właściwie będzie przyjmować) instancję silnika. Metoda ta przypisuje do wewnętrznej zmiennej currentState wynik działania metody silnika gry returnState, czyli aktualny stan gry. Jeżeli aktualny stan gry istnieje i posiada metodę update, jest ona wywoływana. Jeżeli posiada on metodę draw, również ją wywołuje.

I to wszystkie bezpośrednie pola i metody głównego obiektu gry. Przejdę teraz do serca tego wszystkiego czyli GameEngine.

Space Attack Gra w JavaScript – silnik

Pierwsze trzy pola otrzymują wartości przekazane jako argumenty w konstruktorze. Opisałem te wartości wcześniej. Następne pole to config – obiekt/tablica asocjacyjna. Zawiera on wszelkie informacje konfiguracyjne dla gry. Aktualnie są to: rozmiar płótna, liczba klatek na sekundę oraz adresy grafik obiektów gry. Kolejne pole stateStack to tablica. Nazwa nie jest przypadkowa. Tę tablicę będę wykorzystywał jak stos (zwraca się znajomość struktur danych 😉 ) zawierający stany gry. Stan będący aktualnie na szczycie stosu będzie stanem aktywnym. Ostatnie pole pressedKeys to obiektem/tablica asocjacyjna zawierająca dane o przyciśniętych klawiszach.

Kolejne cztery metody służą do obsługiwania stosu stanów. Pierwsza z nich returnState, sprawdza czy pole stateStack zawiera jakieś elementy, jeśli tak zwraca ostatni z nich (czyli ze szczytu stosu :)). Metoda changeState zmienia obecny stan na ten, który zostanie przekazany w parametrze. Jeżeli metoda returnState zwróci jakiś stan i stan ten posiada metodę leave, zostanie ona wywołana a następnie stan ten zostanie usunięty. Dzięki temu mechanizmowi, wszelkie stany, które będą musiały po sobie „posprzątać” (np zresetować liczbę punktów) będą mogły to zrobić zanim zostaną wyłączone. Po usunięciu stanu, metoda sprawdza czy stan z argumentu posiada metodę enter, jeżeli tak jest ona odpalona (analogicznie do leave, wszelkie „przygotowania” przed dodaniem stanu) i na stos stanów dodany jest ten stan. Metod addState oraz removeState, nie będę opisywał. Działają dokładnie tak samo jak odpowiednie fragmenty kodu poprzedniej metody. Jak nazwy wskazują służą one do dodawania lub usuwania stanów (nie do obu tych rzeczy na raz).

Następna metoda to start, która służy do zainicjalizowania game loopa oraz uruchomienia pierwszego stanu. Do stosu stanów dodawana jest referencja do instancji MenuState. Silnik gry ma dostęp do instancji stanów dzięki obiektowi gameStates (a dokładniej do referencji do niego, przekazanej w argumencie konstruktora GameEngine 😉 Następnie metoda start uruchamia setInterval, które co sekundę podzieloną przez liczbę klatek podaną w konfiguracji, odpala funkcję gameLoop (dostęp również dzięki przekazaniu w konstruktorze). Funkcji tej jako parametr silnik gry przekazuje sam siebie.

Dwie ostatnie metody silnika keyDown i keyUp (przypominam, wywoływane przez metody głównego obiektu o tych samych nazwach) jako argument otrzymują kod przyciśniętego klawisza. keyDown dodaje do obiektu pressedKeys obiekt o kluczu równym kodowi podanemu w parametrze i wartości true. keyUp usuwa taki obiekt z obiektu pressedKeys za pomocą operatora delete. Dodatkowo, keyDown pobiera aktualny stan ze stosu i sprawdza czy posiada on metodę o takiej samej nazwie. Jeżeli tak, wywołuje ją, przekazując jako parametr kod przyciśniętego klawisza.

Space Attack Gra w JavaScript – stany

Przyszła pora opisania stanów gry. W obecnej wersji istnieją trzy MenuState, GameState oraz PauseState. Każdy z tych obiektów jest częścią głównego obiektu SpaceAttack. Wszystkie podczas tworzenia instancji, otrzymują referencję do silnika gry, dzięki czemu mogą używać zawartych w nim informacji. Zacznę od MenuState.

Stan ten nie jest zbyt skomplikowany. Posiada jedno pole, odnośnik do kontekstu płótna (dostęp do niego ma dzięki silnikowi), oraz dwie metody draw i keyDown. gameLoop, funkcja która po uruchomieniu przez silnik wywoływana jest co odpowiedni okres czasu przez setInterval , wywołuje metodę draw aktualnego stanu. W przypadku tego stanu metoda draw wypisuje na płótnie powitanie oraz instrukcję dla gracza, jak rozpocząć grę. Nic nie jest uaktualniane w tym stanie, więc nie mamy metody update. Mamy za to keyDown, które, jeżeli zostanie naciśnięta spacja, wywołuje metodę changeState silnika przekazując jako parametr stan gameState.

GameState to główny stan gry, a oto jego kod:

Nie będę dokładnie opisywał całego kodu. Większość mechanizmów gry opisałem już w poprzednim poście. Jeżeli jednak coś nie będzie do końca jasne, dajcie znać w komentarzach. Na wszystkie pytania odpowiem. Warto zwrócić uwagę na to to, że główny stan gry nie posiada metody keyDown. W takim razie jak porusza się statek i skąd wiadomo kiedy strzela? Wiadomo, dzięki metodzie update, która jest ona wywoływana co sekunda/ilość klatek, dzięki silnikowi gry. W tej metodzie, stan sprawdza czy pole pressedKeys silnika nie zawiera danych świadczących o tym, że interesujące ten stan klawisze nie zostały naciśnięte. Jeżeli tak, stan podejmuje odpowiednie działania (uaktualnia pozycje statku, odpala rakietę). To właśnie ten mechanizm powoduje, że statek porusza się płynnie. Pozycja statku uaktualniana jest cały czas a nie tylko w odpowiedzi na przycisk (tak jak było to w poprzedniej wersji). Kolejną nową metodą jest enter, wywoływana podczas dodania stanu do stosów stanu. Tworzony jest nowy statek i dodawany do tablicy aktorów gry. Może to nie wiele ale idealnie ilustruje jak wykorzystać metodę enter. Sprawdza się świetnie jeżeli jakiś stan wymaga aby przed jego rozpoczęciem coś zainicjalizować.

Reszta kodu powinna być już zrozumiała, mogę więc przejść do ostatniego stanu PasueState.

Pause state dodawany jest do stosu stanów przez wciśnięcie ‚p’ (kod 80) podczas gdy aktywny jest główny stan gry. Dodaje go metoda addState silnika, więc obecny stan nie jest usuwany. Metoda keyDown stanu pauzy powoduje, że po wciśnięciu spacji ze szczytu stosu usuwany jest aktualny stan (czyli stan pauzy), za pomocą metody silnika removeState. To dobry przykład ilustrujący zachowanie stosu stanów. dodanie pauzy zatrzymuje grę, a usunięcie jej powoduje, że stan znajdujący się poniżej znów jest aktywny. Po za metodą keyDown stan pauzy posiada również metodę draw, która wypisuje na płótnie informacje o tym, że gra została zapauzowana.

I to wszystko na tę chwilę. Teraz dodanie nowych stanów (na przykład różne poziomy gry) będzie prostsze. Również zmienianie samej gry nie będzie problematyczne. Wystarczy że zaktualizuje główny stan. Nie muszę martwić się, że przy okazji popsuje coś w silniku, ponieważ jest on osobnym obiektem.

Znów wyszedł kobylasty wpis. Jeżeli udało Ci się doczytać do końca daj znać w komentarzu, przybiję Ci wirtualną piątkę 🙂 Jeżeli masz jakiekolwiek pytania lub sugestie, również napisz komentarz. Na pytania zawsze odpowiem a sugestie chętnie przeczytam. Jak zwykle zachęcam też do polubienia mojej strony na facebooku, którą na bieżąco uaktualniam. To również dobry kanał aby złapać ze mną kontakt jeśli ktoś jest zainteresowany 🙂

Dodaj komentarz

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