Prosta gra 2D C++

Mając do dyspozycji bibliotekę graficzną - wyświetlenie czegoś na ekranie nie jest specjalną filozofią. Inaczej ma się natomiast sprawa z wprawieniem obiektów w ruch i tej tematyce zostanie poświęcony niniejszy rozdział. 

Obiekty sceny

Zanim przejdziemy do właściwej części rozdziału, koniecznym jest przygotowanie struktury, która będzie reprezentowała obiekt na scenie. Struktura dla obiektu w niniejszym rozdziale będzie skromna, ponieważ jedyne co będziemy w niej przechowywali to pozycję w osiach X i Y. Na potrzeby niniejszego rozdziału struktura obiektu będzie więc wyglądała następująco: 

C/C++
struct RObiekt
{
    double x;
    double y;
    
    RObiekt( double f_x = 0.0, double f_y = 0.0 )
        : x( f_x )
        , y( f_y )
    { }
}; //struct RObiekt

Do tego wszystkiego dorzucimy jeszcze wektor przechowujący obiekty: 

C/C++
typedef std::vector < RObiekt > VObiektyT;
VObiektyT vObiekty;

Przechowywanie wszystkich obiektów sceny w jednym kontenerze ma dla nas istotne znaczenie oraz niesie ze sobą wiele konsekwencji. Tymi konsekwencjami są: 

  • możliwość dynamicznego dodawania obiektów na scenę w trakcie działania gry;
  • możliwość obsługiwania wszystkich obiektów sceny przy pomocy tego samego kodu (np. pętlą for);
  • kod jest krótszy, a przez to czytelniejszy;
  • ewentualne błędy łatwiej wykryć, ponieważ albo dotyczą wszystkich obiektów albo żadnego;
  • architektura wymusza aby obiekty posiadały wspólne cechy;
  • personalizacja własności obiektu wymaga znajomości dziedziczenia.

Na chwilę obecną nie będzie nas interesowało personalizowanie własności obiektów, więc kwestie związane z dziedziczeniem nie będą na razie omawiane. W każdym razie praca na zbiorach danych jest dużo wygodniejsza i praktyczniejsza aniżeli indywidualne podejście do każdego obiektu sceny dlatego też użycie kontenera do przechowywania obiektów jest po prostu zalecane i tym samym wskazane. 
  
Wektor, który został już przez nas stworzony wypełnimy danymi. Tymi danymi są oczywiście obiekty sceny z którymi będziemy pracować: 

C/C++
vObiekty.push_back( RObiekt( 50, 50 ) );
vObiekty.push_back( RObiekt( 100, 250 ) );
vObiekty.push_back( RObiekt( 300, 100 ) );
vObiekty.push_back( RObiekt( 500, 500 ) );

Obiekty jak już wspominałem w niniejszym rozdziale są skromne i posiadają jedynie swoje położenie - na nasze obecne potrzeby jest to w zupełności wystarczające. 

Rysowanie obiektów

Istotną zaletą umieszczenia wszystkich obiektów w jednym kontenerze jest możliwość łatwego implementowania wszelkiego rodzaju algorytmów. W tym wypadku będzie to algorytm rysujący obiekty na scenie: 

C/C++
oknoAplikacji.Clear();
for( VObiektyT::const_iterator i = vObiekty.begin(); i != vObiekty.end(); ++)
     oknoAplikacji.Draw( sf::Shape::Circle( i->x, i->y, 10, sf::Color::Red ) );

oknoAplikacji.Display();

W powyższym algorytmie przyjęto, że każdy obiekt będzie reprezentowany przez czerwone kółko - nic nie stoi oczywiście na przeszkodzie by to były sprajty, jednak im kod krótszy tym prostszy do zrozumienia, więc taka też implementacja zostanie zastosowana w niniejszym rozdziale. 

Przemieszczanie obiektów

Przemieszczanie obiektów - podejście złe

Jeżeli do tej pory programowałeś tylko i wyłącznie w konsoli bądź tworzyłeś aplikacje okienkowe to z pewnością przemieszczanie obiektu z pozycji obecnej w osi x do pozycji x=100 napisałbyś tak: 

C/C++
RObiekt & obiekt = vObiekty.at( 0 );
for(; obiekt.<= 100; ++obiekt.)
     oknoAplikacji.Draw( sf::Shape::Circle( obiekt.x, obiekt.y, 10, sf::Color::Red ) );

Jeżeli miałbyś następnie przemieścić obiekt w osi y do pozycji y=300 to zapewne napisałbyś kod następujący: 

C/C++
for(; obiekt.<= 300; ++obiekt.)
     oknoAplikacji.Draw( sf::Shape::Circle( obiekt.x, obiekt.y, 10, sf::Color::Red ) );

Takie podejście tworzenia ruchu w grze jest złe z kilku powodów: 

  • nie masz jak obsłużyć kilku animacji jednocześnie;
  • nie możesz zmienić kierunku ruchu obiektu np. w wyniku wystąpienia zderzenia obiektów (czyli wystąpienia kolizji);
  • animacja nie będzie widoczna w przypadku gdy biblioteka graficzna używa podwójnego buforowania - zobaczysz tylko i wyłącznie jak obiekt zmienił położenie z punktu początkowego do punktu końcowego;
  • wytwarzasz kod, którego nie będziesz w stanie dalej rozwijać.

Przemieszczanie obiektów - podejście poprawne

Skoro wiesz już jak nie należy robić przemieszczania obiektów to czas najwyższy zapoznać się z dobrymi i sprawdzonymi praktykami. Przyjrzyjmy się zatem uważnie naszej głównej pętli gry: 

C/C++
while( oknoAplikacji.IsOpened() ) //główna pętla gry
{
    //Obsługa zdarzeń SFML (np. wyjście z aplikacji itp)
    // ... jakiś kod ... //
    
    //obsługa gry (TO NAS INTERESUJE):
    // ... jakiś kod ... //
    
    //Wyświetlanie obiektów (to nas w sumie teraz nie interesuje):
    oknoAplikacji.Clear();
    for( VObiektyT::const_iterator i = vObiekty.begin(); i != vObiekty.end(); ++)
         oknoAplikacji.Draw( sf::Shape::Circle( i->x, i->y, 10, sf::Color::Red ) );
    
    oknoAplikacji.Display();
}

Istotą poprawnego pisania gier u podstaw jest zrozumienie jaka idea przyświeca głównej pętli gry - tą ideą oczywiście jest renderowanie jednej klatki w każdym jednym przebiegu pętli. Oznacza to, że w jednym przebiegu pętli możemy zmodyfikować pozycje wszystkich obiektów pamiętając jednocześnie, że myślimy o kolejnej klatce, która ma się pokazać użytkownikowi gry, a nie o efekcie docelowym jaki użytkownik chce osiągnąć (np. przemieścić postać z punktu A do punktu B). Przemieszczenie wspomnianej postaci z punktu A do punktu B musi nastąpić w czasie, a zatem klatka po klatce będziemy przesuwali obiekt tak aby znalazł się bliżej punktu B. Przemieszczanie obiektu możemy zrealizować np. tak: 

C/C++
void PrzesunObiekt( RObiekt & obiekt, double idzDoX, double idzDoY, double fPredkosc = 1.0 )
{
    if( obiekt.< idzDoX )
    {
        obiekt.+= fPredkosc; //idź w kierunku celu z podaną prędkością
        if( obiekt.> idzDoX )
             obiekt.= idzDoX; //jeżeli przeszliśmy cel to wróć do celu
        
    } else
    if( obiekt.> idzDoX )
    {
        obiekt.-= fPredkosc; //idź w kierunku celu z podaną prędkością
        if( obiekt.< idzDoX )
             obiekt.= idzDoX; //jeżeli przeszliśmy cel to wróć do celu
        
    }
    
    //Analogicznie oś Y:
    if( obiekt.< idzDoY )
    {
        obiekt.+= fPredkosc;
        if( obiekt.> idzDoY )
             obiekt.= idzDoY;
        
    } else
    if( obiekt.> idzDoY )
    {
        obiekt.-= fPredkosc;
        if( obiekt.< idzDoY )
             obiekt.= idzDoY;
        
    }
}

Jeszcze lepszym rozwiązaniem będzie, jeżeli umieścimy powyższą funkcję jako metodę struktury RObject i pozbędziemy się argumentów fPredkosc oraz obiekt. Pole fPredkosc przeniesiemy oczywiście do struktury RObiekt tak abyśmy mogli każdemu obiektowi nadawać różne prędkości. Po wspomnianych przeróbkach i złożeniu kodu w całość, aplikacja będzie wyglądała następująco: 

C/C++
#include <SFML/Graphics.hpp>

struct RObiekt
{
    double x;
    double y;
    double fPredkosc;
    
    RObiekt( double f_x = 0.0, double f_y = 0.0, double f_fPredkosc = 1.0 )
        : x( f_x )
        , y( f_y )
        , fPredkosc( f_fPredkosc )
    { }
    
    void PrzesunObiekt( double idzDoX, double idzDoY )
    {
        if( x < idzDoX )
        {
            x += fPredkosc;
            if( x > idzDoX )
                 x = idzDoX;
            
        } else
        if( x > idzDoX )
        {
            x -= fPredkosc;
            if( x < idzDoX )
                 x = idzDoX;
            
        }
        
        if( y < idzDoY )
        {
            y += fPredkosc;
            if( y > idzDoY )
                 y = idzDoY;
            
        } else
        if( y > idzDoY )
        {
            y -= fPredkosc;
            if( y < idzDoY )
                 y = idzDoY;
            
        }
    }
}; //struct RObiekt

int main()
{
    sf::RenderWindow oknoAplikacji( sf::VideoMode( 800, 600, 32 ), "Wytwarzanie Gier 2D, C++ | http://cpp0x.pl" );
    oknoAplikacji.UseVerticalSync( true ); //Włączenie synchronizacji pionowej - stała liczba FPS (zazwyczaj 60) - zadziała pod warunkiem, że system nie wymusza na aplikacji wyłączenia tego trybu
    
    typedef std::vector < RObiekt > VObiektyT;
    VObiektyT vObiekty;
    
    vObiekty.push_back( RObiekt( 50, 50, 1.0 ) );
    vObiekty.push_back( RObiekt( 100, 250, 3.5 ) );
    vObiekty.push_back( RObiekt( 300, 100, 5.0 ) );
    vObiekty.push_back( RObiekt( 500, 500, 2.5 ) );
    
    while( oknoAplikacji.IsOpened() )
    {
        sf::Event zdarzenie;
        while( oknoAplikacji.GetEvent( zdarzenie ) )
        {
            if( zdarzenie.Type == sf::Event::Closed )
                 oknoAplikacji.Close();
            
            if( zdarzenie.Type == sf::Event::KeyPressed && zdarzenie.Key.Code == sf::Key::Escape )
                 oknoAplikacji.Close();
            
        } //while
        
        //Przemieszczanie obiektów:
        vObiekty[ 0 ].PrzesunObiekt( 300, 300 );
        vObiekty[ 1 ].PrzesunObiekt( 500, 500 );
        vObiekty[ 2 ].PrzesunObiekt( 100, 100 );
        vObiekty[ 3 ].PrzesunObiekt( 300, 40 );
        
        
        oknoAplikacji.Clear();
        for( VObiektyT::const_iterator i = vObiekty.begin(); i != vObiekty.end(); ++)
             oknoAplikacji.Draw( sf::Shape::Circle( i->x, i->y, 10, sf::Color::Red ) );
        
        oknoAplikacji.Display();
    }
    return 0;
}

Podsumowanie

W tym rozdziale dowiedziałeś się jak powinien być zorganizowany kod w głównej pętli gry. Ponadto dowiedziałeś się w jaki sposób należy organizować kod gry od postaw tak, aby był on zarówno elastyczny jak i łatwy w utrzymaniu. 



Dodaj komentarz






Dodaj

© 2013-2024 PRV.pl
Strona została stworzona kreatorem stron w serwisie PRV.pl