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

15.03.2011 - Marcin Milewski
TrudnośćTrudność

Czas na klasę Editor

Przygotowaliśmy grunt pod najważniejszą w tym artykule klasę, więc przechodzimy do sedna sprawy. Za chwilę utworzymy główną klasę odpowiedzialną za przejęcie sterowania od klasy App. Zauważmy, że podobną rzecz robiliśmy już kilkakrotnie -- przy okazji dodawania takich klas jak Game czy HallOfFame. Mówiąc najprościej, tworzymy klasę będącą kolejnym stanem w naszej grze, czyli dziedziczącą po AppState. Pliki związane z kodem edytora wygodnie będzie umieścić w podkatalogu editor. Unikniemy w ten sposób nadmiaru plików w głównym katalogu projektu.

Klasa Editor jest podobna do klasy Game. Ta druga czuwała nad przebiegiem rozgrywki wyświetlając w odpowiednim momencie ekran wyboru poziomu, Hall of Fame, a przez większość czasu jednostki, planszę czy punkty gracza. Klasa, którą za chwilę zobaczymy, będzie miała podobne właściwości. Jej kluczowym, z punktu widzenia gracza, zadaniem jest reagowanie na ruch kursora na ekranie, kliknięcie przycisku myszy czy klawisza na klawiaturze. Poza tym do jej zadań należą także (a może przede wszystkim) rysowanie i aktualizacja jednostek oraz podłoża, a także przełączenie między trybami edycji i rozgrywki.

Deklaracja klasy Editor

Jak widać, klasa Editor ma sporo zadań. Stąd też, jej implementacja zajmuje więcej niż kilka krótkich wierszy kodu. Zobaczmy, co wchodzi w skład tej klasy. Początek wygląda standardowo. Kilka include'ów i deklaracje takich podstawowych metod jak Init, Draw, Update czy ProcessEvents. W konstruktorze znajduje się inicjalizacja pól, do definicji których zaraz dojdziemy.

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
// Plik: editor/Editor.h
#ifndef __EDITOR_H_INCLUDED__
#define __EDITOR_H_INCLUDED__
 
#include "../AppState.h"
#include "../Game.h"
#include "../Level.h"
#include "../SpriteGrid.h"
#include "Brush.h"
 
class Editor;
typedef boost::shared_ptr<Editor> EditorPtr;
 
class Editor : public AppState, public boost::enable_shared_from_this<Editor> {
public:
    explicit Editor(LevelPtr level)
        : m_next_app_state(),
          m_in_game(false),
          m_level(level),
          m_viewer_offset_x(0),                  // Update zadba o poprawną wartość
          m_pointer_x(0), m_pointer_y(0),
          m_pointer_window_x(0), m_pointer_window_y(0),
          m_keys_down(SDLK_LAST, false)          // Wszystkie klawisze puszczone
        {
        SetDone(false);
    }
 
    void Start();
    void Init();
    void Draw();
    bool Update(double dt);
    void ProcessEvents(const SDL_Event& event);
 
    AppStatePtr NextAppState() const {
        return m_next_app_state;
    }
 
    std::string GetLevelName() const {
        if (m_level) {
            return m_level->GetName();
        }
        return "unknown";
    }
  

Jednym z zadań omawianej klasy jest rysowanie. Odbywa się ono w metodzie Draw, która jest czysto wirtualna w klasie bazowej AppState. Skoro jednak istnieje kilka różnych rzeczy do narysowania, to stworzymy dla nich osobne metody -- DrawEntitiesPlayerAndLevel oraz DrawBrushAndGui. Ponieważ są to funkcje pomocnicze, to deklarujemy ja jako prywatne:

1
2
3
4
private:
    void DrawEntitiesPlayerAndLevel(double viewer_x);
    void DrawBrushAndGui(double viewer_x);
  

Innym zadaniem jest obsłużenie kliknięcia myszą na ekranie. Skąd jednak wiedzieć co użytkownik miał na myśli? Odkrycie tego jest zadaniem metody ActionAtCoords. Na razie zajmujemy się tylko deklaracją funkcji w klasie Editor, ale kiedyś przyjdzie taki moment, że będziemy musieli skorzystać z naszych metod. Dlatego już teraz warto zadać sobie pytanie: w jakim układzie współrzędnych są przekazane parametry? Umówmy się, że wewnątrz edytora (w sensie implementacji, a nie tego, co widzi użytkownik) posługujemy się współrzędnymi w przestrzeni świata, czyli niezależnymi od rozmiaru okna czy położenia jednostek na ekranie. Przekształceniem koordynatów kursora na współrzędne świata zajmą się metody MapWindowCoordToWorldX oraz MapWindowCoordToWorldY.
Kiedy już dowiemy się jaką akcję użytkownik miał na myśli i okaże się, że dotyczy ona manipulacji lub dostarczenia informacji na temat pól na planszy, z pomocą przyjdą nam funkcje ClearFieldAt, SetFieldAt, GetFieldAt.

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).
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
    // sprawdza, jaką akcję należy wykonać po kliknięciu na podanych współrzędnych
    void ActionAtCoords(double x, double y);
 
    // Czyści pole pod wskazanymi współrzędnymi (przestrzeń świata)
    // Ustawia pole na wskazany typ (przestrzeń świata)
    // Zwraca typ pola we wskazanych współrzędnych (przestrzeń świata)
    // y -- bottom-up
    void ClearFieldAt(double x, double y);
    void SetFieldAt(double x, double y, FT::FieldType ft);
    FT::FieldType GetFieldAt(double x, double y) const;
 
    // Przekształca współrzędne kursora (przestrzeń okna)
    // na współrzędne świata (przestrzeń świata)
    double MapWindowCoordToWorldX(double x) const;
    double MapWindowCoordToWorldY(double y) const;
 
    // TopDown odbija współrzędną y w pionie. Niektóre elementy kodu umiejscawiają
    // y=0 na górze świata, a inne na dole. Edytor zawsze działa z osią OY
    // skierowaną w górę, więc jeżeli pewna funkcja foo wymaga odbitego argumentu,
    // to należy wywołać ją jako foo(TopDown(some_y_coord)), by zaznaczyć,
    // że pamiętaliśmy o odbiciu
    double TopDown(double y)  const {
       return Engine::Get().GetRenderer()->GetVerticalTilesOnScreenCount() - 1 - y;
    }
  

Podczas spisywania wymagań wspomnieliśmy o możliwości zgrabnego przełączania się między trybem edycji a rozgrywką. Niezbędne okażą się funkcje przełączające nas w każdy z tych stanów oraz umożliwiające sprawdzenie, w którym z nich się aktualnie znajdujemy:

1
2
3
4
5
6
    // metody do przełączania między trybem gry a trybem edytora
    void SwitchToGame()     { m_in_game = true; }
    void SwitchToEditor()   { m_in_game = false; m_game.reset(); }
    bool IsInGame() const   { return m_in_game; }
    bool IsInEditor() const { return !m_in_game; }
  

Poza głównymi trybami (edycji i rozgrywki) wyróżnimy jeszcze 3 (pod)tryby dotyczące tworzenia poziomu. Są one związane z akcją, którą użytkownik zamierza wykonać wybrawszy jedną z trzech odmian pędzla:

  • Stawianie pól planszy (field mode).
  • Stawianie jednostek (entity mode) -- pędzel, który dodaje do poziomu przeciwników czy bonusy.
  • Zadania specjalne -- zadanie nie pasujące do żadnej z dwóch powyższych grup. Np. ustalanie pozycji początkowej graczy, usuwanie elementów z poziomu.

1
2
3
4
    // pędzel do rysowania
    BrushPtr GetBrush()          const { return m_brush; }
    Editor* SetBrush(BrushPtr brush)   { m_brush = brush; return this; }
  

To już wszystkie metody klasy Editor. Pozostało tylko dodanie pól. Nazwy zmiennych oraz umieszczone obok nich komentarze powinny wyjaśniać ich przeznaczenie.

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
private:
   AppStatePtr m_next_app_state;
   bool m_in_game;                     // czy włączona jest gra?
   GamePtr m_game;                     // instancja gry
 
   BrushPtr m_brush;                   // pędzel do rysowania
 
   LevelPtr m_level;                   // dane poziomu (podobnie jak w klasie Game)
   SpriteGrid m_level_view;            // widoczna część poziomu (j.w.)
   double m_viewer_offset_x;           // przesunięcie środka planszy (prz. świata)
 
   double m_pointer_x;                 // położenie kursora (przestrzeń świata)
   double m_pointer_y;                 // bottom-up
   double m_pointer_window_x;          // położenie kursora (przestrzeń okna)
   double m_pointer_window_y;          // bottom-up
 
   LevelEntityData m_player_data;                   // informacje o graczu
   std::vector<EntityPtr> m_entities;               // jednostki (są tylko rysowane)
   std::list<LevelEntityData> m_entities_to_create; // opisy jednostek do stworzenia
 
   std::vector<bool> m_keys_down;      // naciśnięte (i niepuszczone) klawisze
};
 
#endif
  
0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com