Gra 2D, część 7: Diabeł tkwi w szczegółach

05.02.2010 - Marcin Milewski, Łukasz Milewski
Trudność

Poziom zaliczony! Czas na następny

W naszej grze brakuje jeszcze jednego ważnego elementu - przechodzenia do następnego poziomu. Ta istotna funkcjonalność w połączeniu z nowymi poziomami pozwoli zapewnić sporo dobrej zabawy. A więc, zaczynamy!

Do reprezentacji bohatera dodajemy metody, które pozwolą sprawdzić, czy gracz zakończył aktualny poziom i wykonać w tym momencie pewne akcje. Odpowiednie pole (należy pamiętać o jego inicjalizacji w konstruktorze wartością false) oraz funkcje dodajemy do pliku Player.h:

1
2
3
4
5
6
7
    // akcja wywoływana, kiedy gracz zakończy poziom
    // (np. odegranie fanfarów, wyświetlenia napisu, ...
    void LevelCompleted();
 
    bool HasCompletedCurrentLevel() const {
        return m_is_level_completed;
    }

Implementacja metody LevelCompleted w chwili obecnej jest dosyć prosta, jednak w przyszłości będziemy chcieli umieścić w niej dodatkowe akcje, dlatego definiujemy ją w pliku Player.cpp:

1
2
3
void Player::LevelCompleted() {
    m_is_level_completed = true;
}

W naszej teksturze umieścimy specjalne pole, które będzie pozwalało na zakończenie aktualnego poziomu. Dodajemy je obok pola oznaczonego jako platforma_mid, więc konstruktor klasy SpriteConfig należy wzbogacić o wpis:

1
2
3
    Insert("end_of_level",
           SpriteConfigData(DL::Foreground, 1, 1,
                            32, 2 * 32, 32, 32, true));

Dodajemy nowe pole do mapy, więc należy także nadać mu numer, który będzie go identyfikował w pliku z poziomem. Robimy to w pliku Types.h. Oto odpowiedni kawałek kodu po modyfikacji:

1
2
3
4
5
6
7
8
9
10
11
12
namespace FT {
    enum FieldType {
        None = 0,
        PlatformLeftEnd = 1,
        PlatformMidPart = 2,
        PlatformRightEnd = 3,
 
        EndOfLevel = 4,
 
        COUNT
    };
}

Aby powiązać nazwę z odpowiednim numerem, dodajemy stosowny wpis podczas ładowania gry - metoda Game::Init:

1
2
3
  m_level_view.StoreSprite(FT::EndOfLevel,
         SpritePtr(new Sprite(
                engine.SpriteConfig()->Get("end_of_level"))));

W celu sprawdzenia, czy poziom został przez gracza zakończony, wywołujemy na nim metodę HasCompletedCurrentLevel. Następnie możemy wykonać pewne akcje - będzie się to odbywać w metodzie Game::Update:

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
    // uaktualnij obiekt reprezentującego gracza
    m_player->Update(dt, m_level);
 
    // czy gracz zakończył aktualny poziom
    if (m_player->HasCompletedCurrentLevel()) {
        // akcja, jeżeli gracz zakończył poziom
        // np. załadowanie następnego :)
 
    } else if (m_player->GetLifesCount() < 1) {
        // jakieś akcje
    }
  

Pozostaje nam tylko sprawdzić czy gracz wskoczył w odpowiedni kafel na mapie. Kolizje bohatera z poziomem są sprawdzane w metodzie Player::CheckCollisionsWithLevel. Na jej początku dodajemy odpowiedni warunek:

1
2
3
4
5
6
7
void Player::CheckCollisionsWithLevel(double dt, 
                                      LevelPtr level) {
    size_t x_tile, y_tile;
    GetCurrentTile(&x_tile, &y_tile);
    if (level->Field(x_tile, y_tile - 1) == FT::EndOfLevel) {
        LevelCompleted();
    }

Zapamiętujmemy rozdzielczość okna

Zauważmy, że tworząc możliwość wpisania własnego wyniku (klasa ScoreSubmit) wykorzystaliśmy założenie, że rozdzielczość okna to 600x400.

1
2
    double x = event.motion.x / 600.0;
    double y = 1.0 - event.motion.y / 400.0;

Oczywiście wykorzysytwanie w kodzie takich założeń nie jest dobrym pomysłem. Gdyby nowy programista dołączył do projektu i chciał dodać możliwość zmiany rozdzielczości w menu głównym, to zapewne przeoczyłby te dwie linijki w ScoreSubmit.

Ten problem można rozwiązać na wiele sposobów. Najprościej jest zapamiętać szerokość i wysokość obrazu. Dlatego tworzymy nowy plik Window.h, w którym będze taka klasa:

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
#ifndef __WINDOW_H__
#define __WINDOW_H__
 
class Window {
public:
    explicit Window() {
        m_width = 0;
        m_height = 0;
    }
 
    size_t GetWidth() const {
        return m_width;
    }
 
    size_t GetHeight() const {
        return m_height;
    }
 
    void SetSize(size_t width, size_t height) {
        m_width = width;
        m_height = height;
    }
 
private:
    size_t m_width;
    size_t m_height;
};
 
typedef boost::shared_ptr<Window> WindowPtr;
 
#endif /* __WINDOW_H__ */
  

Do klasy Engine dodajemy pole m_window typu WindowPtr oraz metodę Window, która zwraca zapamiętane okno. W metodzie Load dopisujemy

1
    m_window.reset(new Window::Window()); 

Teraz zmieniamy metodę App::Resize (plik App.cpp) tak, aby zamiast

1
2
    m_window_width = width;
    m_window_height = height;
było
1
    Engine::Get().Window()->SetSize(width, height);

Czyli zapamietujemmy szerokość i wysokość okna w klasie Enigne w polu m_window, zamiast bezpośrednio w klasie App. Dzięki temu możemy odwołać się do rozdzielczości okna w dowolnym miejscu programu.

5
Twoja ocena: Brak Ocena: 5 (2 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com