Gra 2D, część 11: Edytor poziomów cz. 1

15.03.2011 - Marcin Milewski
TrudnośćTrudność

Definicja klasy Editor

Odbywszy krótką wycieczkę, na której poznaliśmy klasę Brush, przechodzimy do omówienia serca naszego edytora poziomów, czyli implementacji klasy Editor.

Inicjalizacja każdego ze stanów jest bardzo podobna, żeby nie powiedzieć, że taka sama. Mamy tutaj inicjowanie graficznego interfejsu użytkownika oraz powiązanie sprite'ów z typami pól planszy. Oto początek pliku Editor.cpp:

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
// Plik editor/Editor.cpp
#include "../StdAfx.h"
#include "../Engine.h"
#include "../Text.h"
#include "../Utils.h"
#include "../Entity.h"
#include "Editor.h"
 
void Editor::Start() {
}
 
void Editor::Init() {
    m_level_view.StoreSprite(FT::PlatformTopLeft,
                             Sprite::GetByName("PlatformTopLeft"));
    m_level_view.StoreSprite(FT::PlatformLeft,           
                             Sprite::GetByName("PlatformLeft"));
    m_level_view.StoreSprite(FT::PlatformMid,            
                             Sprite::GetByName("PlatformMid"));
    m_level_view.StoreSprite(FT::PlatformTop,            
                             Sprite::GetByName("PlatformTop"));
    m_level_view.StoreSprite(FT::PlatformLeftTopRight,   
                             Sprite::GetByName("PlatformLeftTopRight"));
    m_level_view.StoreSprite(FT::PlatformLeftRight,      
                             Sprite::GetByName("PlatformLeftRight"));
    m_level_view.StoreSprite(FT::PlatformTopRight,       
                             Sprite::GetByName("PlatformTopRight"));
    m_level_view.StoreSprite(FT::PlatformRight,          
                             Sprite::GetByName("PlatformRight"));
 
    m_level_view.StoreSprite(FT::EndOfLevel,             
                             Sprite::GetByName("EndOfLevel"));
 
    m_level_view.StoreSprite(FT::NcPlatformTopLeft,      
                             Sprite::GetByName("NcPlatformTopLeft"));
    m_level_view.StoreSprite(FT::NcPlatformLeft,         
                             Sprite::GetByName("NcPlatformLeft"));
    m_level_view.StoreSprite(FT::NcPlatformMid,          
                             Sprite::GetByName("NcPlatformMid"));
    m_level_view.StoreSprite(FT::NcPlatformTop,          
                             Sprite::GetByName("NcPlatformTop"));
    m_level_view.StoreSprite(FT::NcPlatformLeftTopRight, 
                             Sprite::GetByName("NcPlatformLeftTopRight"));
    m_level_view.StoreSprite(FT::NcPlatformLeftRight,    
                             Sprite::GetByName("NcPlatformLeftRight"));
    m_level_view.StoreSprite(FT::NcPlatformTopRight,     
                             Sprite::GetByName("NcPlatformTopRight"));
    m_level_view.StoreSprite(FT::NcPlatformRight,        
                             Sprite::GetByName("NcPlatformRight"));
}
  

Wywołanie Sprite::GetByName(name) jest skróŧem od SpritePtr(new Sprite(engine.GetSpriteConfig()->Get(name)). Wyrażenie to jest bardziej intuicyjne oraz o wiele krótsze, a zatem łatwiejsze do zapamiętania. Aby uzyskać taki mechanizm wystarczy utworzyć statyczną metodę GetByName w klasie Sprite. Zobaczmy na kod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Plik: Sprite.h
(...)
public:
    static SpritePtr GetByName(const std::string& name);   // NOWE
    
private:
    SpriteConfigData m_data;
    size_t m_current_frame;
(...)
 
 
// Plik: Sprite.cpp
(...)
SpritePtr Sprite::GetByName(const std::string& name) {
    return SpritePtr(new Sprite(Engine::Get().GetSpriteConfig()->Get(name)));
}
  

Rysowanie

Następnym przystankiem jest metoda Editor::Draw. Ona również jest prawie taka sama jak Game::Draw. Kluczowe zadania zostały oddelegowane do pomocniczych (zatem także prywatnych) funkcji. Jedynym ciekawym elementem metody Draw są jej pierwsze wiersze. Odpowiadają one za przekazanie sterowania w całości do instancji klasy Game, jeżeli aktywna jest gra. W trybie edycji warunek jest niespełniony, więc sterowanie przechodzi dalej, czyli do edytora poziomów.

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
void Editor::Draw() {
    if (IsInGame()) {      // przekazanie starowania do m_game
        m_game->Draw();
        return;            // !
    }
    if (IsClearBeforeDraw()) {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glLoadIdentity();
    }
 
    glPushAttrib(GL_COLOR_BUFFER_BIT);
    {
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
 
        const double tile_width = Engine::Get().GetRenderer()->GetTileWidth();
        const double viewer_x   = -(m_viewer_offset_x * tile_width - 0.45);
 
        DrawEntitiesPlayerAndLevel(viewer_x);
        DrawBrushAndGui(viewer_x);
    }
    glPopAttrib();
 
    if (IsSwapAfterDraw()) {
        SDL_GL_SwapBuffers();
    }
}
  

Wróćmy do tematu metod pomocniczych. Pierwsza z nich, DrawEntitiesPlayerAndLevel, odpowiada za narysowanie jednostek, postaci gracza oraz poziomu. Czyli wszystkiego co jest zdefiniowane w plikach poziomu - tymi z rozszerzeniem lvl oraz ents. Druga natomiast, DrawBrushAndGui, rysuje pędzel obok kursora myszy oraz GUI. Omówieniem tego drugiego zajmiemy się za jakiś czas. Teraz przedstawmy implementację pierwszej ze wspomnianych metod pomocniczych.

Metoda Editor::DrawEntitiesPlayerAndLevel, jako argument otrzymuje przesunięcie obserwatora na osi odciętych. Przypomnijmy, że nasza kamera nie może poruszać się w pionie, więc wystarczy podać tylko przesunięcie w poziomie. Do narysowania encji zawartych w kontenerze m_entities wykorzystujemy algorytm std::for_each z biblioteki standardowej oraz mechanizm funkcji anonimowych z boosta. Oczywiście moglibyśmy użyć tutaj także zwykłej pętli for. Warto jednak poznawać nowe rozwiązania.
Następnie rysujemy sprite postaci gracza, o ile takowy został dodany do poziomu. Na zakończenie rysujemy poziom w sposób znany z metody Game::Draw. Cały kod znajduje się poniżej:

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
void Editor::DrawEntitiesPlayerAndLevel(double viewer_x) {
    const double tile_width  = Engine::Get().GetRenderer()->GetTileWidth();
    const double tile_height = Engine::Get().GetRenderer()->GetTileHeight();
 
    glPushMatrix();
    {
        glTranslated(viewer_x, 0, 0);
 
        // jednostki i gracz
        std::for_each(m_entities.begin(), m_entities.end(),
                      boost::bind(&Entity::Draw, _1));
        if (m_player_data.x >= 0) {
            const SpritePtr player_sprite = Sprite::GetByName("player_stop");
            const double player_x(m_player_data.x * tile_width),
                         player_y(m_player_data.y * tile_height);
            player_sprite->DrawCurrentFrame(player_x, player_y,
                                            tile_width, tile_height);
        } else {
            // nie ma danych gracza, to znaczy, że nie został ustawiony. Dlatego
            // nie rysujemy jego sprite'a. Przy przejściu do podglądu gry,
            // zostanie dodany na domyślnej pozycji.
        }
        
        // poziom
        double offset = m_viewer_offset_x;
        m_level_view.SetLevel(m_level, offset);
        m_level_view.Draw(offset);
    }
    glPopMatrix();
}
  

Druga z pomocniczych metod to DrawBrushAndGui. Przyjmuje taki sam argument co funkcja DrawEntitiesPlayerAndLevel. Zgodnie z nazwą realizuje ona dwa zadania. Pierwszym jest narysowanie aktywnego pędzla, którego pobranie sprowadza się do wywołania funkcji GetActiveBrush z instancji GUI. Pozycja, na której należy go narysować jest wyliczana na podstawie rozmiaru kafla oraz położenia kursora na ekranie.

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 Editor::DrawBrushAndGui(double viewer_x) {
    const double tile_width  = Engine::Get().GetRenderer()->GetTileWidth();
    const double tile_height = Engine::Get().GetRenderer()->GetTileHeight();
 
    glPushAttrib(GL_ENABLE_BIT);
    {
        // wyłącz test głębokości, żeby pędzel oraz gui były zawsze na wierzchu
        glDisable(GL_DEPTH_TEST);
 
        // na razie ustawiamy domyślny pędzel
        BrushPtr default_brush = Brush::New(Sprite::GetByName("PlatformTop"),
                                            FT::PlatformTop);
        SetBrush(default_brush);
 
        if (GetBrush()) {
           glPushMatrix();
           {
              glTranslated(viewer_x, 0, 0);
              Size size(tile_width, tile_height);
              Position position(m_pointer_x * tile_width,
                                m_pointer_y * tile_height);
 
              // podświetlenie
              Engine::Get().GetRenderer()->DrawQuad(position, position + size,
                                                    1,1,1,.4);
              // obiekt spod pędzla
              GetBrush()->GetSprite()->DrawCurrentFrame(position, size);
           }
           glPopMatrix();
        }
 
        m_gui->Draw();
    }
    glPopAttrib();
}
  

Aktualizacja

Po omówieniu mechanizmu rysowania przyszedł czas na metodę Editor::Update. Podobnie jak w przypadku, Editor::Draw, rozpoczynamy od sprawdzenia, czy aktywny jest tryb rozgrywki. Jeżeli tak, to sterowanie przekazujemy właśnie do niego. Zakończenie rozgrywki określamy na podstawie wyniku zwróconego z metody IsDone zmiennej m_game (czyli gry utworzonej na podstawie właśnie edytowanego poziomu). Mimo, że ta instancja sugeruje następny stan (poprzez metodę Game::NextAppState) w który powinna przejść aplikacja, to ignorujemy ten wynik przełączając program ponownie w tryb edycji.

Następnie następują sprawdzenia czy użytkownik chce zakończyć pracę z edytorem (klawisz ESC), bądź przewinąć planszę w lewą lub prawą stronę. Technicznym szczegółem jest upewnienie się, że poziom nie zostanie przewinięty poza lewą krawędź planszy. Dopuściłoby to próbę ustawienia pola na ujemnych współrzędnych, co prawdopodobnie zaowocowałoby błędnym działaniem edytora. Zobaczmy kod całej metody:

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
bool Editor::Update(double dt) {
    if (IsInGame()) {
        m_game->Update(dt);
        if (m_game->IsDone()) {
            SwitchToEditor();
        }
        return !IsDone();
    }
 
    if (m_keys_down[SDLK_ESCAPE]) {
        SetDone(true);
    } else if (m_keys_down[SDLK_LEFT]) {
        m_viewer_offset_x -= dt * 28.19;    // 28.19 to prędkość przesuwania
    } else if (m_keys_down[SDLK_RIGHT]) {
        m_viewer_offset_x += dt * 28.24;    // jw
    }
 
    // upewnij się że edytor nie zagląda poza lewą krawędź planszy. Znajdują się tam
    // ujemne wartości na osi odciętych, co może powodować błędy w obliczeniach.
    const double tiles_in_row = 1.0/Engine::Get().GetRenderer()->GetTileWidth();
    m_viewer_offset_x = std::max(m_viewer_offset_x, tiles_in_row/2-1);
 
    return !IsDone();
}
  

Przetwarzanie zdarzeń

Ostatnią funkcją, którą należy zaimplementować tworząc nowy stan (czyli klasę dziedziczącą po AppState, a taką właśnie jest Editor), jest ProcessEvents. Jej kod zaczyna się od sprawdzenia czy użytkownik chce przełączyć się między trybami rozgrywki oraz edycji -- służy do tego klawisz 0 (zero). Jeżeli okaże się, że aktualnie przetwarzanym trybem jest edytor, to przekazujemy sterowanie obiektowi m_game. Analogicznie gdy znajdujemy się w grze - wciśnięcie zera nakazuje powrót do edycji poziomu. Zobaczmy 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
void Editor::ProcessEvents(const SDL_Event& event) {
   if (IsDone()) {
       return;
   }
 
   if (event.type == SDL_KEYUP && event.key.keysym.sym==SDLK_0) {
       if (IsInGame()) {
          SwitchToEditor();
       } else {
          LevelPtr level(new Level(m_level, m_entities_to_create, m_player_data));
          level->ShrinkWidth();
          level->SaveEntitiesToFile("data/new.ents");
          level->SaveFieldsToFile("data/new.lvl");
          m_game.reset(new Game(level, PlayerPtr()));
          m_game->Init();
          m_game->Start();
          SwitchToGame();
       }
       return;
   }
   if (IsInGame()) {
       m_game->ProcessEvents(event);
       return;
   }
  

Zauważmy także, że już w momencie nadejścia zdarzenia dotyczącego myszy przekształcamy współrzędne z układu współrzędnych okna na współrzędne świata. Uniezależniamy się tym samym od sposobu, w jaki SDL dostarcza nam informacje o położeniu kursora. Tym przekształceniem zajmują się funkcje MapWindowCoordToWorldX oraz MapWindowCoordToWorldY. Natomiast o odpowiednią reakcję na zdarzenie dba metoda ActionAtCoords.

Układ współrzędnych, w którym SDL dostarcza współrzędne kursora (przestrzeń okna).
SO-szerokość okna, WO-wysokość okna.
Początek układu współrzędnych znajduje się w lewym górnym rogu.
Znormalizowany układ współrzędnych - położenie kursora niezależne od rozmiarów okna. Na przykład (0,5;0,5) oznacza środek okna (50% na każdej współrzędnej).
Układ współrzędnych świata. Zauważmy, że początek układu jest w lewym dolnym narożniku.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    const double window_width  = (Engine::Get().GetWindow()->GetWidth());
    const double window_height = (Engine::Get().GetWindow()->GetHeight());
 
    if (event.type == SDL_QUIT) {
        SetDone();
    } else if (event.type == SDL_KEYDOWN) {
        m_keys_down[event.key.keysym.sym] = true;
    } else if (event.type == SDL_KEYUP) {
        m_keys_down[event.key.keysym.sym] = false;
    } else if (event.type == SDL_MOUSEMOTION) {
        m_pointer_window_x =       event.motion.x / window_width;
        m_pointer_window_y = 1.0 - event.motion.y / window_height;
        m_pointer_x = MapWindowCoordToWorldX(m_pointer_window_x);
        m_pointer_y = MapWindowCoordToWorldY(m_pointer_window_y);
    } else if (event.type == SDL_MOUSEBUTTONDOWN) {
        m_pointer_window_x =       event.motion.x / window_width;
        m_pointer_window_y = 1.0 - event.motion.y / window_height;
        m_pointer_x = MapWindowCoordToWorldX(m_pointer_window_x);
        m_pointer_y = MapWindowCoordToWorldY(m_pointer_window_y);
        ActionAtCoords(m_pointer_x, m_pointer_y);
        }
    }
  

Pozostałe metody pomocnicze

Omówiliśmy już większość implementacji klasy Editor. Pozostało nam już tylko przedstawienie pomocniczych metody, które wystąpiły w poprzednich kodach. Z wyjątkiem ActionAtCoords, są one na tyle krótkie i jasne, że nie wymagają dodatkowego komentarza.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
double Editor::MapWindowCoordToWorldX(double x) const {
    const double tiles_in_row = 1.0/Engine::Get().GetRenderer()->GetTileWidth();
    double k = x*tiles_in_row + m_viewer_offset_x - tiles_in_row/2 + 1;
    return k;
}
 
double Editor::MapWindowCoordToWorldY(double y) const {
    const double th = Engine::Get().GetRenderer()->GetTileHeight();
    return y/th;
}
 
void Editor::ClearFieldAt(double x, double y) {
    SetFieldAt(x, y, FT::None);
}
 
void Editor::SetFieldAt(double x, double y, FT::FieldType ft) {
    m_level->EnsureWidth(static_cast<size_t>(x+1));
    m_level->SetField(static_cast<size_t>(x), static_cast<size_t>(TopDown(y)), ft);
}
 
FT::FieldType Editor::GetFieldAt(double x, double y) const {
    return m_level->Field(static_cast<size_t>(x), static_cast<size_t>(TopDown(y)));
}
  

Metoda ActionAtCoords zostanie zaimplementowana w kolejnym artykule. Na razie podajemy jedynie jej zalążek, czyli możliwie najprostsze (ale sensowne, to znaczy nie zgłaszamy wyjątku o ile nie ma takiej potrzeby) zachowanie.

1
2
3
4
void Editor::ActionAtCoords(double x, double y) {
    SetFieldAt(static_cast<size_t>(x), static_cast<size_t>(y), FT::PlatformTop);
}
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Plik: EntityFactory.cpp
(...)
class Entity;
class LevelEntityData;      // NOWE
typedef boost::shared_ptr<Entity> EntityPtr;
 
class EntityFactory {
public:
    explicit EntityFactory();
    EntityPtr CreateEntity(ET::EntityType type, double x, double y);
    EntityPtr CreateEntity(const std::string& name, double x, double y);
    EntityPtr CreateEntity(const LevelEntityData& entity_data);    // NOWE
};
(...)
 
// Plik: EntityFactory.h
(...)
EntityPtr EntityFactory::CreateEntity(const LevelEntityData& entity_data) {
    return CreateEntity(entity_data.name, entity_data.x, entity_data.y);
}
  
0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com