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);
}
|