Gra 2D, część 2: Wyżej, dalej, szybciej - poruszanie postacią

05.01.2010 - Marcin Milewski
Trudność

Poprzedni artykuł - tworzenie okna i sprite'y Następny artykuł - przewijana mapa

W tym artykule zajmiemy się stworzeniem postaci sterowanej przez gracza. Stworzymy w tym celu model, według którego bohater będzie się poruszał, aby ostatecznie tchnąć w niego życie i oddać kontrolę nad nim graczowi.

Artykuł ten bazuje na kodzie stworzonym w poprzedniej części cyklu, o tworzeniu okna i animacji na sprite'ach. Lekko uaktualnioną wersję źródeł można znaleźć tutaj. Oto efekt, który uzyskamy podczas tego artykułu:

Efekt końcowy

Na początek kilka założeń dotyczących animacji oraz poruszania postacią w grze:

  1. Gracz może stać, iść, biec lub skakać - sterowanie będzie się odbywać za pomocą klawiatury
  2. Można sterować ruchem bohatera również podczas skoku
  3. Animacja będzie składać się z trzech stanów: stoi, idzie w lewo, idzie w prawo. Pozostałe stany pozostawiamy jako proste ćwiczenie do wykonania.

Przyjmujemy, że przez pojęcie "gracza" rozumiemy osobę siedzącą przed komputerem, a postać sterowana w grze to "bohater" czy właśnie "postać".

Animacja bohatera

Jak zaznaczyliśmy we wstępie, nasz bohater będzie mógł znajdować się w jednym z trzech stanów: idzie w prawo, idzie w lewo lub stoi. Każdemu z tych stanów odpowiada jeden wiersz na teksturze. Oto interesujący nas kawałek tej tekstury:

Animacja postaci

Aktualne wymiary obrazka to 1024x1024, więc trzeba dokonać stosownego uaktualnienia w pliku Renderer.cpp w metodzie DrawSprite:

1
2
3
4
    // plik Renderer.cpp, metoda DrawSprite
    const double texture_w = 1024.0;
    const double texture_h = 1024.0;
  

Musimy także poinformować program, gdzie na teksturze znajdują się poszczególne animacje. Przypomnijmy, że definiuje się je w pliku SpriteConfig.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SpriteConfig::SpriteConfig() {
    Insert("player_right", 
           SpriteConfigData(
               DL::Player, 5, 0.2, 0, 4*32, 32, 32, true));
 
    Insert("player_left",  
           SpriteConfigData(
               DL::Player, 5, 0.2, 0, 5*32, 32, 32, true));
 
    Insert("player_stop",  
           SpriteConfigData(
               DL::Player, 1, 0.2, 0, 6*32, 32, 32, true));
}
  

Natomiast klasę Engine należy poinformować, że nasza tekstura jest teraz w pliku tex.bmp (przy okazji zmieńmy nazwę zmiennej na bardziej odpowiednią). Ponadto włączamy test przezroczystości nakazując, aby piksele, które mają współrzędną A (od Alpha) większą niż 0.5 były rysowane jako przezroczyste. Pole m_player klasy App będzie reprezentowało postać sterowaną przez gracza. Metoda SetSprites przekaże postaci informację o sprite'ach, które ma wykorzystywać do odpowiednich animacji.

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    // inicjalizacja OpenGL
    glClearColor(0, 0, 0, 0);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
    glEnable(GL_TEXTURE_2D);
    // niewyświetlanie przezroczystych fragmentów sprite'a
    glEnable(GL_ALPHA_TEST); 
    glAlphaFunc(GL_GEQUAL, 0.1);
 
    const std::string atlas_filename = "data/tex.bmp";
    Engine& engine = Engine::Get();
    engine.Load();
    engine.Renderer()->LoadTexture(atlas_filename);
    m_player->SetSprites(
            SpritePtr(new Sprite(engine.SpriteConfig()->
                                   Get("player_left"))),
            SpritePtr(new Sprite(engine.SpriteConfig()->
                                   Get("player_right"))),
            SpritePtr(new Sprite(engine.SpriteConfig()->
                                   Get("player_stop"))) );
 
    // pętla główna (...)
  

Wskaźnik na obiekt bohatera dodajemy do klasy App. Należy dołączyć także odpowiedni nagłówek oraz zainicjować wskaźnik bohatera. Deklaracja klasy App wygląda teraz następująco:

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <SDL/SDL.h>
#include "Player.h"
 
class App {
public:
    explicit App(size_t win_width, 
                 size_t win_height, 
                 bool fullscreen_mode) :
        m_window_width(win_width),
        m_window_height(win_height),
        m_fullscreen(fullscreen_mode) {
        // 10-szerokość levelu (na razie 10 kafli, 1 ekran)
        m_player.reset(new Player(1, 1, 10));   
    }
 
    void Run();
 
private:
    void Draw();
    void Update(double dt);
    void Resize(size_t width, size_t height);
    void ProcessEvents();
 
private:
    size_t m_window_width;   // szerokość okna
    size_t m_window_height;  // wysokość okna
    bool m_fullscreen;       // tryb pełnoekranowy
    bool is_done;
    SDL_Surface* m_screen;
 
    PlayerPtr m_player;
};
  

Obiekt postaci należy oczywiście aktualizować oraz rysować. Zrobimy to miejscach, gdzie wcześniej aktualizowane oraz rysowane były przykładowe liczniki, czyli w metodach odpowiednio Update oraz Draw.

1
2
3
4
5
6
7
8
9
10
11
12
void App::Update(double dt) {
    m_player->Update(dt);
}
 
void App::Draw() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    // narysuj postać gracza
    m_player->Draw();
    SDL_GL_SwapBuffers();
}
  

Szczegóły dotyczące klasy Player omówimy za chwilę. Teraz potrzebujemy jeszcze struktury, pozwalającej nam określić stan, w którym może znajdować się bohater. Zgodnie z założeniem podajemy tylko 3 stany. W pozostałych stanach będziemy odtwarzali animację któregoś ze zdefiniowanych stanów. Do opisania stanu definiujemy odpowiednie wyliczenie w pliku Types.h:

1
2
3
4
5
6
7
8
namespace PS {
    enum PlayerState {
        Stand,
        GoLeft,
        GoRight
    };
}
  

Trochę ruchu nikomu jeszcze nie zaszkodziło ;)

Nasz bohater będzie poruszał się w dwuwymiarowym układzie współrzędnych. Mamy zatem ruch w poziomie oraz w pionie. Do modelowanie tego pierwszego użyjemy ruchu jednostajnego, czyli prędkość poruszania się będzie stała w dla konkretnego stanu postaci. Wyróżniamy 3 takie stany:

  • Stanie - $ v_s=0 $
  • Chodzenie - $ v_c=const $
  • Bieganie - $ v_b=const=v_c \cdot k $, gdzie $ k $ jest pewną stałą większą od 1

Pojawia się pytanie: czy taki model wiernie oddaje rzeczywistość? Odpowiedź brzmi: nie, ale to nie szkodzi. Zauważmy, że czas, w którym ciało przyspiesza (np. moment przejścia od stanu spoczynku do chodu) jest bardzo krótki. Jego pominięcie zostanie zauważone (o ile w ogóle) dopiero po dokładnym przyjrzeniu się ruchu postaci. Przypomnijmy zależności między wartościami, które występują w ruchu jednostajnym:

$  a = 0  $
$  v = const  $
$  Δs = v \cdot t  $

Inaczej wygląda rzecz, kiedy przyjrzymy się ruchowi w pionie. Gdyby postać poruszała się wyłącznie góra-dół (czyli prędkość pozioma byłaby równa 0), moglibyśmy rozważyć zaniedbanie przyspieszenia w pionie. Zwróćmy także uwagę, że tor, po którym poruszałby się bohater, byłby stosunkowo krótki, więc pominięcie przyspieszenia nie byłoby widoczne na pierwszy rzut oka. Jednak, jak wspomnieliśmy, ruch naszego bohatera odbywa się w dwóch wymiarach. Kiedy postać wykona skok po skosie, to oczekiwanym torem jego ruchu będzie parabola. Zwróćmy uwagę, że ruch ten będzie trwał wystarczająco długo, aby zauważyć tor, po którym porusza się sprite. Dlatego ruch w pionie opiszemy jako połączenie ruchu jednostajnie opóźnionego (wznoszenie się postaci) z ruchem jednostajnie przyspieszonym (opadanie).

Przypomnijmy wzory wykorzystywane do opisania ruchu jednostajnie przyspieszonego/opóźnionego:

$  a = const  $
$  Δv = a \cdot Δt  $
$  Δs = v \cdot Δt  $

Na poniższej animacji widzimy dwa sposoby poruszania się postaci. Sprite po lewej jest aktualizowany z wykorzystaniem ruchu jednostajnie przyspieszonego/opóźnionego, a sprite po prawej - ruchu jednostajnego prostoliniowego:


4.666665
Twoja ocena: Brak Ocena: 4.7 (3 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com