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
|