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:
struct RObiekt
{
double x;
double y;
RObiekt( double f_x = 0.0, double f_y = 0.0 )
: x( f_x )
, y( f_y )
{ }
};
Do tego wszystkiego dorzucimy jeszcze wektor przechowujący obiekty:
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ą:
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ć:
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:
oknoAplikacji.Clear();
for( VObiektyT::const_iterator i = vObiekty.begin(); i != vObiekty.end(); ++i )
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:
RObiekt & obiekt = vObiekty.at( 0 );
for(; obiekt.x <= 100; ++obiekt.x )
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:
for(; obiekt.y <= 300; ++obiekt.y )
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:
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:
while( oknoAplikacji.IsOpened() )
{
oknoAplikacji.Clear();
for( VObiektyT::const_iterator i = vObiekty.begin(); i != vObiekty.end(); ++i )
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:
void PrzesunObiekt( RObiekt & obiekt, double idzDoX, double idzDoY, double fPredkosc = 1.0 )
{
if( obiekt.x < idzDoX )
{
obiekt.x += fPredkosc;
if( obiekt.x > idzDoX )
obiekt.x = idzDoX;
} else
if( obiekt.x > idzDoX )
{
obiekt.x -= fPredkosc;
if( obiekt.x < idzDoX )
obiekt.x = idzDoX;
}
if( obiekt.y < idzDoY )
{
obiekt.y += fPredkosc;
if( obiekt.y > idzDoY )
obiekt.y = idzDoY;
} else
if( obiekt.y > idzDoY )
{
obiekt.y -= fPredkosc;
if( obiekt.y < idzDoY )
obiekt.y = 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:
#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;
}
}
};
int main()
{
sf::RenderWindow oknoAplikacji( sf::VideoMode( 800, 600, 32 ), "Wytwarzanie Gier 2D, C++ | http://cpp0x.pl" );
oknoAplikacji.UseVerticalSync( true );
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();
}
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(); ++i )
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.
Zadanie domowe
Rozbuduj kod przedstawiony w niniejszym rozdziale tak aby dowolnie wybrany przez Ciebie obiekt przemieszczał się do punktu w którym kliknie się myszą na ekranie. Obsługa myszy jest omówiona w kursie SFML (
Obsługa zdarzeń - klawiatura, mysz i inne), natomiast cała reszta wymaga od Ciebie odrobiny kreatywności. Powodzenia! :)