06_sat

#Piszemy gre w SFML’u – Lekcja 4

Dzisiaj zajmiemy się generowaniem jednostek, a także kolizją. Czas aby nasza gra miała większy cel, czyli przetrwanie poszczególnych fal.

!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 już wspomniałem dzisiaj zajmiemy się obsługą jednostek na scenie naszej gry, jednak zanim nimi się zajmiemy to napiszmy sobie zabezpieczenie dzięki któremu nasz gracz nie będzie uciekał nam poza ekran:

Dodaliśmy dwie „serie/bloki” if’ów, jeden blok zawiera po 2 if’y odpowiednio dla pozycji X jak i Y. Omówię teraz jak działają te warunki na podstawie jednego bloku if’a dla pozycji X (pozycja Y działa w analogiczny sposób):

Pierwszy warunek nam sprawdza czy gracz nie wyszedł za ekran z prawej strony, jak zapewne zauważyłeś/aś podzieliłem akurat w tym wypadku liczbę 1285 na 2 części: 1280 i 5. Zrobiłem to dlatego aby łatwiej ci było zauważyć co oznacza ta liczba, bo np zamiast 1280 mógłbym tam wstawić zmienną oznaczającą szerokość ekranu np. SCREEN_WIDTH. A co oznacza liczba 5? Jest to liczba która określa margines za który może wyjść gracz poza ekran, dzięki temu gdy gracz tylko dotknie pozycję 1280 to nie zostanie od razu przeniesiony na pozycję (0,Y).

Jak zapewne zauważyłeś/aś pobieramy pozycję od shape i jak doskonale pamiętamy ustawiliśmy sobie środek naszej figury na zerowy wierzchołek:

 

Następnie w razie spełnienia warunku ustawiamy naszą pozycję zmieniając jedynie pozycję X na przeciwległy kraniec, a Y zostawiamy tak jak był. W sumie moglibyśmy także sprawić, że kamera poruszałaby się za graczem, można by to było zrobić za pomocą sf::View, ale o tym kiedy indziej.

Wróćmy do meritum tej lekcji, czyli jednostek oraz kolizji. Zanim zaczniemy pisać kod należy się zastanowić czym właściwie jednostka? Jednostką możemy nazwać każdy obiekt w naszej grze, czyli jednostką możemy nazwać między innymi gracza. Jednostką może być także pocisk który wystrzeli gracz, a także ten wystrzelony przez komputer, jednostkami będą także wrogie statki oraz asteroidy.

Zajmijmy się teraz najprostszą jednostką czyli pociskiem. tworzymy klasę od której będziemy dziedziczyć, nazwiemy ją Entity (od jednostek), będzie ona dziedziczyła po Drawable, dzięki czemu w łatwy sposób będziemy ją mogli narysować :).

Jak widzimy niewiele rzeczy teraz posiada, mamy zmienną bool, która decyduje o tym czy ta jednostka może skrzywdzić gracza (przecież nie chcemy aby gracz zestrzelił się własnymi pociskami). Zróbmy teraz strzelanie krok po kroku, tworzymy klasę Bullet, która będzie także naszym systemem pocisków, jeśli pocisk ucieknie poza ekran to my go usuniemy:

Dziedziczy ona po klasie Entity oraz Transformable oraz siłą rzeczy po Drawable (klasa Entity dziedziczy Drawable). Sam kod będziemy jeszcze rozbudowywać, jednak teraz zajmijmy się właściwym kodem:

Konstruktor jest raczej standardowy, ustawia czy pociski mogą skrzywdzić gracza, a także typ kształtu oraz kolor. Destruktor obecnie jest pusty, a więc nim się nie będziemy zajmować.

Teraz coś ciekawego, dodawanie kolejnych pocisków:

Argumenty przyjmują odpowiednio: f, czyli faktor po którym ma się poruszać nasz pocisk oraz pos, czyli pozycja początkowa, metoda append dodaje kolejny vertex do naszego VertexArray, podobnie robimy z f: „wrzucamy” go do naszego vectora factor, ta metoda będzie używana gdy gracz będzie chciał wystrzelić pocisk.

Teraz zajmiemy się remove, które ma za zadanie określony pocisk usunąć z tablicy poprzez przesunięcie go na sam koniec i zmniejszenie po tym tablicy. Metodą update zajmiemy się za chwilę, zmienimy także jej zawartość ponieważ ona była jedynie testowa. Reszta kodu powinna być prosta do zrozumienia, mam także zadanie dla ciebie, zmień metodę setColor tak aby zmieniała kolor WSZYSTKICH pocisków, także tych wystrzelonych.

Czas na przetestowanie obecnie działającego kodu, na chwilę wróćmy do klasy Player i zmienimy metodę getPosition(int), a to dlatego że obecnie zwracana tam metoda getPoint(index) nie zwracała bieżącej pozycji punktu, musimy ją zamienić na:

W ten sposób dostajemy pozycję punktu po wszelkich transformacjach typu zmiana pozycji, rotacje itp.

Idziemy do klasy Engine gdzie tworzymy obiekt (w Engine.h): Bullet bullets_player; i dopisujemy odpowiednio:

Tych linii nie będziemy już zmieniać, przejdźmy do runEngine() i pod warunkami wciśnięcia klawiszy dopisujemy:

Ten warunek jeszcze ulegnie zmianom, ale o tym za chwilę. Gdybyśmy teraz uruchomili kod zobaczymy jego niedoskonałość, mianowicie pociski będą leciałby w prawidłową stronę lecz tylko wtedy gdy gracz się porusza (polecam kod w tym momencie uruchomić, link do obecnej wersji kodu).

Wróćmy na chwilę do klasy Player i dodajmy jedną metodę:

Ten kod powinien być dla was znajomy, zwraca on factor pod jakim ma lecieć odpowiedni pocisk zwiększony o prędkość gracza w danej chwili, przy zmienieniu dodatkowo w Bullet update te wartości razy 2 dostaniemy 2 razy szybsze pociski:

Teraz zajmijmy się usuwaniem pocisków bo przecież nie chcemy sytuacji gdy gra nam się posypie bo tak zaspamujemy grę pociskami, że nie wystarczy pamięci.

Wyświetlamy sobie obecny stan pocisków przed aktualizacją oraz po:

Jak widać kod działa, lecz strzelamy za szybko, gdybyśmy tak strzelali w naszej grze gra traciłaby sens po potrafimy wystrzelić bardzo dużo pocisków w małym czasie. Dlatego też ustawimy 2 ograniczenia: częstotliwość z jaką będą wystrzelane pociski oraz ilość amunicji, na początku idziemy do klasy Bullet i dodajemy 2 zmiene: Clock timer, Time frequency, zmieniamy także konstruktor i dodajemy mu argument int = 500 i dopisujemy w nim:

W metodzie add dodajemy warunek:

Teraz strzelamy pociskami z dużą przerwą bo co pół sekundy, warto także dorobić metodę zmieniającą prędkość tak aby można było w to integrować po działaniu gry, np jako bonus czasowy przyspieszający prędkość strzelania po zebraniu jakiejś paczki.

Czas na amunicje, dodajemy do Bullet 2 zmienne: int ammunition oraz bool infinite, pierwsza oznacza ile amunicji posiadamy, 2 czy amunicja jest nieskończona, ustawiamy w konstruktorze odpowiednio wartości tym zmiennym: 100 oraz false. Tworzymy także nową metodę void addAmmunition(int ammo=100).

Myślę że sprawę pocisków mamy już na razie załatwioną, czas na naszego wroga: asteroidy.


Tworzymy nową klasę o nazwie Asteroids i zastanówmy się przez chwilę jak ta klasa powinna wyglądać:

  • powinna móc generować asteroidy różnej wielkości,
  • duże asteroidy po zniszczeniu przechodzą w mniejsze,
  • poruszają się ruchem jednostajnym prostoliniowym, w przypadku gdy zostają zniszczone odrywają się w różnych kierunkach,
  • po opuszczeniu ekranu usuwają się z pamięci.

Na podstawie tego możemy już wypisać sobie kilka nowych metod:

W zasadzie nie ma co tutaj analizować, z założenia wszystko jest łatwe do zrealizowania, a tak wygląda cały plik nagłówkowy:

Radzę przeanalizować co poszczególne rzeczy mogą robić jak nam się przydadzą, np łatwo wywnioskować, że struktura Asteroid jest stworzona jedynie dla naszej wygody, przechowuje ona prędkość z jaką się porusza, sam jej kształt oraz czy jest duża (parametr big), tzn. czy się rozpada na mniejsze po zniszczeniu. Tutaj mamy taki „standard” metod:

Jednak zajmijmy się pierwszą wersją metody generate jaką stworzyłem, chcę ci ją pokazać abyś zrozumiał/a że nie ma sensu komplikować sobie czasami życia:

Kod działa, nie powiem że nie, ale rezultat nie jest taki jak powinien być, wychodzi nam bardzo często takie coś jak na rysunku po lewej, a zależy nam zawsze na czymś w rodzaju figury po prawej:

Teraz należy zauważyć, że można podobny do tego uzyskać efekt w bardzo łatwy sposób, mianowicie, mamy środek okręgu oraz jego promień, jedyne co teraz musimy zrobić to wyliczyć współrzędne punktu na okręgu o środku (x,y) i promieniu r, wtedy będziemy mieli pożądany przez nas efekt.

06_sat_alg-scheme

Skoro już wiemy jak w teorii będziemy generować asteroidę, czas na praktykę, czyli kod:

Ten kod akurat zasługuje na omówienie: na początku sprawdzamy czy współrzędne są równe (0,0), standardowo taka współrzędna oznacza że ma wygenerować się zupełnie nowa asteroida, jeśli ma podane współrzędne oznacza to że powstała z rozpadu większej więc musi być mała.

Następnie ustawiamy wielkość promienia, w przypadku dużej jest o 50% większy od małego, w kolejnym kroku losujemy ilość wierzchołków z kilkoma ogranicznikami: co najmniej musi być ich 5, a maksymalnie 7.

W nieszczęsnej (dla mnie) pętli losujemy pozycje wierzchołków, ta linia jes bardzo ważna: float alpha = rand()%(360/size)+j*(360/size);, rozbijmy to sobie na 2 części:

  1. rand()%(360/size) – ta części losuje kąt tylko w pierwszej części koła, tzn, jeśli figura ma 4 wierzchołki to liczba jest losowana z przedziału [0,90).
  2. j*(360/size) – ta wartość jest dodawana do poprzedniej, oznacza ona w której części koła losowano liczbę, tzn: mamy 4 wierzchołki, a więc koło jest podzielone na 4 ćwiartki: 0-90, 90-180, 180-270, 270-360, załóżmy że w pierwszym kroku wylosowaliśmy liczbę ’45’, tzn że wylosowany kąt dla 2 ćwiartki to 45 + 1*(360/4 => 90) = 135, czyli alpha wynosi 135.

Kolejna linia to konwersja kąta na radiany, dalej mamy liczoną współrzędną punktu mając środek okręgu (zmienne x,y) i jego promień (radius), jak liczymy poszczególne wierzchołki?

  • x + cos(alpha) * radius
  • y + sin(alpha) * radius

To są ogólne wzorki, aby do nich dojść wystarczy kartka papieru, długopis oraz wiedza matematyczna, niech mi teraz ktoś powie, że matma się nie przydaje ;). Nie będziemy sobie tego tutaj rozpisywać jak do tego dojść, zaciekawionych zapraszam do próbowania zrobienia tego samemu.

Następnie widzimy dosyć łatwy kod, poszliśmy na łatwiznę i ustawiamy punkt wg którego obsługujemy naszą asteroidą z punktu 0, a nie z jego środka ciężkości, jeśli chcesz możesz to napisać i pochwalić się w komentarz.

Losowanie prędkości jest dosyć proste, na obecną chwilę nie potrzebujemy niczego bardziej zaawansowanego.

Skoro mamy możliwość tworzenia asteroid, przydałoby się móc je usuwać ze sceny gry, czyli czas na del()

Kod jest nam znany, jest dosłownie skopiowany z remove z klasy Bullet.

Efekt działania kodu

Czas na update, jeżeli ustawiłeś Origin na środek ciężkości figury, to możesz dodać też obracanie figury w bardzo łatwy sposób: po prostu dodaj w strukturze Asteroid zmienną która decyduje o rotacji.

Muszę przyznać, że całkiem ładnie nam to wyszło i chciałoby aż się rzecz że sprawę mamy załatwioną, lecz pozostała nam jeszcze 1 rzecz: metoda zwracająca pozycję poszczególnych wierzchołków dla odpowiedniej asteroidy.

Ta metoda też jest ci doskonale znana, nieco bardziej rozbudowana z klasy Player tylko że z dwoma argumentami: i to nr asteroidy, it to nr punktu w tej asteroidzie.


Polecam tutaj zrobić sobie przerwę, ponieważ zaraz rozpoczniemy kolizje, takie przerwy podczas intensywnego myślenia mają niesamowicie dobry wpływ na myślenie po powrocie.

Kolizje to coś co musi znaleźć się w każdej grze, w SFML mamy narzędzie dzięki któremu możemy w łatwy sposób sprawdzić czy zachodzi kolizja, ale (zawsze jest jakieś ale) nie jest ono dokładne, a raczej nie jest ono dokładne w obiektach innych niż prostokąt, czy kwadrat nie poddany rotacji, możemy zobaczyć to na przykładzie poniżej:

Ten czerwony kwadrat to wizualizacja FloatRect, bo o nim tutaj mowa, mimo że napis jest przekrzywiony za pomocą kodu to traktowany on jest jak o większy prostokąt, ponieważ FloatRect jest zawsze równoległy do ścian okna.

My zajmiemy się nieco innym sposobem wykrywania kolizji, ponieważ w naszym przypadku to by się nie sprawdziło, dlaczego? Wyobraźmy sobie taką sytuację: gracz strzela w asteroidę i mimo że pocisk jeszcze nie dotarł do asteroidy zostanie ona zniszczona, ponieważ wykryto kolizję.

Na czerwono: zasięg wykrycia kolizji

Jak widzimy pocisk (nieco powiększony) jest już w prostokącie FloatRect tej figury, pewnie za to że asteroida szybciej się rozpada nikt by się nie pogniewał, ale tak samo by działało otarcie się gracza z asteroidą, jeżeli by przeleciał odpowiednio blisko to by został zniszczony mimo tego że jej nie dotknął. Jak zapewne widzisz wkraczamy już w szczegóły naszego projektu, które wymagają od nas więcej napracowania.

Wchodzimy teraz w działy geometrii obliczeniowej, która jest w bardzo dużej mierze mi obca, wykorzystuje się tutaj m.in. działania na macierzach, czy skalarach, z tego powodu mogą poniżej w wyjaśnieniach pojawić się błędy, dlatego jeśli jakiś znajdziesz napisz mi o nim w komentarzu.

Do zaawansowanej kolizji pomiędzy obiektami WYPUKŁYMI wykorzystuje się algorytm SAT (Separating Axis Theroem), który sprawdza czy dwa obiekty wypukłe przecinają się. Wykorzystuje się tutaj twierdzenie, które mówi że jeśli dwa obiekty wypukłe nie przecinają się to między nimi da się narysować linię, jednak my wykorzystujemy tutaj nieco zmienioną wersję tego twierdzenia, która mówi, że:

jeżeli dwa wypukłe obiekty nie kolidują ze sobą to istnieje oś, dla której projekcje obu tych obiektów nie zachodzą na siebie.

O sposobach wykrywania kolizji napisałem 2-częściowy artykuł: Kolizje w grach 2D (Kategoria: Poradniki).

 


Skoro część typowo bibliograficzną mamy za sobą, czas na właściwy kod w naszej grze, jeżeli zdarzy się, że doszła jakaś metoda której nie omówiłem, ani nie wspomniałem wcześniej daj mi znać w komentarzu, ta lekcja powstawała w ciągu kilku dni i mogło się tak zdarzyć.

W klasie Asteroids tworzymy nową metodę bool isEmpty(), ta metoda ma za zadanie sprawdzić czy są utworzone jakieś asteroidy.

W Engine.h tworzymy funkcje(!):

Te funkcje sprawdzają czy w podanym przedziale nachodzą na siebie wielokąty i wyglądają one następująco:

Jeżeli przejrzałeś kod z kursu Polycolly to zapewne zauważyłeś, że ten kod praktycznie się niczym nie różni, a to dlatego, że nie rozumiem do końca wszystkich fragmentów, jednak spróbujmy to przeanalizować:

  • min = max = (axVertices[0].x * xAxis.x + axVertices[0].y * xAxis.y) – nadajemy tutaj wartość początkową zmiennym min oraz max, jest to produkt pomiędzy axVertices[0] i xAxis
  • następnie szukamy spomiędzy wszystkich wierzchołków wartość min oraz max

W klasie Engine deklarujemy metodę bool check_collision(Vector2f *A,int sizeA,Vector2f *B, int SizeB, Vector2f &offset), która sprawdza czy zaszła kolizja, przyjmuje ona za argumenty wskaźniki na współrzędne wierzchołków naszych figur, ich wielkość, a także sprawdzany wektor.

Sprawdzamy tutaj osie zarówno wielokąta A jak i B, ta metoda może maksymalnie przyjąć wielokąty składające się w sumie z 64 wierzchołków, decyduje o tym tablica xAxis[64], w pętlach następuje utworzenie wektora i sprawdzenie poprzez rzutowanie go czy istnieje kolizja pomiędzy obiektami.

Tutaj kończymy z kodem, o którym mam małe pojęcie, w metodzie update() napisałem taki kod dla testów:

Przeanalizujmy kod, sprawdzamy czy w ogóle są jakieś asteroidy jeśli tak to tworzymy tablicę i umieszczamy w niej wierzchołki pozycji gracza, możesz powiedzieć zaraz, zaraz przecież SAT działa tylko na figury wypukłe, a statek gracza jest figurą wklęsła w dodatku ma 4 wierzchołki. Masz rację i z tego względu nasza kolizja nie będzie doskonała, ponieważ z tego co przekazujemy później do gry wynika że gracz jest trójkątem, dzięki temu możemy posłużyć się tutaj SAT’em. Następnie w przypadku wystąpienia kolizji nasza gra wyświetla w konsoli tekst.

Jednak warto by było udoskonalić ten kod dla więcej niż 1 asteroidy, tak się wtedy prezentuje:

Obecnie graczowi nic się nie dzieje, tylko je niszczy ale tym zajmiemy się w lekcji po dźwiękach (czyli w lekcji 6).

Zanim przejdziemy dalej, opowiem wam pewną historyjkę, która jest także usprawiedliwieniem dlaczego ta część pojawia się tak późno. Wyobraźcie sobie sytuację gdzie macie taki kod:

Nasza funkcja zawsze zwraca false, lecz mimo tego if się wykonuje, dlaczego? Nie mam pojęcia, ja spotkałem się właśnie z takim bugiem przy kończeniu tej lekcji, mimo że sekundę wcześniej sprawdzałem co funkcja zwraca i wyświetlała mi konsola że fałsz, ale dalej instrukcje w if’ie się wykonywały. Strasznie długo się męczyłem z rozwiązaniem tego problemu, ostatecznie rozwiązanie okazało się banalne: usunąłem if’a z kodu i napisałem go znowu w niezmienionej formie, kod działa. Jest to najdziwniejszy błąd z jakim się spotkałem (korzystam z VS 2010).

Ot wykrywanie, czy punkt jest w środku wielokąta.

Ten fragment kodu znajduje się w klasie Engine w metodzie update() zaraz po sprawdzaniu pocisków, na początku sprawdzamy czy w ogóle, wystrzelono jakieś pociski, jeżeli tak mamy pętlę odpowiadającą za sprawdzenie wszystkich pocisków, następna pętla odpowiada za sprawdzenie wszystkich asteroid. Tutaj tworzymy tablicę za pomocą operatora new oraz uzupełniamy ją w kolejnej pętli, następnie widzimy if’a o którym dopiero opowiadałem, w którym cały czas wykonywały się instrukcje. Dalej mamy już tylko usunięcie tablicy z pamięci za pomocą delete.


Ta lekcja z pewnością by nie powstała gdyby nie opracowania niektórych tematów przez innych autorów, ponieważ wymaga ona wiedzy, której nie dostarcza się w liceum, pokazuje to także potrzebę ciągłego kształcenia się. Jeżeli planujesz napisać prostą grę zdecydowanie polecam ci wykorzystanie mapy kafelkowej, ponieważ do kafelków jest dużo łatwiej napisać kolizję.

Jeżeli się martwisz, że obecnie gra nie prezentuje się zbyt dobrze lub że pociski są słabo widoczne to tym zajmiemy się w jednej z kolejnych lekcji. Następna lekcja będzie trochę luźniejsza, ponieważ zajmiemy się dźwiękami i muzyką w grze.

Tradycyjnie, jestem otwarty na wszelkie uwagi, pytania, itp. Zapraszam do komentowania

 

Kod źródłowy

>>Pobierz<< | >>GitHub<<


  • Dejna

    Po ściągnieciu kodu źródłowego występuje bład

    /src/Engine.cpp||In constructor ‘Engine::Engine(sf::RenderWindow&)’:|

    /src/Engine.cpp|8|error: no matching function for call to ‘Asteroids::Asteroids()’

    nie wiem w czym jest błąd zwłaszcza po zakomentowaniu klasy asteroids program działa

    • Każdy kod był kompilowany po każdej lekcji (na win7, visual studio c++ 2010), nie modyfikowałeś go jakoś dodatkowo? Problem wydaje się leżeć w braku konstruktora Asteroids bez argumentów, więc proponuje dodać coś takiego, może pomoże: http://pastebin.com/YWtjjA1g

  • Jestem Nick

    Przed chwilą dokładnie tak jak Pan utknąłem na ifie który się wykonywał, także podczas zabawy z kolizjami. Okazało się że po instrukcji if(…) postawiłem średnik, Pan pewnie zrobił tak samo 😛
    if(!zmienna); <<<< tutaj 🙂
    {
    kod…
    }

    • W moim wypadku wina leżała w 100% po stronie visuala. Visual aby nie sprawdzać wszystkich plików przy każdej kompilacji pod kątem błędów sprawdza jedynie, które pliki zostały zmieniane. U mnie tej zmiany nie naniósł i uruchamiał wcześniej zachowany build (to było na wersji Express 2010).

  • pw1602

    Może błąd nie mający wielkiego znaczenia, ale przynajmniej mnie kłuje w oczy 😛

    „W klasie Engine deklarujemy metodę
    bool check_collision(Vector2f *A,int sizeA,Vector2f *B, int SizeB, Vector2f &offset)(…), ich wielekość, (…).”

    • Dzięki, ten kurs może mieć masę literówek z racji że jest najstarszy więc najmniej się przykładałem.

  • Karol

    Gdy dochodzę do momentu w którym zaleca pan uruchomić kod dostaje error:cannot declare field ‚Engine::bullets_player’ to be of abstract type ‚Bullet’

    • Hmm, wydaje mi się że sam wtedy robiłem przerwę na testy, ale może coś od momentu publikacji zmieniałem coś w kodzie (nie jestem w stanie stwierdzić na pewno, za dawno to było). W takim razie zignoruj to zalecenie 😉