[Piszemy grę RPG] #3 Gracz, część 1

Witam w kolejnej części poradnika Piszemy grę RPG, w której zajmiemy się klasą gracza.

W poprzedniej lekcji zajmowaliśmy się wczytywaniem i rysowaniem mapy, jeżeli nie posiadasz kodu z poprzedniej lekcji, to możesz pobrać go stąd.

 

Koncept gracza


Jak w przypadku pisania np. klasy Level, tak i teraz musimy się zastanowić jak powinna wyglądać klasa gracza zarówno na tym wstępnym etapie, jak i późniejszym gdy będziemy chcieli go łatwo rozbudować o nowe funkcje.

Jak pamiętamy dobrze ze schematu, który poznaliśmy w pierwszej lekcji klasa Player, będzie klasą abstrakcyjną, co oznacza że musi zawierać jedynie części wspólne łączące wszystkie klasy postaci (Maga, Łucznika i Wojownika).

Jak w każdym szanującym się RPG, każda postać posiada:

  • odpowiednio groźne imię,
  • statystyki (punkty życia, siłę, zręczność, inteligencję),
  • punkty doświadczenia i poziom postaci,
  • ekwipunek,
  • punkty talentów,
  • … .

W tej lekcji jedynie zadeklarujemy sobie te cechy (i to nie wszystkie), ponieważ tematem tej lekcji jest poruszanie się oraz kolizja pomiędzy graczem i otoczeniem, resztą zajmiemy się później.

Poniżej na screenie możecie zobaczyć przeze mnie monety-pionki, które reprezentują poszczególne klasy postaci, paczkę z postaciami znajdziesz tutaj.

Ranger | Warrior | Mage
Ranger | Warrior | Mage

 

Klasa Player wersja alpha


Pliki z monetami symbolizujące klasy postaci kopiujemy do folderu data. Poniżej przedstawiam plik nagłówkowy klasy Player  do samodzielnej analizy.

Zajmijmy się standardowym konstruktorem tej klasy, który ustawia domyślne wartości poszczególnych zmiennych.

Destruktor pozostawiamy pusty, kolejne 3 metody są dość oczywiste w swojej budowie i nie sądzę aby potrzebowały komentarza.

Kolejną funkcją jest ustawienie wskaźnika na teksturę, zapewne się dziwicie po co nam wskaźnik na teksturę skoro za chwilę i tak przekazujemy adres do sprite’a. W przypadku animacji to rozwiązanie ma więcej sensu i dla czystego formalizmu w którejś lekcji zobaczymy jak można tą konstrukcję wykorzystać.

Następna metoda służy do rysowania obiektu na scenie. Tutaj rysowanie odbędzie się w nieco inny sposób i co prawda moglibyśmy dziedziczyć klasą Player , po Drawable  dzięki czemu instrukcja: window.draw(player)  w klasie Game  byłaby legalna, jednak takie rozwiązanie jest nieco prostsze (jak dziedziczyć po Drawable  pokazałem w innych poradnikach).

Ostatnie trywialne metody:

 

Poruszanie wersja alpha (proste poruszanie)


Teraz zajmiemy się prostym poruszaniem gracza po scenie, chcemy aby gracz po wciśnięciu strzałek w sposób widoczny dla naszego oka się poruszał.

Oczywisty jest fakt, że musimy pozbyć się naszego tymczasowego reprezentanta gracza, który był Vectorem o nazwie player, zatem dodajemy do pliku nagłówkowego: #include "Player.h" oraz zamieniamy typ Vector2f player  na Player player.

Nie zapomnijmy także o zdefiniowaniu nowej tekstury do przechowywania teksturę gracza:

 

W konstruktorze wczytujemy teksturę gracza, którą tymczasowo w moim wypadku zawsze będzie moneta Rangera, w momencie gdy dodamy możliwość wyboru klasy postaci to zmienimy tutaj nasz kod.

Zmieniamy także sposób w jaki ustawimy startową pozycję gracza, oraz przypiszemy mu teksturę (wciąż jesteśmy w konstruktorze).

Zmiany czekają nas także w pętli przechwytywania klawiszy funkcji start(), dla ułatwienia podaję metodę po zmianach (zmieniliśmy napisania typu player.x += TILESIZE na player.move(TILESIZE,0)):

Czeka nas jeszcze jedna zmiana z racji wcześniejszego posługiwania się Vectorem, w metodzie updateMap(), podmieniamy pierwszą linię na:

Aby zobaczyć gracza na scenie wystarczy zaktualizować draw() o linię:

Ta linia znajduje się zaraz po narysowaniu mapy na scenie.

Efekt po uruchomieniu
Efekt po uruchomieniu

 

Poruszanie wersja beta (płynne poruszanie)


Jeżeli uruchomiłeś kod po ostatnich zmianach (jeżeli nie to polecam uruchomić), to prawdopodobnie zauważyłeś, że nasza postać porusza się „skokowo”, tzn nagle zmienia pozycję o szerokość kafelka. Kolejnym niepożądanym elementem jest fakt, że gracz może wyjść poza ekran.

W tym paragrafie zajmiemy się nieco płynniejszym poruszaniem, przy okazji wyjdzie użyteczność wysyłania argumentu delta w metodzie update(..) .

Teraz trochę namieszamy w funkcjach, uważaj bo będzie się działo 😉

Deklarujemy w pliku nagłówkowym:

Możesz powiedzieć: „hej, przecież już mieliśmy zadeklarowane funkcje float x() , float y() , void move(...) , czemu podałeś je jako coś nowego, czyżby błąd?”, cóż… masz rację, ale nie do końca. Bo to nie są zupełnie stare metody, zmienimy ich sposób działania, zaraz wszystko się wyjaśni.

Od początku, doszły nam dwie nowe zmienne: dstPos oraz offsetPos. Pierwsza z nich to docelowa pozycja gracza na scenie, z kolei druga to różnica docelowej pozycji i aktualnej pozycji gracza, czyli jest to wektor przesunięcia gracza.

Przyjrzyjmy się pierwszemu wariactwu jakie uczyniliśmy po to aby nie musieć zmieniać kodu w Game.cpp na nieco bardziej aktualne funkcje.

Przyjrzyjmy się jakiejś parze funkcji, np x() i realX(), jak widzimy wcześniejsza zawartość x() wskoczyła do realX(), z kolei x() teraz zwraca przyszłą pozycję gracza! Czyli tą, którą za chwilę będzie miał i chcemy aby kamera, w której jest to używane pod uwagę brała pozycję o „pełnych” wartościach (wielokrotności rozmiaru kafla), dzięki temu mamy pewność, że nie dojdzie do sytuacji gdy kamera pobierze pozycję gracza, gdy ten będzie w trakcie wykonywania ruchu przez co mogłaby się ustawić w zły sposób.

Kolejny twist to analogiczna sytuacja jak w poprzednim przypadku: do moveInstant(..) wpada zawartość poprzedniego move(..) z tym, że argumenty są innego typu!

Nasza funkcja do poruszania działa wtedy i tylko wtedy, gdy poprzedni ruch (przesunięcie) gracza zostało wykonane do końca, w środku funkcji aktualizujemy wcześniej stworzone zmienne.

Mała aktualizacja ustawiania pozycji, musimy nadpisać dstPos, aby gracz nie zmierzał w kierunku poprzedniej pozycji 😉

Samo przesuwanie gracza znajduje się w metodzie update(). Przyjrzyjmy się jej nieco dokładniej.

W metodzie update() na samym początku sprawdzamy czy w ogóle trzeba wykonać jakiś ruch, za samo przemieszczanie odpowiada jedynie linia:

Która mówi tyle: weź ilość pikseli o jakie masz przesunąć obiekt i przemnóż przez czas jaki minął od wyświetlenia ostatniej klatki (zazwyczaj ta wartość jest określona na przedziale (0,0.1], chociaż oczywiście górna granica może dążyć do nieskończoności).

Jak łatwo sobie wyobrazić otrzymamy wartości ~1, jednak szansa na to że ich suma zwróci liczbę równą 1 jest mała, co oznacza że musimy sprawdzać czy pozycja gracza jest w pobliżu docelowej pozycji +/- epsilon. Jak widzisz zmienne speed oraz epsilon przyjąłem jako 1, jednak są one zupełnie dowolne i możesz bez problemu je zmienić w ramach swoich potrzeb.

Dalsze warunki sprawdzają czy pozycja gracza znajduje się wewnątrz naszego epsilona oraz czy akurat zmieniamy daną pozycję, druga część warunku mogła też być postaci offset.x > 0 , lecz myślę że obecna forma lepiej wyjaśnia skąd ten warunek się bierze.

Jak niewiele trzeba, aby dość uboga w mechanikę „gra” zaczynała wyglądać coraz lepiej, mam nadzieję, że czujemy drzemiący w SFML potencjał, tą niezwykłą prostotę, aż chce się powiedzieć „How cool is that!”.

Jeżeli czujesz (w sumie naturalną) potrzebę ulepszenia pracy kamery, aby nie przemieszczała się skokowo to zachęcam cię do ulepszenia jej przemieszczania w analogiczny sposób jak zrobiliśmy to z graczem.

 

Dygresja odnośnie poruszania wersji beta


Sposób w jaki wykonaliśmy to poruszanie jest w 100% jednakże prawie identyczny efekt można było osiągnąć przy użyciu jednej dodatkowej zmiennej przechowującej docelową pozycję gracza.

Główną różnicą jest to, że w miarę przybliżania się gracza do pozycji docelowej to prędkość gracza by minimalnie zwalniała (wynika to z tego, że różnica odległości maleje) oraz pozycja gracza oscylowałaby w pobliżu pozycji docelowej (gracz by drgał, raz jego pozycja byłaby minimalnie większa, innym razem mniejsza).

Przemieszczanie zastosowane w poprzednim paragrafie zapewnia też minimalnie lepszą płynność i akurat w tym przypadku jest moim zdaniem lepsze, dlatego zostaniemy przy nim.

 

Poruszanie wersja gamma (kolizje)


Zgodnie z tym o czym (chyba) mówiłem wcześniej, będziemy dążyli do maksymalnego uproszczenia niektórych mechanik i to właśnie uczynimy z wykrywaniem kolizji, jest to jednocześnie powodem dla którego bawimy się na widoku od góry na mapie kafelkowej. Dzięki temu możemy zastosować łatwy w użyciu system box-colliderów.

Dla niewtajemniczonych krótki opis (więcej informacji znajdziesz m.in tutaj):

Box-collider jak sama nazwa mówi służy do wykrywania kolizji pomiędzy czworokątem, a dowolnym innym obiektem, jego implementacja jest jedną z łatwiejszych, a w przypadku mapy kafelkowej sprawa się jeszcze bardziej upraszcza.

Do dzieła!

Zaczniemy od ograniczenia możliwości wychodzenia poza poziom, dzięki czemu unikniemy kilka przykrych w przyszłości sytuacji.

Od teraz będziemy poruszali graczem z poziomu innej metody, która jako argumenty przyjmuje o ile pikseli ma się gracz poruszyć (czyli nic nowego).

Wywołujemy ją po wciśnięciu przycisków, po prostu zamieniamy player.move(..), na movePlayer(..).

Sama istota tej funkcji to wyliczenie pozycji gracza po przemieszczeniu, następnie tą wartość konwertujemy na wymiary w kaflach i sprawdzamy czy znajdują się wewnątrz map, jeżeli nie to nie wykonujemy ruchu.

Czas na wielki moment, kolizje potrafią być dość trudnym tematem dla osób zaczynających pisanie gier, oto najważniejsze linie w tym paragrafie (dopisujemy pod if’em sprawdzającym czy wychodzimy poza krawędzie mapy):

Tak jak mówiłem: maksymalne uproszczenie, w dodatku niezbyt odczuwamy wykrywanie kolizji przez cokolwiek. W ramach testów polecam zmienić nieco naszą mapę testową i pozmieniać w niej niektóre kafelki na nr’y: 7,8,9, czyli odpowiedniki kafelków kamieni w grze.

Na koniec jeszcze filmik pokazujący to co dzisiaj udało nam się osiągnąć:

Podsumowanie


W tej lekcji zrobiliśmy kolejny krok w kierunku ukończenia naszej gry, kolejna zdobyta wiedza. Napisaliśmy prostą reprezentację gracza, która potrafi się już poruszać, a także dodaliśmy kolizje do naszej gry.

 

download

Jak zwykle zapraszam do systemu komentarzy i zapraszam do kolejnej lekcji, w której skupimy się na interakcji z obiektami.

Code ON!


  • Adam Pajkert

    Wygląda obiecująco, ciekawi mnie jak poradzisz sobie z kilkoma problemami które wystąpią potem , z niecierpliwością czekam na kolejną część, pozdrawiam 😀

    • Z jakimi problemami? Jeżeli się uda to załatwimy wszystko bez żadnych komplikacji, bo kod mam dość dobrze rozplanowany. Co do kolejnych części to w tym miesiącu powinny się pojawić jeszcze dwie (może trzy). Pozdrawiam

  • Pan Kulomb

    Co będzie w kolejnych lekcjach?

    • W tym kursie będę starał się poruszać zgodnie z przedstawionym przeze mnie schematem z lekcji 1, najbliższe lekcję będą dotyczyły gracza, a konkretnie: interakcji z obiektami na scenie, klasami postaci oraz drzewka umiejętności. Pozdrawiam.

  • Pan Kulomb

    Zdecydowanie za dużo robisz komentarzy. Często tłumaczą one tylko język angielski na polski.

    W ogóle jaki jest sens takiego komentarza?!
    sf::Sprite sprite; // sprite

    • Możliwe, że jest ich sporo, ale robię je głównie z dwóch powodów:
      1) komentarze w plikach nagłówkowych to skróty ułatwiające szybkie przeszukiwanie kodu, dużo łatwiej jest przeszukać kod gdy jest w tej postaci niż szukać nazw zmiennych pomiędzy typami funkcji i nawiasami (tutaj zawiera się także moja odpowiedź na twój zarzut odnośnie sprite’a)
      2) sporo osób odwiedza mojego bloga dokładnie z tego powodu, że jest po polsku, stąd zwykłe tłumaczenie z angielskiego.

      • Pan Kulomb

        Tamta linia była w pliku nagłównkowym, a nie źródłowym, a deklaracje funkcji i tak były oddzielone od zmiennych.

        • Przecież nic nie pisałem o plikach źródłowych? A nie zawsze deklaracje zmiennych są wyraźnie oddzielone od metod.

  • Program123

    Postarasz się stworzyć tutorial odnośnie kolizji w grach platformowych?

    • To co tutaj wykorzystałem nadaje można wykorzystać także w platformówkach, jednak wiem że chodzi ci o nieco płynniejszą fizykę gdzie dochodzi jeszcze grawitacja, a pola na których mogą się zderzyć postacie nie są na sztywno zdefiniowane jak tutaj, co nieco komplikuje sprawę, Jeżeli potrzebujesz artykułu dotyczącego jedynie kolizji to napisałem jakiś czas temu część pierwszą skrótu kolizji 2D: http://szymonsiarkiewicz.pl/poradniki/kolizje-w-grach-2d/

  • Lora

    Dlaczego poruszanie się realizujesz za pomocą eventów? Takie sterowanie jest niewygodne.

    • W jakim sensie niewygodne? W tym wypadku jest zupełnie wystarczające.

      • Lora

        W sensie, że trzeba ciągle klikać strzałkę żeby się poruszać, zamiast po prostu przytrzymać klawisz.

        • Tylko, że ta gra jest stylizowana na grę planszową, gdzie 1 ruch to 1 tura.