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

15.03.2011 - Marcin Milewski
TrudnośćTrudność

Editor -- implementacja

Przed chwilą przedstawiliśmy aktualizacje w deklaracji klasy Editor, dlatego teraz zajmiemy się jej definicją. Pierwsze wiersze kodu, które należy dopisać, to wywoływanie metod typu Init, Update, Draw,... w odpowiednich miejscach. Funkcję Start obiektu m_gui wywołujemy w metodzie Editor::Start, Init -- na końcu Editor::Init, a Update -- przed powrotem z funkcji Editor::Update. Poniżej poszczególne fragmenty kodu:

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
void Editor::Start() {
    m_gui->Start();
}
 
void Editor::Init() {
    // dodawanie sprite'ów
    // (...)
 
    m_gui->Init();
}
 
bool Editor::Update(double dt) {
    // (...)
 
    // 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);
 
    // aktualizacja GUI
    m_gui->Update(dt);       // NOWE!
 
    return !IsDone();
}
  

Miejsce rysowania GUI przygotowaliśmy w metodzie Editor::DrawBrushAndGui, więc teraz wystarczy podmienić odpowiedni TODO na wywołanie m_gui->Draw(). Rysowanie jest jednak troszkę bardziej skomplikowane. Trzeba zadbać jeszcze o przypisanie aktywnego pędzla do pola m_brush oraz obsłużenie metody ShouldSnapToGrid. Poprzednią wersję Editor::DrawBrushAndGui, czyli:

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
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);
        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();
        }
 
        // TODO: tutaj narysować GUI
    }
    glPopAttrib();
}
  

Zamieniamy na:

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
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);
       SetBrush(m_gui->GetActiveBrush());
       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);
               // niektóre pola są wyrównywane do siatki,
               // więc trzeba przyciąć współrzędne
               if (ShouldSnapToGrid()) {
                   position = Position(
                                static_cast<int>(m_pointer_x) * tile_width,
                                static_cast<int>(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();
       }
       if (IsGuiVisible()) {                  // TU PODMIENIAMY
          m_gui->Draw();
       }
   }
   glPopAttrib();
}
  

I znów ta metoda ShouldSnapToGrid.... Może więc już pora, aby przedstawić jej implementację? Zwraca prawdę w dwóch przypadkach: gdy rysujemy podłoże oraz w kiedy używamy gumki. W przyszłości można dodać tutaj więcej warunków. Oto kod:

1
2
3
4
5
6
7
8
9
10
bool Editor::ShouldSnapToGrid() const {
    if (InPaintingFieldMode())
        return true;
    if (InPaintingSpecialMode()
        && GetBrush()->IsSpecial()
        && GetBrush()->GetSpecialType()==Brush::ST::Eraser)
        return true;
    return false;
}
      

Zajrzyjmy do ostatniej metody pomocniczej -- ActionAtCoords. Na podstawie argumentów, czyli współrzędnych kursora (we współrzędnych świata, stąd typ double), oraz wybranego przez użytkownika pędzla, decyduje ona o akcji do wykonania. Wiele przypadków jest łatwych w implementacji. Ciekawe jest dodawanie nowej encji. Zauważmy, że jednostka tworzona jest pod dwiema postaciami. Pierwsza jest jedynie opisem, na podstawie którego jednostka będzie zapisana w pliku *.ents. Opis ten jest zapamiętywany w polu m_entities_to_create. Druga reprezentacja to normalna jednostka w grze (zauważmy wykorzystanie fabryki), co skutkuje dodaniem jej do kontenera m_entities. Oto kompletna implementacje omawianej 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
26
27
28
29
30
31
32
33
34
35
void Editor::ActionAtCoords(double x, double y) {
    BrushPtr b = m_gui->GetActiveBrush();
    if (b) {
        if (InPaintingFieldMode()) {
            SetFieldAt(static_cast<size_t>(x), static_cast<size_t>(y),
                       b->GetFieldType());
        } else if (InPaintingEntityMode()) {
            const ET::EntityType entity_type = b->GetEntityType();
            assert(entity_type!=ET::UNKNOWN);
            assert(entity_type!=ET::COUNT);
            const std::string name = EntityTypeAsString(entity_type);
            const LevelEntityData entity_data(name, x, y);
            m_entities_to_create.push_back(entity_data);
            EntityFactory factory;
            m_entities.push_back(factory.CreateEntity(entity_data));
            // std::cout << "New entity: " << name << " "
            //           << x << " " << y << std::endl;
        } else if (InPaintingSpecialMode()) {
            const Brush::ST::SpecialType special_type = b->GetSpecialType();
            if (special_type == Brush::ST::Player) {
                m_player_data = LevelEntityData("player", x, y);
            } else if (special_type == Brush::ST::Eraser) {
                ClearFieldAt(static_cast<size_t>(x), static_cast<size_t>(y));
            } else {
                std::cerr << "Niezdefiniowana akcja w trybie specjalnym"
                          << std::endl;
            }
        }
        else {
            std::cerr << "Nie odnaleziono trybu rysowania" << std::endl;
            assert(false && "Nie odnaleziono trybu rysowania");
        }
    }
}
  
RTTI - informacja o typie
W języku C++ można przekształcić typ na ciąg znaków wykorzystując mechanizm RTTI. Odbywa się to np. tak:
1
2
3
4
5
#include <typeinfo>
class Gracz { /*...*/ };
/*...*/
Gracz bohater;
std::cout << typeid(bohater).name();
Na ekranie powinno pojawić się coś jak "Gracz". Niestety nie jesteśmy dokładnie tego stwierdzić, gdyż standard języka nie prezyzuje tego zachowania. Każdy kompilator może zwrócić inny wynik.

Niestety, w C++ nie da się elementu wyliczenia (ang. enum) w postaci ciągu znaków (a tworzenie osobnej klasy dla każego typu jednostki jest bardzo niepraktyczne). Taka operacja jest nam potrzebna, aby zapisać do pliku rodzaj jednostki. Dlatego musimy dodać prostą funkcję tłumaczącą typ jednostki na instancję klasy std::string.

Na pierwszy rzut oka, takie rozwiązanie powoduje, że po dodaniu nowego typu jednostki musimy dodać odpowiedni wpis do EntityTypeAsString. Faktycznie jest to pewien kłopot. Jednak, gdy ktoś spróbuje dodać taką jednostkę do planszy (za pośrednictwem edytora), to program przerwie pracę z komunikatem, że taki typ jednostki jest nieobsługiwany.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Plik: Types.h
std::string EntityTypeAsString(ET::EntityType et);
 
// Plik: Types.cpp
#include "StdAfx.h"
#include "Types.h"
 
std::string EntityTypeAsString(ET::EntityType et) {
    switch (et) {
    case ET::PlayerBullet : return "player_bullet";
    case ET::TwinShot     : return "twinshot_upgrade";
    case ET::Mush         : return "mush";
 
    case ET::UNKNOWN      : return "unknown";
    case ET::COUNT        : assert(false && "count nie jest typem jednostki");
                            break;
    }
    assert(false && "nieobsłużony typ jednostki");
}
  

Ostatnia aktualizacja kodu klasy Editor to dostosowanie metody Editor::ProcessEvents do nowych warunków, czyli pojawienia się GUI. Jeżeli jest ono aktywne (widoczne), to należy przekazywać od niego komunikaty wejścia (zdarzenia klawiatury, myszy,...). Jeżeli zostaną tam obsłużone, czyli np. wywołaniem_gui->OnKeyDown zwróci True, to metoda Editor::ProcessEvents już nie powinna przetwarzać tego komunikatu. Poprawienie implementacji jest więc kwestią dopisania kilku warunków. Porównajmy starą wersję:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void Editor::ProcessEvents(const SDL_Event& event) {
    // (...)
 
    double window_width = Engine::Get().GetWindow()->GetWidth();
    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);
    }
  

Z nową:

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
void Editor::ProcessEvents(const SDL_Event& event) {
    // (...)
 
    double window_width = Engine::Get().GetWindow()->GetWidth();
    double window_height = Engine::Get().GetWindow()->GetHeight();
    if (event.type == SDL_QUIT) {
        SetDone();
    } else if (event.type == SDL_KEYDOWN) {
        if (IsGuiHidden() || m_gui->OnKeyDown(event.key.keysym.sym)==false) {
            m_keys_down[event.key.keysym.sym] = true;
        }
    } else if (event.type == SDL_KEYUP) {
        if (event.key.keysym.sym==SDLK_1) {
            ToggleGui();
        }else if (IsGuiHidden() || m_gui->OnKeyUp(event.key.keysym.sym)==false){
            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;
        if (IsGuiVisible()
           && m_gui->OnMouseMove(m_pointer_window_x, m_pointer_window_y)) {
            // przesuń kursor poza ekran (można to zrobić ładniej)
            m_pointer_x = m_pointer_y = 1000;
        } else {
            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;
        if (IsGuiVisible()
           && m_gui->OnMouseDown(event.button.button,
                                 m_pointer_window_x,
                                 m_pointer_window_y)) {
        } else {
            m_pointer_x = MapWindowCoordToWorldX(m_pointer_window_x);
            m_pointer_y = MapWindowCoordToWorldY(m_pointer_window_y);
            ActionAtCoords(m_pointer_x, m_pointer_y);
        }
    }
  
0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com