(WIP) Wiedźma – gra co miesiąc: styczeń

Obiecałem, że w 2016 roku na blogu będę co miesiąc prezentował jedną, nową grę. Minęły już daw tygodnie stycznia i ciągle żadnych wieści w temacie. Do dziś 🙂 W tym poście przedstawiam pierwsze informacje o styczniowej grze.

W tym miesiącu stworzę grę, w której gracz będzie mógł wcielić się w postać wiedźmy, poszukującej składników do potężnego zaklęcia. Nie będę póki co zdradzał szczegółów fabuły. Skupie się na tym co do tej pory przygotowałem, czyli nowy silnik gry.

gra co miesiąc: styczeń

Grę w obecnym stanie można wypróbować klikając na powyższy obrazek. Jak zawsze przygotowałem też paczkę z kodem do pobrania pod tym linkiem.

gra co miesiąc: styczeń – nowości

W dwóch ostatnich grach: space attack i gra z mikołajem, korzystałem z silnika opartego na funkcji setInterval. Działał on całkiem nieźle, ale w JavaScripcie dostępny jest lepszy sposób na tworzenie animacji: requestAnimationFrame. To właśnie requestAnimationFrame obsługuję game loop w nowym silniku. Zmieniłem również sposób obsługi stanów gry. Zrezygnowałem ze stosu stanów. Zamiast tego stworzyłem znacznie prostszy system, który jednak daje te same możliwości.

Kolejną nowością jest stan odpowiadający za wczytywanie zewnętrznych elementów gry, takich jak grafika czy dźwięki. Dzięki niemu, gra nie włączy się, dopóki wszystkie grafiki i dźwięki nie będą gotowe. Wprawdzie w tej chwili, gra wczytuje tylko jedną grafikę zawierającą wszystkie obiekty gry, ale w przyszłości liczba elementów zewnętrznych będzie większa. Stan wczytywania, jest na to przygotowany.

Gra jest oczywiście w fazie wczesnego rozwoju, obecnie nie wiele się dzieje. Moim głównym celem na tym etapie tworzenia gry, było napisanie, stabilnego i prostego silnika gry. Myślę, że mi się udało. Czas przejść do kodu.

gra co miesiąc: styczeń – kod

Nie będę wklejał wszystkiego na raz, kod już w tej chwili jest na to zbyt duży. Zaprezentuję go fragmentami i opiszę, za co dana część odpowiada. Pominę też plik HTML. jego zawartość można obejrzeć w źródle strony z grą, lub zaglądając do paczki z plikami. Element body przypisany ma atrybut onload, dzięki który wywoływana jest funkcja initAll, gdy tylko element ten będzie wczytany. Zawartość funkcji initAll, wygląda tak:

Nic skomplikowanego. Pobieram element canvas oraz jego kontekst i przypisuje je do odpowiednich zmiennych. Następnie tworzona jest instancja obiektu Game, którego konstruktor przyjmuje referencje do płótna i jego kontekstu. Na końcu wywoływana jest metoda obiektu gry: start.

Reszta kodu zawarta jest wewnątrz obiektu Game. Kod tego obiektu można umownie podzielić na trzy części: pola, metody, obiekty stanów. Pierwsza część jest zdecydowanie najmniejsza, wygląda ona tak:

Konstruktor obiektu przyjmuje dwa argumenty, referencje do płótna oraz jego kontekstu. Argumenty te są od razu przypisywane do odpowiednich pól. Tworzę też zmienną self, dzięki której będę mógł odwoływać się do tego obiektu, w innych kontekstach takich jak wykrywacze zdarzeń czy requestAnimationFrame. Przyda się ona także w obiektach stanów, znajdujących się wewnątrz tego obiektu.

Pola assets oraz loadedAssets, wykorzystywane są przez stan wczytywania elementów zewnętrznych. assets to lista tych elementów, natomiast loadedAssets, przechowywać będzie informacje na temat tego ile z tych elementów zostało już załadowanych.

Następne pole, state jest bardzo ważne. Przechowuje ono łańcuch znaków, który jest równy nazwie aktualnego stanu gry. To pole będzie służyło do zmiany stanów i wywoływania metod aktualnego stanu. Początkowa wartość tego pola to loadingState. Będzie to pierwszy uruchomiony przez program stan.

Pole masterSprite będzie przechowywać odnośnik do obrazka zawierającego grafiki obiektów z gry. Odpowiednia wartość zostanie przypisana do niego w czasie stanu ładowania gry.

pressedKeys to obiekt, służący do przetrzymywania informacji o aktualnie naciśniętych klawiszach.

Ostatnie pole zawiera obiekt spriteObject. Jest to wzór, na podstawie którego tworzyć będę wszystkie obiekty gry, które mają być narysowane na płótnie.

Kolejny fragment kodu to metody głównego obiektu gry:

Pierwsza metoda, start jest wywoływana na początku programu. Służy ona do zainicjalizowania gry. Wywołuje ona dwie kolejne metody: activateKeys oraz mainLoop.

Działanie metody activateKeys powinno być jasne. Pojawia się w co drugim moim projekcie w takiej czy innej formie. Dzięki niej, kody przyciśniętych przycisków zapisywane są w polu pressedKeys. Po puszczeniu klawisza, jego kod usuwany jest z pressedKeys.

Ostatnia metoda, mainLoop, to serce silnika gry. Dzięki niej gra jest cały czas aktualizowana i rysowana. To jaki jest wynik aktualizacji zależy od aktualnego stanu. Na początku umieszczam wywołanie requestAnimationFrame. Dzięki temu funkcja ta będzie wywoływana mniej więcej 60 razy na sekundę.
Oto co dokładnie będzie się działo podczas jednego wywołania.

Metoda ta dzięki wartości pola state, odnosi się do obiektu, o tej samej nazwie, który musi być aktualnym stanem.

Jeżeli obecny stan głównego obiektu posiada metodę init a jego pole initalised równe jest false, to metoda init zostanie wywołana.

Jeżeli obecny stan głównego obiektu posiada metodę update, zostanie ona wywołana. W ten sam sposób obsłużona zostanie również metoda draw tego stanu.

Na koniec sprawdzane jest czy obecny stan ma pole toLeave równe true. Jeżeli tak, do zmiennej tymczasowej przypisywana jest wartość pola nextState obecnego stanu. Następnie pole to oraz pola initalised oraz toLeave, ustawiane są na początkowe wartości. Na koniec zmieniana jest wartość pola state głównego obiektu.

Ten bardzo prosty sposób na obsługę stanów daje mi spore możliwości. Unikam też pokrętnego przekazywania obiektów między sobą, z którym musiałem radzić sobie we wcześniej wykorzystywanym przeze mnie silniku.

Czas przejść do ostatniej części obiektu Game, czyli obiektów stanów. Są one zapisane jako pola głównego obiektu. W obecnej wersji gry mam cztery stany:

  • loadingState – stan odpowiadający za wczytanie wszystkich zewnętrznych elementów gry, takich jak grafika czy dźwięki.
  • menuState – menu gry, czyli stan w którym gracz może wybrać czy chce rozpocząć nową grę czy nie. Mogą tu też pojawić się opcje takie jak ustawienia dźwięku lub trudności
  • storyState – stan, który będzie uruchamiany przed głównym stanem gry. Będzie on przedstawiał graczowi fabułę gry.
  • gameState – główny stan gry 🙂

ponieważ stany to najobszerniejszy fragment kodu, będę prezentował je po kolei. Pierwszy stan to loadingState:

Wbrew pozorom jest to bardzo prosty stan. Pierwsze cztery pola, służą do obsługi animowanego tekstu informującego, gracza o tym, że gra się wczytuje. Kolejne trzy pola wykorzystywane są przez mechanizm obsługi stanów.

Metoda init jest mało skomplikowana. Ustawia ona wartość pola initialised na true, oraz wywołuje metodę loadAssets. Kolejna metoda to update. Ta metoda wywoływana cyklicznie przez główną pętle gry. Składa się ona z dwóch wyrażeń warunkowych. Pierwsze sprawia, do zawartości pola text co 10 obrotów pętli dodawana jest kropka. Jeżeli są już trzy kropki, wszystkie zostaną usunięte.

Drugie wyrażenie warunkowe jest znacznie ważniejsze. Sprawdza czy tablica w polu assets głównego obiektu zawiera jakieś elementy. Jeżeli tak i jeżeli ich ilość jest równa wartości pola loadedAssets głównego obiektu, stan sygnalizuje gotowość do przejścia w kolejny. Wartość pola toLeave ustawiana jest na true a wartość pola nextState na menuState. Oznacza to, że gdy wszystkie elementy zewnętrzne zostaną wczytane, stan gry zmieni się na „menuState”.

Kolejna metoda obiektu loadingState to draw. Nie dzieje się tu nic wyjątkowego. Po prostu na płótnie wypisywany jest zawartość pola text. Prawda jest taka, że gracz raczej i tak nie zobaczy tego tekstu ponieważ elementy zewnętrzne zostaną wczytane bardzo szybko.

Ostatnia metoda to loadAssets. W tej metodzie deklarowane są wszystkie elementy zewnętrzne. Póki co jest tu tylko jeden taki element: mainSprite, czyli obrazek z grafikami gry. Jest on dodawany do tablicy assets głównego obiektu. Otrzymuje również wykrywacz zdarzenia load. Gdy obrazek zostanie wczytany, wartość pola loadedAssets zostanie zwiększona o jeden.

Kolejny stan to menuState:

Obecnie w tym stanie też nie dzieje się zbyt wiele. Jest to tak naprawdę tak zwany ‚placeholder’, czyli coś co rezerwuje miejsce dla konkretnego obiektu w przyszłości. Metoda init wypełnia jedynie swoją najbardziej podstawową funkcję, czyli zmienia initalised na true. Metoda draw wypisuje na płótnie tekst witający gracza (ten tekst to też tylko ‚placeholder’ 🙂 ). Metoda update sprawdza czy w obiekcie pressedKeys głównego obiektu, zalogowana została spacja. Jeżeli tak sygnalizuje gotowość zmiany stanu na storyState:

Ten stan to też tylko placeholder i działa dokładnie tak samo jak poprzedni stan. czyli czeka na wciśnięcie spacji. W przyszłości będzie on wyświetlał tekst oraz grafikę, wprowadzające gracza w klimat gry 🙂 . Po wciśnięciu spacji aktywowany zostaje główny stan gry czyli gameState.

Uff… Straszna kobyła (a będzie jeszcze większa). Na szczęście większość tego kodu opisałem już przy okazji prezentacji requestAnimationFrame. Jakieś 90 procent tego kodu powtarza się. Dlatego większość opiszę pobieżnie, skupiając się jedynie na (nielicznych) zmianach.

Pierwsze trzy pola potrzebne są do obsługi mechanizmu stanów. W obecnej wersji, z tego stanu nie da się już wyjść, ale w przyszłości będzie to możliwe (na przykład gdy postać gracza zginie 🙂 ). Pole mainEntities, to tablica, w której znajdować będą się główne obiekty gry (w tej chwili tylko obiekty tła). background oraz moonStars będą przechowywały obiekty tła. Pole witch zawierać będzie obiekt głównej bohaterki gry czyli wiedźmy.

Kolejne dwa pola to obiekty gameWorld oraz screen. Będę wykorzystywał je do utworzenia efektu przesuwającego się świata gry. Mechanizm ten będzie działał dokładnie tak samo jak opisałem to we wcześniej wspomnianym poście.

Metoda init inicjalizuje obiekty tła oraz obiekt wiedźmy. background oraz moonStars są już znane. Obiekt wiedźmy trochę się zmienił. Ma ona teraz stałą prędkość wpisaną w pole speed. Nowością są też pola baseY, angle oraz waveRange. W połączeniu z javascriptową metodą sin obiektu Math, powodują że podczas lotu wiedźma lekko się ‚kołysze’.

Na koniec metoda init dodaje obiekty tła do tablicy mainEntities.

Kolejna metoda to update. Już na początku widać, że wiedźma poruszać się będzie cały czas, ze stałą prędkością. Będzie to zręcznościowy aspekt gry, w końcu sterowanie magiczną miotłą nie może być zbyt proste. Strzałki na klawiaturze jedynie zmieniają kierunek lotu wiedźmy.

Kolejne linijki to wykorzystanie funkcji trygonometrycznych. Nie będę dokładnie opisywał jak to działa, poświęcę na to osobny post, ponieważ temat zdecydowanie tego wymaga. To co trzeba wiedzieć na potrzeby gry, to to, że wiedźma w poziomie porusza się po sinusoidzie. Jej „środek” to wartość pola baseY, które jest odpowiednio modyfikowane o sinus kąta angle. Kąt ten cały czas rośnie aż dojdzie do 1 (najwyższy punkt w sinusoidzie), wtedy jest z powrotem ustawiany na 0. Efektem tego jest to, że faktyczna współrzędna y wiedźmy to baseY który stopniowo rośnie o wartość od zera aż do tej określonej w zmiennej waveRange a następnie spada aż do wartości minus basY minus waveRange.

Następnie w kodzie znajdują się cztery wyrażenia warunkowe obsługujące sytuacje w których przyciskane są odpowiednie strzałki (a przeciwne im strzałki nie są). Strzałki w prawo i w lewo zmieniają kierunek lotu wiedźmy oraz wartość jej pola facing. Natomiast strzałki w górę i w dół, zmieniają wartość pola baseY.

Dalszy ciąg funkcji update jest identyczny z tym co opisałem wcześniej. W skrócie: wiedźma oraz ‚wyświetlacz’ nie mogą przenieść się poza krańce świat gry. ‚Wyświetlacz’ przesuwa się, gdy wiedźma doleci do obszarów przy jego lewej i prawej granicy. Gwiazdy przesuwają się wraz z ‚wyświetlaczem’, przez co sprawiają wrażenie, że są dalej.

Ostatni fragment kodu to metoda draw. I tutaj nie ma nic nowego. Rysunki pobierane są z głównego obrazka i rysowane na płótnie na podstawie informacji zawartych w obiektach je opisujących. W tej chwili to tylko wiedźma oraz dwa obiekty tła, ale już niedługo będzie ich znacznie więcej 🙂 .

Tak naprawdę najżmudniejsza część pracy za mną. Kolejne kroki to rozwój logiki gry. Najpierw dodam możliwość ukończenia gry poprzez zebranie wymaganych do tego przedmiotów. Kiedy to będzie gotowe, wypełnię świat gry potworami, które będą namiętnie przeszkadzać graczowi w osiągnięciu celu.

Jeżeli chcesz być na bieżąco z aktualizacjami tej gry, zachęcam do polubienia mojej strony na facebooku. Regularnie zamieszczam tam informacje o wszystkich nowościach na blogu.

Dodaj komentarz

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