#Piszemy gre w SFML’u – Lekcja 6

Jak bardzo często się okazuje, detale w grach są bardzo ważne i to one nie raz, nie dwa decydują o tym czy gra jest grywalna (wiem masło maślane), czy gracz pozostanie przy naszym produkcie na dłużej. Nasza gra posiada już podstawowe mechaniki, teraz czas je udoskonalić.

!UWAGA! Kurs leży w kategorii ‚Obsolete’ co oznacza, że może być nieaktualny, zawierać błędy i nie polecam z niego korzystać. [INFO]

Kod z poprzedniej lekcji

Jak ładnie ująłem to we wstępie nasza gra posiada już podstawowe mechaniki, które sprawiają że dla nas programistów, gra jest w zasadzie ukończona, ponieważ działa w pewien sposób kolizja pomiędzy obiektami, można strzelać pociskami w asteroidy. Jednak grze brakuje ważnego elementu, który jest m.in. naszym założeniem odnośnie gry: ma być to survival, a grą survivalową nie nazwiemy czegoś co generuje jedną asteroidę ;).

Muszę przyznać, że częściowo i nieświadomie oszukałem cię odnośnie tego co pojawi się w tej lekcji, ponieważ jak się okazuje, to wg wcześniej przygotowanego przeze mnie planu (o którym ostatnio zapomniałem) efekty specjalne pojawią się dopiero w następnej lekcji…


Zacznijmy od poprawek, jeżeli uruchamiałeś kod przygotowany w lekcjach, to pewnie zauważyłeś, że generowana asteroida nie porusza się, lecz stoi w miejscu. Jest to spowodowane tym, że w metodzie generate() po losowaniu prędkości nadałem jej wartość 0, dla mojej wygody przy sprawdzaniu kolizji, zdaje się że nie poinformowałem się o tym, dlatego też zamiast wartości (0,0) zmieniamy na:

Z racji, że nie lubię mieć bałaganu w kodzie, dlatego też utworzyłem w klasie Engine metodę typu void o nazwie collision() i do niej przeniosłem kod z metody update, który odpowiadał za sprawdzanie kolizji.

Skoro porządki mamy za sobą, czas na poważniejsze zmiany. Nasza gra jest dalej jakby nie patrzeć symulatorem statku z nieskończoną amunicją przeciwko (uwaga): 1 asteroidzie, któa i tak się zniszczy jeżeli wyleci poza ekran. Dlatego co planujemy zrobić podczas tej lekcji:

  • dodamy generator asteroid (tzn. fabrykę jednostek), która będzie generowała asteroidy,
  • dodamy ilość żyć gracza,
  • gracz będzie mógł (w końcu) zginąć,
  • wyświetlimy na ekranie: ilość żyć, amunicję, czas jaki przetrwaliśmy,
  • skoro mamy skończoną amunicję musimy dodać paczki z ammo,
  • jeżeli mamy paczki z ammo to dlaczego by nie dodać innych, np z życiem i tymczasowymi boostami?

Nie wiem jak twoim zdaniem, ale wg mnie gra z tego opisu już z miejsca prezentuje się lepiej i wbrew pozorom nie zajmie nam to dużo linii kodu jak się może wydawać, a więc czas popisać kod.

 

I. Fabryka jednostek


Przechodzimy do Asteroids::generate, gdzie zmienimy lekko kod, zamiast losowania pozycji asteroidy na całej scenie, losujemy ją tylko na krawędziach

Schemat losowania osi przeciwników
Schemat losowania osi przeciwników

Dodatkowo wyłączmy ich usuwanie gdy wyjdą poza krawędź i po prostu niech zachowują się jak gracz, dzięki temu gra będzie ciekawsza bo gracz nie będzie mógł się bawić w ich unikanie:

Ten kod powyżej wklejamy w update zamiast usuwania asteroid, skoro mamy to już za sobą zajmijmy się fabryką asteroid, która będzie działała na takiej zasadzie:

– na początku gry wygeneruj 5 asteroid,

– przy każdej nowej fali generuj 2 razy więcej asteroid niż ostatnio

Do Engine dodajemy takie zmienne jak (w nawiasach są wartości jakie nadajemy w konstruktorze):

size_t nr fali(1), liczba_asteroid(0), Clock czas_fali(czas_fali.restart()),czas_gry

Oprócz tego w runEngine tymczasowo nadajemy liczba_asteroid wartość 5, oraz modyfikujemy kod update w następujący sposób:

A także przy kolizji obiektów dodajemy linijkę: liczba_asteroid–; tak aby istniała szansa na spełnienie tego warunku. To tyle jeżeli chodzi o pierwsze założenie, przejdźmy do kolejnego.


Taka mała dygresja, w obecnej wersji kodu znajduje się mały błąd z usuwaniem asteroid, który powoduje błąd krytyczny gry informujący o przejściu poza zasięg vectora, w najbliższym czasie zostanie to poprawione przy lekcji gdzie tego błędu nie zauważyłem.

Problem  leży przy sprawdzaniu kolizji dla pocisku, po prostu pętla wciąż się wykonuje dla danego pocisku gdy już został usunięty i to generuje problemy, wystarczy dodać jeszcze jednego if’a którego zadaniem jest przerwanie pętli (metoda collision)

 

II. Życie i śmierć


Nieco dramatyczny nagłówek, ale i ważny w grze, bo co to za frajda z gry gdy nie możemy przegrać? Przejdźmy do klasy Player, gdzie dodamy parę rzeczy. Pamiętasz może enum „Status”, wreszcie zrobimy z niego pożytek. Dodajmy do niego jeszcze STATUS_IMMORTAL oznaczający, że gracz jest nieśmiertelny, przyda to się nam do bonusu, który będzie dawał graczowi tymczasową nieśmiertelność, a także dodajmy zmienną odpowiadającą za ilość żyć size_t lifes. Dodajemy także wskaźnik na Clock special_effects, ale tym zajmiemy się później. Wartości początkowe dla status to STATUS_ALIVE, a dla lifes to 3.

Brakuje nam jeszcze metody, która by mogła skrzywdzić gracza:

Jeżeli gracz zostanie trafiony przez asteroide i wciąż ma dodatkowe życia to dostaje na 2.5 sekundy nieśmiertelność oraz zmieniamy jego kolor na czerwony, w przeciwnym wypadku kończy się gra. Dodajemy także w destruktorze instrukcję:

Dzięki temu będziemy mieli pewność, że nie zostawiamy w pamięci jakichś niepotrzebnych danych, tworzymy metodę getLifes(), która po prostu zwraca lifes. Przyda nam się metoda do wyświetlania czasu trwania bonusu:

Metoda powyżej zwraca czas w milisekundach, który będziemy zamieniać sobie na sekundy. W Player::update dopisujemy:

Przechodzimy teraz do klasy Engine, gdzie wykrywamy kolizję pomiędzy graczem, a asteroidą i w ifie dopisujemy wywołanie metody enemy_coll(), if wygląda następująco:

Dopisujemy jeszcze w runEngine w pętli while(!menu) if’a, który sprawdza czy status gracza nie zmienił się na DEAD, jeżeli tak zmienia wartość menu na true.

W ten sposób wykonaliśmy kolejny punkt z założeń, czas na kolejny 🙂

 

III. Wyświetlamy użyteczne informacje dla gracza


Jak w każdej grze przydałoby się podzielić z graczem jakimiś informacjami, które mogą mu się przydać typu ilość amunicji, życia, czy czas, który przetrwał. W naszym przypadku nie będzie problemu z brakiem widoczności wyświetlanych danych, jednak zazwyczaj scena gry jest różnokolorowa i warto wyświetlić te dane na jakimś pasku, tak aby były w pełni czytelna. My nie będziemy go rysować ponieważ nie ma takiej potrzeby, jednak aby było ciekawiej informacje o czasie trwania specjalnego efektu będziemy wyświetlać jedynie gdy taki bonus będzie aktywny.

SQUARE – przykład gry z paskiem na którym wyświetlane są użyteczne dla gracza informacje

Najważniejsze informacje będą wyświetlone na górze ekranu, czyli: ilość żyć, czas gry, ammo. W prawym dolnym rogu wyświetlimy czas trwania specjalnego efektu. Pracujemy w klasie Engine i tworzymy tam tablicę (dla ułatwienia w komentarzu zapisałem, co który element oznacza)

Musimy także utworzyć sobie funkcję, zamieniającą liczby na string (musimy dołączyć bibliotekę sstream):

Przechodzimy do konstruktora, który tak wygląda po zmianach:

Linie odnośnie pozycji napisu będą aktywowane jedynie gdy będą potrzebne, nie będą aktualizowane cały czas (wyjątkiem jedynie jest czas gry). Mamy tutaj ustawienie napisów w odpowiednich pozycjach, stąd różne argumenty w ich ustawianiu, pierwszy napis był łatwiejszy do zrealizowania, ponieważ nie musieliśmy uwzględniać długości napisu.

Drugi napis (a raczej pozycja drugiego napisu) powstał w sposób, który jest już nam doskonale znany, dzielmy szerokość ekranu przez 2 i odejmujemy od tego połowę szerokości drugiego napisu.  W ten sposób zyskujemy pozycję x na której musimy umieścić nasz napis aby był idealnie na środku ekranu.

Pozycja trzeciego napisała powstała przez odjęcie od szerokości ekranu szerokości napisu oraz dodatkowo od 10, ponieważ chcemy mieć 10-pikselowy margines.

W zasadzie nie musielibyśmy sie tak bawić, jeżeli ustawilibyśmy sobie na początku punkt wg któóego poruszamy całą figurę jako środek figury 😉


Poustawiajmy sobie zmianę tekstu, dla poszczególnych napisów:

napis[0] ulegnie zmianie gdy gracz straci życie, więc wystarczy, że w metodzie collision() przy wykryciu kolizji zapiszemy linijkę kodu:

Z napis[1] jest nieco więcej zabawy i go musimy niestety aktualizować bez przerwy, bo on wyświetla czas gry, a więc idziemy do update().

Tutaj mam dla was kolejną cenną uwagę odnośnie SFML’a, która pokazuje, że człowiek uczy się na błędach. Chciałem utworzyć zmienną do czcionki tylko tymczasowo, co okazało się głupie z mojej strony bo sprawiło, że musiałem męczyć się z błędem „Access violation reading location”. Dlatego też, jeżeli będzie wam wyskakiwał taki błąd w debuggerze i wskazywał na metody ustawiania stringa dla sf::Text, a inne pozostawi w spokoju (no i jeszcze gdy będziecie chcieli wyświetlić tekst), to będzie znaczyło, że gdzieś obiekt Font wam się usunął z pamięci 🙂

W update zmieniło się nieco, przede wszystkim zmieniliśmy typ obiektu czas_gry na Clock, a to dlatego, że chcemy wyświetlać na bieżąco czas całej gry. Oczywiście przy każdej aktualizacji wyśrodkowujemy napis.

Następną rzeczą jaką chcemy wyświetlić jest ilość amunicji, dlatego też umiejscowimy kod do aktualizowania napisu w collision, w sprawdzaniu pocisków:

Zostało nam jeszcze dodanie ostatniego napisu, który będzie się zmieniał w update i będzie działał w następujący sposób, jeżeli czas do końca bonusu wynosi 0 to zacznie znikać, jeżeli czas do końca bonusu jest większy od zera to będzie się on pojawiał:

Kod jest w miarę prosty jeżeli rozumiemy jak działa system kolorów RGBA i tutaj wykorzystujemy ostatnią literę A (alpha), ale o tym za chwilę. Na początku sprawdzamy czy mamy przyciemnić tekst (czy ma zniknąć), następnie pobieramy kolor tekstu gdzie interesuje nas jedynie wartość a (alpha), czyli przezroczystość tekstu, który zmniejszamy o 5 dopóki jego wartość nie będzie równa 0. Im wartość alpha bliższa zeru tym obiekt jest bardziej przezroczysty, wartość 255 oznacza że obiekt nie jest wcale przezroczysty.

Rozjaśnianie działa w sposób analogiczny, tylko że w nim dodajemy wartość o 5. Pod if’em mamy standardowe ustawienie stringa oraz prawidłowe umieszczenie go na scenie.

Pozostało nam jeszcze narysowanie tych obiektów, co robimy oczywiście w draw():

 

IV. Rozdajemy amunicję, życie i nieśmiertelność


Przemyślałem kwestię paczek i postanowiłem lekko zmienić koncepcję, mianowicie wpadłem na 2 pomysły: amunicja itd będzie przydzielana po zniszczeniu asteroidy w sposób losowy lub będzie działo się to dopiero po skończeniu fali i zostaniemy przy tym drugim pomyśle.

Tworzymy  w Player nową metody:

I znowu przechodzimy do Engine, a konkretnie do update(), gdzie jest if odnośnie nowej fali.


To tyle na dzisiaj, w kolejnej lekcji zajmiemy się poprawą wyglądu menu, a także powiększymy pociski i je nieco poprawimy. Bonusowy filmik  z gry:

 

Kod źródłowy:
>>Pobierz<< | >>GitHub<<


  • pw1602

    Brakuje schematu losowania jednostek.

    • Dzięki za info, ale okazało się że i tak specjalnie pomocny to on nie był.