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

05.01.2010 - Marcin Milewski
Trudność

Implementacja poruszania postacią

Po krótkiej powtórce z teorii, nadszedł czas na zajęcie praktyczne, czyli implementację poruszania postacią. Na postaci sterowanej przez gracza będziemy chcieli wykonywać kilka czynności:

  • Zaktualizować jej położenie
  • Narysować ją
  • Zmienić jej stan: idź w lewo, idź w prawo, zatrzymaj się, skocz, spada

Zanim zobaczymy jak te czynności przekładają się na konkretny kod, określmy sposób w jaki postać gracza będzie zmieniała swój stan. Na początku każdej klatki bohater będzie spadał w dół, gdyż jest pod wpływem grawitacji. Następnie będziemy na niego nakładali kolejne ograniczenia (nie może iść w lewo, nie może skakać, ...). W ten prosty sposób przedstawimy wszystkie żądane zachowania. Spójrzmy teraz na deklarację klasy Player w pliku Player.h:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#ifndef __PLAYER_H__
#define __PLAYER_H__
 
#include <boost/shared_ptr.hpp> 
#include "Sprite.h"
#include "Types.h"
 
class Player {
public:
    explicit Player(double x, double y, size_t level_width);
 
    void SetSprites(SpritePtr left, SpritePtr right, SpritePtr stop);
    void Update(double dt);
    void Draw() const;
 
    double GetX() const { return m_x; }
    double GetY() const { return m_y; }
    double GetDefaultYVelocity()     const { 
        return DefaultYVelocity; 
    }
    double GetDefaultYAcceleration() const { 
        return DefaultYAcceleration; 
    }
 
    void Run()               { m_running_factor = 2.0; }
    void StopRunning()       { m_running_factor = 1.0; }
    void GoLeft()            { m_vx -= 4.0; m_state=PS::GoLeft; }
    void GoRight()           { m_vx += 4.0; m_state=PS::GoRight;}
    void StopLeft()          { m_vx += 4.0; m_state=PS::Stand; }
    void StopRight()         { m_vx -= 4.0; m_state=PS::Stand; }
    void ForbidGoingLeft()   { m_can_go_left = false; }
    void ForbidGoingRight()  { m_can_go_right = false; }
    void Fall()              { m_vy = 0.0; m_is_on_ground = false;}
    void Jump(double y_velocity = DefaultYVelocity);
    void AllowToJump()       { m_jump_allowed = true; }
    void ForbidToJump()      { m_jump_allowed = false; }
    void SetDefaultMoving()  { m_is_on_ground = false;
                               m_can_go_right = m_can_go_left=true;
    }
    void PlayerOnGround()    { m_vy = 0;
                               m_is_on_ground=m_jump_allowed=true;
    }
 
    double GetNextXPosition(double dt) const { 
        return m_x + m_vx * dt * m_running_factor; 
    }
    double GetNextYPosition(double dt) const { 
        return m_y + (m_vy + m_ay * dt) * dt; 
    }
 
private:
    enum { DefaultYVelocity = 20, DefaultYAcceleration=-60 };
 
    PS::PlayerState m_state;  // stan, w którym znajduje się postać
    SpritePtr m_left;         // animacja - postać idzie w lewo
    SpritePtr m_right;        // animacja - postać idzie w prawo
    SpritePtr m_stop;         // animacja - postać stoi
    double m_x;               // położenie postaci na osi odciętych
    double m_y;               // położenie postaci na osi rzędnych
    double m_vx;              // prędkość na osi OX
    double m_vy;              // prędkość gracza w pionie
    double m_ay;              // przyspieszenie gracza w pionie
    double m_running_factor;  // współczynnik biegania. 
                              //         >1.0 => biegnie, 
                              //         <1.0 => spowolnienie
    bool m_jump_allowed;      // czy gracz może skakać 
                              // (np. jest na podłożu)
    size_t m_level_width;     // szerokość poziomu (w kaflach)
    bool m_is_on_ground;      // czy postać jest na podłożu
    bool m_can_go_left;       // czy postać może iść w lewo
    bool m_can_go_right;      // czy postać może iść w prawo
};
typedef boost::shared_ptr<Player> PlayerPtr;
 
#endif
  

Nie powinno być wątpliwości co do większości metod oraz pól. Warto jednak zaznaczyć, że klasa Player powinna zapewniać metody pozwalające nie tylko pobierać jej stan, ale także go zmieniać. Na przykład metody dotyczące skoku przydadzą się przy reagowaniu na kolizje z obiektami na planszy (np. żółw, na którego można skoczyć i wybić się w górę). Wartości stałych DefaultYVelocity oraz DefaultYAcceleration, jak również wartości określające prędkość i przyspieszenie postaci zostały dobrane drogą eksperymentu. W każdej chwili można zmienić je na inne, jeżeli uznamy, że bohater porusza się za wolno lub skacze zbyt nisko.

Wśród mnogości pól i metod zauważamy kilka funkcji odnoszących się jakby do tej samej funkcjonalności: np. StopLeft oraz ForbidGoingLeft. Pierwsza z nich mówi graczowi, że ruch w lewo definitywnie się zakończył (np. gracz nie trzyma już dłużej odpowiedniego klawisza), druga natomiast zabrania w danym momencie poruszania się w lewo - powodem może być np. ściana, lub przesuwanie jakiegoś elementu.

Mając zdefiniowane funkcje do manipulacji bohaterem, możemy dodać kod, który będzie z nich korzystał zapewniając tym samym interakcję między graczem z postacią w wirtualnym świecie. Kod obsługujący wejście znajduje się w metodzie App::ProcessEvents w pliku App.cpp. Zapewnia on reakcję bohatera na naciśnięcie odpowiednich klawiszy. Obecnie wygląda on 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void App::ProcessEvents() {
    if (is_done)
        return;
 
    // przyjrzyj zdarzenia
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_VIDEORESIZE) {
            Resize(event.resize.w, event.resize.h);
        } else if (event.type == SDL_QUIT) {
            is_done = true;
            break;
        // naciśnięcie ESC -> wyjście z gry
        } else if (event.type == SDL_KEYDOWN 
                   && event.key.keysym.sym == SDLK_ESCAPE) {
            is_done = true;
            break;
        // klawisz d - bieganie
        } else if (event.type == SDL_KEYDOWN 
                   && event.key.keysym.sym == SDLK_d) {
            m_player->Run();
        } else if (event.type == SDL_KEYUP 
                   && event.key.keysym.sym == SDLK_d) {
            m_player->StopRunning();
        // strzałka w górę - skok
        } else if (event.type == SDL_KEYDOWN 
                   && event.key.keysym.sym == SDLK_UP) {
            m_player->Jump();
        // strzałka w lewo - idź w lewo
        } else if (event.type == SDL_KEYDOWN 
                   && event.key.keysym.sym == SDLK_LEFT) {
            m_player->GoLeft();
        // strzałka w prawo - idź w prawo
        } else if (event.type == SDL_KEYDOWN 
                   && event.key.keysym.sym == SDLK_RIGHT) {
            m_player->GoRight();
        // zwolnienie strzałki w lewo 
        // - zatrzymaj poruszanie w lewą stronę
        } else if (event.type == SDL_KEYUP 
                   && event.key.keysym.sym == SDLK_LEFT) {
            m_player->StopLeft();
        // zwolnienie strzałki w prawo 
        // - zatrzymaj poruszanie w prawą stronę
        } else if (event.type == SDL_KEYUP 
                   && event.key.keysym.sym == SDLK_RIGHT) {
            m_player->StopRight();
        }
    }
}
  

Przyjrzyjmy się implementacji klasy Player. W konstruktorze ustawiamy początkowy stan postaci: stoi na pozycji (x, y), porusza się z prędkością [0, 0] oraz przyspieszeniem [0, DefaultYAcceleration]. Może skakać (zakładamy, że stoi na podłożu), porusza się z normalną prędkością (nie biegnie - m_running_factor=1.0). Dodatkowo przekazujemy informacje na temat szerokość poziomu. Te informacje będą potrzebne, aby określić początek i koniec planszy oraz aby narysować postać na ekranie. W ciele konstruktora ustawiamy domyślny stan ruchu - prawdopodobnie nadpisując ustawione na liście inicjalizacji pola.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <cmath>
#include "Player.h"
#include "Engine.h"
 
Player::Player(double x, double y, size_t level_width) 
    : m_state(PS::Stand),
      m_x(x),
      m_y(y),
      m_vx(0.0),
      m_vy(0),
      m_ay(DefaultYAcceleration),
      m_running_factor(1.0),
      m_jump_allowed(true),
      m_level_width(level_width),
      m_is_on_ground(true),
      m_can_go_left(true),
      m_can_go_right(true) {
    SetDefaultMoving();
}
  

Metodę SetSprite widzieliśmy już wcześniej. Ustawia ona sprite'y dla poszczególnych animacji bohatera - idzie w lewo, idzie w prawo, stoi.

1
2
3
4
5
6
7
8
void Player::SetSprites(SpritePtr left, 
                        SpritePtr right, 
                        SpritePtr stop) {
    m_left = left;
    m_right = right;
    m_stop = stop;
}
  

W metodzie Jump zmieniamy pola obiektu tak, aby możliwy był skok. Nadajemy wartości prędkości oraz przyspieszeniu w pionie, a także dbamy o to, aby postać nie mogła skoczyć, jeżeli nie jest to możliwe. W Draw przeliczamy współrzędne postaci w wirtualnym świecie na współrzędne na ekranie, a następnie rysujemy odpowiedniego sprite'a.

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
34
35
36
void Player::Jump(double y_velocity) {
    // wykonaj skok o ile jest taka możliwość
    if (m_jump_allowed) {
        m_jump_allowed = false;
        m_is_on_ground = false;
        m_vy = y_velocity;            // początkowa prędkość
        m_ay = DefaultYAcceleration;  // przyspieszenie
    }
}
 
void Player::Draw() const {
    const double tile_width = 
                Engine::Get().Renderer()->GetTileWidth();
    const double tile_height = 
                Engine::Get().Renderer()->GetTileHeight();
 
    // wylicz pozycję gracza na ekranie
    const double pos_x = m_x * tile_width;
    const double pos_y = m_y * tile_height;
 
    switch (m_state) {
    case PS::Stand:
        m_stop->DrawCurrentFrame(pos_x, pos_y, 
                                 tile_width, tile_height);
        break;
    case PS::GoLeft:
        m_left->DrawCurrentFrame(pos_x, pos_y, 
                                 tile_width, tile_height);
        break;
    case PS::GoRight:
        m_right->DrawCurrentFrame(pos_x, pos_y, 
                                  tile_width, tile_height);
        break;
    }
}
  

Na koniec najciekawsza z metod klasy Player, metoda Update. W pierwszych liniach modyfikujemy prędkość oraz położenie zgodnie ze wzorami podanymi w części teoretycznej tego artykułu. Przy aktualizacji położenia bohatera na osi OX (metoda GetNextXPosition) zauważmy występujący tam współczynnik m_running_factor odpowiadający za szybsze poruszanie się postaci (bieganie). Ustalamy również granice na planszy, w których postać powinna się zmieścić. Nie chcemy, aby wyszła ona za początek lub koniec planszy oraz aby spadła poniżej poziomu y=1. To ostatnie wymaganie jest tymczasowe, gdyż w przyszłości bohater będzie poruszał się po podłożu, z którego będzie mógł spaść. W końcowej części, na postawie prędkości w poziomie, ustalamy w jakim stanie znajduje się postać, po czym aktualizujemy animację.

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void Player::Update(double dt) {
    // wylicz nową prędkość oraz połóżenie na osi OY
    if (!m_is_on_ground) {
        m_y = GetNextYPosition(dt);
        m_vy += m_ay * dt;
    }
 
    // jeżeli poniżej pierwszego kafla, to nie spadaj niżej.
    // Na razie ustalamy poziom na y=1, aby postać nie uciekała
    //  za ekran
    if (m_y < 1) {
        m_y = 1;
        PlayerOnGround();
    }
 
    // wylicz pozycję gracza w poziomie (oś OX).
    m_x = GetNextXPosition(dt);
 
    // nie można wyjść poza mapę
    if (m_x < 0) {
        m_x = 0; // nie można wyjść za początek mapy
    } else if (m_x > m_level_width - 1) {
        // nie można wyjść za prawą krawędź mapy
        m_x = m_level_width - 1; 
    }
 
    // ustal stan ruchu gracza na podstawie prędkości
    if (fabs(m_vx) < 0.01) {
        m_state = PS::Stand;
        m_vx = 0;
    } else if (m_vx > 0.0) {
        m_state = PS::GoRight;
    } else {
        m_state = PS::GoLeft;
    }
 
    // uaktualnij animację
    switch (m_state) {
    case PS::Stand:
        m_stop->Update(dt);
        break;
    case PS::GoLeft:
        m_left->Update(dt);
        break;
    case PS::GoRight:
        m_right->Update(dt);
        break;
    }
}
  

Przedstawiona metoda Update i krótkie pomocnicze metody dbają o to, aby bohater poruszał się w taki sposób jak chce tego gracz. Warto poświęcić chwilę czasu na dokładne zrozumienie zmiany stanu postaci, gdyż do tego zagadnienia wrócimy w artykule o wykrywaniu kolizji. Zobaczymy w nim m.in. jak stworzony przez nasz model sprawdza się w akcji.

Warto dodać informację podczas uruchamiania aplikacji o sposobie sterowania postacią. Oto uaktualniona zawartość pliku main.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "App.h"
#include <iostream>
 
int main(int argc, char *argv[]) {
    std::cout 
      << "      strzałki lewo/prawo - poruszanie się postacią\n"
      << "         strzałka do góry - skok\n"
      << " przytrzymanie klawisza d - bieganie"
      << std::endl;
 
    App app(600, 400, false);
    app.Run();
    return 0;
}
  

A oto efekt końcowy:

Efekt końcowy

Kompletny kod źródłowy do tego artykułu można znaleźć pod tym adresem. Dostępna jest też wersja z projektem w Dev-C++ pod Windows.

Masz pytanie, uwagę? Zauważyłeś błąd? Powiedz o tym na forum.

Zadania dla dociekliwych

  1. Rozszerz klasę Player o stany skacze, spada oraz biegnie. Taka zmiana wiąże się z dodaniem nowych animacji oraz zapewnienia odpowiedniego przejścia między stanami postaci. Czy jest możliwe aby dodać nowy stan bez dodawania nowej animacji? Przyjrzyj się stanowi biegnie.
  2. Ogranicz (częściowo lub całkowicie) możliwość sterowania postacią gdy znajduje się w powietrzu.

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

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

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com