Nadszedł czas, aby dodać pędzel o specjalnej mocy. Nasz multipędzel będzie mógł narysować nawet kilkadziesiąt kafelków jednocześnie! Dzięki temu, podłoże w naszych poziomach będzie powstawać w zawrotnym tempie.
Rozwiniemy nasz edytor o możliwość wypełniania wielu pól planszy jednym ruchem myszy. Podczas tworzenia poziomu, gracz będzie mógł wybrać sposób w jaki ma zostać wypełniony zaznaczony obszar. W momencie definiowania zaznaczenia wyświetlany będzie podgląd wyniku działania programu. A co jeśli użytkownik się pomyli? Jak wycofać zmianę kilkudziesięciu pól? Zadbamy też o ten aspekt. Dzięki prostemu refaktoringowi nasz edytor będzie mógł cofnąć każdą wprowadzoną zmianę. Zabierzmy się więc do pracy.
Poprzedni artykuł - Edytor poziomów cz. 2 [2]
Kod źródłowy, który będziemy modyfikować w tym artykule można znaleźć tutaj [4].
Dotychczas, w edytorze mogliśmy wybrać typ pędzla ze względu na rodzaj malowanego obiektu. Dokładniej, mieliśmy trzy możliwości: malowanie podłoża, malowanie jednostek oraz malowanie specjalne. Teraz chcemy mieć możliwość rysowania nie jednego, lecz wielu elementów jednym pociągnięciem pędzla. Wykorzystamy do tego celu dziedziczenie.
Pisząc klasę Brush nie braliśmy pod uwagę, że może ona być w przyszłości klasą bazową. Dlatego teraz będziemy musieli wykonać kilka (drobnych) zmian, żeby ją do tego przystosować.
Pierwszą modyfikacją jest oczywiście dodanie wirtualnego destruktora. Do tej pory nie było go wcale*, więc jako jego definicję podajemy pusty blok. Poniżej znajduje się kawałek kodu, który należy dodać do deklaracji klasy Brush. (*)Tak naprawdę to był, gdyż jeżeli nie zdefiniujemy go w sposób jawny, to kompilator wygeneruje go za nas. Podobnie dzieje się w przypadku konstruktora bezargumentowego czy kopiującego.
1 2 3 | // Plik: editor/Brush.h virtual ~Brush() {} |
Druga modyfikacja to dodanie możliwości zmiany implementacji rysowania pędzla. Obecnie pędzel jedynie przechowuje sprite'a, którym należy go narysować. Teraz chcemy zorganizować to nieco inaczej. Dodajemy metodę Draw do klasy Brush, aby pędzel mógł narysować się sam. Dzięki temu, z punktu widzenia klasy korzystającej z pędzla, sposób rysowania będzie zawsze taki sam – czyli będzie sprowadzał się do wywołania metody Draw.
Polecenie rysujące pędzel przenosimy z metody Editor::DrawBrushAndGui do Brush::DrawIcon, które zostanie wywołane przez metodę Brush::Draw. Tym samym, wszystko działa jak dawniej, a my możemy przejść do kolejnej modyfikacji czyli do dodania szkicu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // Plik: editor/Brush.h public: virtual void Draw(Position scr_position, Size scr_size) const { DrawIcon(scr_position, scr_size); } protected: // Rysuje mały kafelek obok kursora. void DrawIcon(Position scr_position, Size scr_size) const { GetSprite()->DrawCurrentFrame(scr_position, scr_size); } // Plik Editor.cpp, metoda Editor::DrawBrushAndGui // Zamiast: Engine::Get().GetRenderer()->DrawQuad(position, position+size, 1,1,1,.4); GetBrush()->GetSprite()->DrawCurrentFrame(position, size); // Teraz jest: Engine::Get().GetRenderer()->DrawQuad(position, position+size, 1,1,1,.4); GetBrush()->Draw(position, size); |
Trzecia modyfikacja to dodanie funkcji rysującej szkic, czyli pogląd działania pędzla. Do tej pory ikona
rysowana obok pędzla dobrze obrazowała efekt jego działania (czyli dodanie pojedynczego elementu do
planszy). Nowy pędzel będzie mógł postawić wiele elementów jednocześnie. Ponieważ jego działanie może być
dość skomplikowane, to przyda się możliwość dostarczenia użytkownikowi podglądu. Do klasy Brush
dodajemy wirtualną metodę DrawSketch. Każda klasa pochodna będzie mogła dostarczyć właściwą dla
niej implementację. W
Brush jej ciało zostawiamy puste.
Taki sposób implementacji (tj. gdy klasa pochodna może
zmienić część algorytmu) nosi nazwę wzorca metody szablonowej (ang. template method pattern).
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Plik: editor/Brush.h protected: // Metoda do przesłonięcia w klasach pochodnych. // Rysowanie szkicu/podglądu. virtual void DrawSketch(Position scr_position, Size scr_size) const { } // Wywołanie dopisujemy do metody Draw: virtual void Draw(Position scr_position, Size scr_size) const { DrawSketch(scr_position, scr_size); DrawIcon(scr_position, scr_size); } |
Ostatnią zmianą, którą wprowadzimy jest dodanie informacji o tym, że konkretna instancja pędzla jest tak
naprawdę multipędzlem. Klasa korzystająca z pędzla powinna sprawdzić czy jest on specjalnego typu i w razie
potrzeby wykonać rzutowanie na klasę multipędzla.
Dopisujemy stałą Multi do wyliczenia
Brush::ST::SpecialType.
1 2 3 4 5 6 7 | // Plik: editor/Brush.h class Brush { public: struct ST { enum SpecialType { UNKNOWN, Player, Eraser, Multi }; }; |
Na początku pliku trzeba jeszcze dołączyć potrzebne pliki nagłówkowe. Początek pliku Brush.h wygląda teraz następująco:
1 2 3 4 5 6 7 8 9 | // Plik: editor/Brush.h #ifndef __BRUSH_H_INCLUDED__ #define __BRUSH_H_INCLUDED__ #include "../StdAfx.h" #include "../Sprite.h" class Brush; |
Mamy już zdefiniowaną klasę bazową dla pędzla. Dlatego teraz zajmiemy się utworzeniem klasy pochodnej, MultiBrush. Będzie ona reprezentowała multipędzel. Jej zadaniem będzie przechowywanie danych przekazanych od użytkownika za pomocą urządzenia wejściowego (w naszym przypadku myszy). Na ich podstawie na ekranie będzie rysowany podgląd efektu malowania pędzlem. Kiedy użytkownik zwolni klawisz myszy, instancja klasy MultiBrush zwróci polecenie, które należy wykonać aby na ekranie pojawił się efekt działania multipędzla. Do przetwarzania polecenia wrócimy za chwilę. Teraz przyjrzyjmy się deklaracji i definicji klasy MultiBrush.
Deklarację klasy MultiBrush zaczynamy od wskazania klasy bazowej oraz zdefiniowania typu inteligentnego wskaźnika. Dodajemy także statyczną metodę New, które ułatwi tworzenie wskaźników do obiektów typu MultiBrush.
1 2 3 4 5 6 7 8 9 10 11 12 | // Plik: editor/Brush.h class MultiBrush; typedef boost::shared_ptr<MultiBrush> MultiBrushPtr; class MultiBrush : public Brush { public: explicit MultiBrush(SpritePtr sprite); static MultiBrushPtr New(SpritePtr sprite) { return MultiBrushPtr(new MultiBrush(sprite)); } |
Dostarczanie informacji do klasy MultiBrush będzie odbywało się w 3 etapach.
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 | // Plik: editor/Brush.h public: void StartAt(const Position& pos) { m_start = m_end = pos; m_is_active = true; } void StartAt(double x, double y) { StartAt(Position(x, y)); } void MoveTo(const Position& pos) { if (IsActive()) { m_end = pos; } } void MoveTo(double x, double y) { MoveTo(Position(x, y)); } void FinishAt(const Position& pos) { if (IsActive()) { m_end = pos; m_is_active = false; } } void FinishAt(double x, double y) { FinishAt(Position(x, y)); } private: Position GetStart() const { return m_start; } Position GetEnd() const { return m_end; } bool IsActive() const { return m_is_active; } Position m_start, m_end; bool m_is_active; }; |
Należy jeszcze nadać wartości początkowe polom klasy. Jak zaznaczyliśmy wcześniej, przed wywołaniem StartAt, pędzel jest na pewno nieaktywny. Dla pozycji m_start oraz m_end wpisujemy wartości -1, gdyż współrzędne świata gry należą zawsze do pierwszej ćwiartki kartezjańskiego układu współrzędnych. Więc jeśli podczas odczytu punktów zaznaczenia trafimy na liczbę ujemną, to możemy być pewni, że malowanie nie zostało rozpoczęte. Oto zmieniony kod konstruktora:
1 2 3 4 5 6 7 8 9 10 | // Plik: editor/Brush.h class MultiBrush : public Brush { public: explicit MultiBrush(SpritePtr sprite) : Brush(sprite, Brush::ST::Multi), m_start(-1, -1), m_end(-1, -1), m_is_active(false) { } |
Informacja o aktywności pędzla jest istotna, gdyż pozwala stwierdzić czy szkic powinien być rysowany na ekranie. Miejscem odpowiedzialnym za wyświetlanie podglądu jest metoda wirtualna DrawSketch. W klasie bazowej jej implementacja jest pusta, dlatego do deklaracji MultiBrush dopisujemy:
1 2 3 4 5 | // Plik: editor/Brush.h public: // (...) virtual void DrawSketch(Position scr_position, Size scr_size) const; |
Definicję podajemy w pliku źródłowym editor/MultiBrush.cpp. W skrócie mówiąc, narysowanie szkicu polega na wyświetleniu prostokąta wypełnionego kafelkiem PlatformMid między punktami m_start oraz m_end. Dodatkowo malujemy żółty, przezroczysty prostokąt, aby odróżnić aktualnie rysowany kawałek od elementów już dodanych do poziomu.
Podczas rysowania wykorzystujemy pomocniczą klasę TileGridHelper. Jej zadaniem jest dostarczenie kilku użytecznych funkcji:
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 | // Plik: editor/MultiBrush.cpp #include "../TileGridHelper.h" #include "Brush.h" void MultiBrush::DrawSketch(Position scr_position, Size scr_size) const { if (!IsActive()) { return; } TileGridHelper tgh(GetStart(), GetEnd()); tgh.SnapToGrid(); tgh.SortCoordsOfBox(); unsigned tiles_hor = tgh.TilesHorizontally(), tiles_ver = tgh.TilesVertically(); if (tiles_hor > 50 || tiles_ver > 50) { std::cerr << "[MultiBrush::DrawSketch] " << "Uzyskano niepokojąco duże wartości:" << "\n\ttiles_hor: " << tiles_hor << "\n\ttiles_ver: " << tiles_ver << "\n"; } const double tile_width = Engine::Get().GetRenderer()->GetTileWidth(); const double tile_height = Engine::Get().GetRenderer()->GetTileHeight(); Position begWorld = tgh.Beg().scale(tile_width, tile_height), endWorld = tgh.End().scale(tile_width, tile_height); for (unsigned i = 0; i < tiles_ver; i++) { for (unsigned j = 0; j < tiles_hor; j++) { Sprite::GetByName("PlatformMid")->DrawCurrentFrame( begWorld + Position(j * tile_width, i * tile_height), Size(tile_width, tile_height)); } } Engine::Get().GetRenderer()->DrawQuad(begWorld, endWorld, 1,1,0, .7); } |
W powyższym kodzie przemyciliśmy jedną drobną zmianę w klasie Vector (czyli także w Position). Chodzi o możliwość skalowania wektora, czyli mnożenia każdej współrzędnej przez pewien współczynnik. Do struktury Vector dopisujemy:
1 2 3 | // Plik: Vector2.h Vector2 scale(double sx, double sy) const { return Vector2(x*sx, y*sy); } |
Przed chwilą napisaliśmy kod, który korzysta z klasy TileGridHelper. Poniżej prezentujemy deklarację tej klasy:
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 | // Plik: TileGridHelper.h #ifndef TILEGRIDHELPER_H #define TILEGRIDHELPER_H #include "StdAfx.h" #include "BasicMathTypes.h" class TileGridHelper { public: explicit TileGridHelper(const Position& beg, const Position& end); // Przyciąga podany punkt do najbliższego punktu siatki TileGridHelper& SnapToGrid(); // Zmienia współrzędne podanych punktów tak, że w beg znajdują się // minimalne (lewy dolny narożnik), a w end maksymalne (prawy górny narożnik). TileGridHelper& SortCoordsOfBox(); // Wymagają posortowanych współrzędnych unsigned TilesHorizontally() const; unsigned TilesVertically() const; Position Beg() const { return m_beg; } Position End() const { return m_end; } private: Position m_beg, m_end; }; #endif |
Następnie przyjrzyjmy się jej definicji. Każda z metod ma dobrze określone, konkretne zadanie. Dzięki temu ich implementacja jest prosta oraz (jeżeli ktoś ma ochotę) łatwa do testowania. Przekazane w konstruktorze punkty zapamiętujemy w polach klasy:
1 2 3 4 5 6 7 | // Plik: TileGridHelper.cpp #include "TileGridHelper.h" TileGridHelper::TileGridHelper(const Position& beg, const Position& end) : m_beg(beg), m_end(end) { } |
Metoda SnapToGrid zaokrągla każdą ze współrzędnych obu punktów. Dlaczego tak jest dobrze? Dlatego, że punkty przekazane w konstruktorze pochodzą z przestrzeni świata. To znaczy, że punkt (3,14; 8,0) oznacza ósmy (tak naprawdę dziewiąty, bo pierwszy kafelek jest definiowany przez punkt (0,0)) kafelek w pionie oraz nieco ponad trzeci w poziomie. Zaokrąglając wartość każdej współrzędnej otrzymujemy punkt oznaczający początek (czyli lewy dolny narożnik) najbliższego kafelka. Ponadto metoda zwraca referencję do instancji, na rzecz której została wywołana. Pozwala to na łańcuchowe wywoływanie metod (ang. method chaining).
1 2 3 4 5 6 7 | // Plik: TileGridHelper.cpp TileGridHelper& TileGridHelper::SnapToGrid() { m_beg = Position(round(m_beg.X()), round(m_beg.Y())); m_end = Position(round(m_end.X()), round(m_end.Y())); return *this; } |
Kolejną, równie prostą w implementacji funkcją jest SortCoordsOfBox. Jak podawaliśmy wcześniej, dla dowolnych dwóch punktów określających prostokąt, modyfikuje je ona tak, aby m_beg oraz m_end oznaczały odpowiednio lewy dolny oraz prawy górny narożnik prostokąta. Podobnie jak SnapToGrid, ta metoda również zwraca referencję do TileGridHelper.
1 2 3 4 5 6 7 8 9 10 | // Plik: TileGridHelper.cpp TileGridHelper& TileGridHelper::SortCoordsOfBox() { Position orga = m_beg, orgb = m_end; m_beg[0] = std::min(orga.X(), orgb.X()); m_end[0] = std::max(orga.X(), orgb.X()); m_beg[1] = std::min(orga.Y(), orgb.Y()); m_end[1] = std::max(orga.Y(), orgb.Y()); return *this; } |
Do zaimplementowania pozostały metody zwracające rozmiar prostokąta wyrażony w kafelkach w poziomie oraz pionie. Ich implementacja polega na odjęciu odpowiednich współrzędnych przechowywanych punktów. Na wszelki wypadek wykonujemy jeszcze zaokrąglenie. Oto omawiane metody:
1 2 3 4 5 6 7 8 | unsigned TileGridHelper::TilesHorizontally() const { return static_cast<unsigned>(round(m_end[0] - m_beg[0])); } unsigned TileGridHelper::TilesVertically() const { return static_cast<unsigned>(round(m_end[1] - m_beg[1])); } |
Definiując podklasę MultiBrush dopisaliśmy metody StartAt, MoveTo, FinishAt. Do tej pory nigdzie ich nie wywoływaliśmy. Łatwo się domyślić, że właściwym ku temu miejscem jest obsługa zdarzeń przychodzących do klasy Editor. Dodamy zatem bliźniacze dla ActionAtCoords, prywatne metody MoveToCoords oraz ReleaseAtCoords, odpowiadające za reakcję na przesuniecie kursora oraz zwolnienia klawisza myszy. Odpowiedni kod w pliku Editor.h wygląda następująco:
1 2 3 4 5 6 7 | // Plik: editor/Editor.h Editor* SetBrush(BrushPtr brush) { m_brush = brush; return this; } void ReleaseAtCoords(double x, double y); // Wsp.świata void MoveToCoords(double x, double y); // Wsp.świata void ActionAtCoords(double x, double y); // Wsp.świata |
Definicja obu nowych metod jest bardzo podobna. Polega na upewnieniu się czy używanym pędzlem jest teraz multipędzel, a następnie wywołaniu odpowiedniej jego metody. Zanim jednak odpowiednia funkcja zostanie wywołana, musimy rzutować pędzel na jego podklasę, czyli MultiBrush. Ponieważ korzystamy z inteligentnych wskaźników z biblioteki boost, nie możemy użyć zwykłego dynamic_cast z biblioteki standardowej. Na szczęście razem z klasą boost::shared_ptr otrzymujemy funkcję do rzutowania wskaźników -- boost::dynamic_pointer_cast.
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 | // Plik editor/Editor.cpp #include <boost/pointer_cast.hpp> // (...) void Editor::ReleaseAtCoords(double x, double y) { BrushPtr brush = m_gui->GetActiveBrush(); if (! brush) { return; } if (brush->GetSpecialType() == Brush::ST::Multi) { MultiBrushPtr multibrush = boost::dynamic_pointer_cast<MultiBrush>(brush); multibrush->FinishAt(x, y); } } void Editor::MoveToCoords(double x, double y) { BrushPtr brush = m_gui->GetActiveBrush(); if (! brush) { return; } if (brush->GetSpecialType() == Brush::ST::Multi) { MultiBrushPtr multibrush = boost::dynamic_pointer_cast<MultiBrush>(brush); multibrush->MoveTo(x, y); } } |
Wywołania powyższych funkcji umieszczamy w metodzie Editor::ProcessEvents w obsłudze odpowiednich zdarzeń (ruch myszą, zwolnienie klawisza myszy). Nowe wiersze zostały oznaczone gwiazdkami:
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 | // Plik: editor/Editor.cpp } else if (event.type == SDL_MOUSEMOTION) { m_pointer_window_x = event.motion.x / win_width; m_pointer_window_y = 1.0 - event.motion.y / win_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); * MoveToCoords(m_pointer_x, m_pointer_y); } } else if (event.type == SDL_MOUSEBUTTONDOWN) { m_pointer_window_x = event.motion.x / win_width; m_pointer_window_y = 1.0 - event.motion.y / win_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); } * } else if (event.type == SDL_MOUSEBUTTONUP) { * m_pointer_x = MapWindowCoordToWorldX(m_pointer_window_x); * m_pointer_y = MapWindowCoordToWorldY(m_pointer_window_y); * ReleaseAtCoords(m_pointer_x, m_pointer_y); } |
Wywołanie funkcji StartAt powinno pojawić się w momencie naciśnięcia klawisza myszy, czyli w metodzie Editor::ActionAtCoords. Jest w niej sprawdzany rodzaj pędzla, więc musimy dodać tam obsługę nowego typu Brush::ST::Multi. Zamieniamy więc:
1 2 3 4 5 6 | } 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; } |
na
1 2 3 4 5 6 7 8 9 | } else if (special_type == Brush::ST::Eraser) { ClearFieldAt(static_cast<size_t>(x), static_cast<size_t>(y)); } else if (special_type == Brush::ST::Multi) { MultiBrushPtr mb = boost::dynamic_pointer_cast<MultiBrush>(brush); mb->StartAt(x, y); } else { std::cerr << "Niezdefiniowana akcja w trybie specjalnym" << std::endl; } |
Mechanizm obsługi nowego pędzla jest już prawie ukończony. Zanim przejdziemy dalej,
dodamy kod do wyświetlania w GUI przycisku skojarzonego z nowym pędzlem.
Wykorzystamy istniejącą klasę
BrushButton, gdyż nasz przycisk będzie skojarzony z pędzlem. Dokładniej rzecz ujmując, będzie
przechowywał wskaźnik (oczywiście inteligentny) na podklasę Brush, czyli MultiBrush. Dla
przycisku nie ma to jednak większego znaczenia, więc nie musimy tworzyć nowej klasy reprezentującej
przycisk.
Aby przycisk pojawił się na ekranie, należy utworzyć instancję BrushButton i dodać ją do kontenera m_buttons klasy EditorGui. Rejestrowanie kontrolek odbywa się w metodzie EditorGui::Init. Dopisujemy nowe makra, które będą pomocne w dodawaniu przycisków z multipędzlami. Wprawne oko natychmiast zauważy, że definicje ADD_BUTTON oraz ADD_MULTIBUTTON różnią się makrem wykorzystanym do tworzenia przycisku. Są to odpowiednio BUTTON i MULTIBUTTON. Te z kolei różnią się sposobem tworzenia pędzla. Pierwsza makrodefinicja tworzy pędzel typu Brush, a druga -- MultiBrush.
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 | // Plik editor/EditorGui.cpp, metoda EditorGui::Init #define BUTTON(name,pos,size,type) \ BrushButtonPtr(new BrushButton(Sprite::GetByName(name), \ pos, size, \ Brush::New(Sprite::GetByName(name), type))) #define ADD_BUTTON(name,pos,size,type) \ m_buttons.push_back(BUTTON(name,pos,size,type)) #define MULTIBUTTON(name,pos,size,type) \ BrushButtonPtr(new BrushButton(Sprite::GetByName(name), \ pos, size, \ MultiBrush::New(Sprite::GetByName(name)))) #define ADD_MULTIBUTTON(name,pos,size,type) \ m_buttons.push_back(MULTIBUTTON(name,pos,size,type)) // nazwy sprite'ów zdefiniowane są w SpriteConfig::SpriteConfig ADD_BUTTON("gui_eraser", Position( 0, .0), default_size*2, Brush::ST::Eraser); ADD_MULTIBUTTON("PlatformMid", Position(.8, .1), default_size, Brush::ST::Multi); // (...) #undef ADD_MULTIBUTTON #undef MULTIBUTTON #undef ADD_BUTTON #undef BUTTON } // koniec metody Init |
W wersji edytora, którą właśnie tworzymy, mamy możliwość dodawania do planszy wielu elementów jednym pociągnięciem pędzla. Z jednej strony jest to bardzo wygodne ułatwienie. Z drugiej jednak, w przypadku pomyłki w malowaniu wielu klocków, użytkownikowi trudno jest wycofać wprowadzone zmiany.
W tym rozdziale zaproponujemy rozwiązanie powyższego problemu. Wykorzystamy do tego wzorzec projektowy polecenie. Efekt jego działania możemy porównać do zarządzania zadaniami dla drukarki. Możemy zlecić wydruk pliku, przejrzeć oczekujące dokumenty czy wycofać któryś z elementów w kolejce. Ten wzorzec projektowy jest bardzo prosty, a jednocześnie bardzo przydatny i pozwala na implementację takich funkcjonalności jak wielopoziomowe cofanie zmian, wyświetlanie pasków postępu czy nagrywanie makr. Z pewnością warto dodać go do swojego warsztatu.
Wykorzystując opisany wzorzec, dodamy do naszego edytora możliwość cofania wykonanych akcji, czyli między innymi:
Podstawowym elementem opisywanego wzorca jest zadanie do wykonania, zwane także poleceniem lub komendą. Jest to klasa bazowa (zazwyczaj abstrakcyjna), która dostarcza interfejsu w postaci dwóch (czysto wirtualnych) metod: wykonaj oraz cofnij. Ponadto dodajemy funkcję, która pozwala na sprawdzenie czy polecenie jest gotowe do wykonania. Jeżeli zwróci ona wartość fałsz, może to oznaczać, że nie zostały przekazane wszystkie wymagane informacje, aby zadanie wykonać (np. nie jest poniedziałek lub użytkownik zaznaczył zbyt mały obszar). Warunki wykonania są sprawdzane w konkretnych poleceniach, czyli klasach pochodnych.
Zauważmy, że w metodzie Execute przekazujemy argument typu Editor*. Dlaczego jest to
zwykły wskaźnik? Ponieważ służy on jedynie do wykonania pewnych akcji na edytorze. W żadnym wypadku nie
powinno się takiego wskaźnika zapamiętywać w strukturze polecenia. Jeżeli będziemy używali go zgodnie z
przeznaczeniem, to nigdy nie dojdzie do wycieku pamięci (lub innych kłopotów z pamięcią, np. podwójnym
zwolnieniem obiektu).
Poniżej przedstawiamy klasę bazową dla poleceń wykonywanych w edytorze:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // Plik: editor/EditorCommand.h #ifndef __EDITORCOMMAND_H_INCLUDED__ #define __EDITORCOMMAND_H_INCLUDED__ #include "../StdAfx.h" #include "../Entity.h" class Editor; class EditorCommand; typedef boost::shared_ptr<EditorCommand> EditorCommandPtr; class EditorCommand { public: virtual ~EditorCommand() {} public: virtual void Execute(Editor* editor) = 0; virtual void Undo(Editor* editor) = 0; virtual bool IsReady() const = 0; bool IsNotReady() const { return !IsReady(); } }; |
Drugim elementem, którego potrzebujemy jest miejsce do przechowywania wykonanych zadań. Dzięki temu będziemy mogli uzyskać dostęp do ostatnio wykonanego zadania, aby je wycofać. W naszym przypadku polecenia dotyczą akcji wykonywanych w edytorze, więc historię wykonywanych poleceń zapamiętamy w klasie Editor.
1 2 3 4 5 6 7 8 9 10 11 12 | // Plik: editor/Editor.h #include "EditorCommand.h" // (...) private: // (...) std::vector<bool> m_keys_down; typedef std::list<EditorCommandPtr> EditorCommandsContainer; EditorCommandsContainer m_commands; }; |
Dodajemy także funkcję, która ułatwi nam jednoczesne dodawanie oraz wykonywanie poleceń do historii.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // Plik: editor/Editor.h private: // (...) // Dodaje polecenie do historii, a następnie je wykonuje void RegisterAndExecuteCommand(EditorCommandPtr command); // Plik: editor/Editor.cpp void Editor::RegisterAndExecuteCommand(EditorCommandPtr command) { if (command && command->IsReady()) { m_commands.push_back(command); command->Execute(this); } } |
Skoro jesteśmy już przy dodawaniu i wycofywaniu poleceń, to warto zaznaczyć, że polecenie nie pamięta czy zostało wycofane czy nie. Dlatego o poprawność tej operacji powinien zatroszczyć się edytor. To właśnie do jego zadań należy także zadbanie o właściwą kolejność cofania. W przeciwnym wypadku użytkownik może otrzymać niespodziewane lub wręcz nawet niechciane rezultaty. Wiele zależy od tego jak bardzo zadania są od siebie niezależne.
Kod do obsługi wycofywania wykonanych poleceń podpinamy pod zdarzenie naciśnięcia klawisza Backspace.
1 2 3 4 5 6 7 8 9 10 11 | // Plik: editor/Editor.cpp, metoda Editor::ProcessEvents } else if (event.type == SDL_KEYDOWN) { if (IsGuiHidden() || m_gui->OnKeyDown(event.key.keysym.sym)==false) { m_keys_down[event.key.keysym.sym] = true; } if (event.key.keysym.sym == SDLK_BACKSPACE && m_commands.empty()==false) { m_commands.back()->Undo(this); m_commands.pop_back(); } } else if (event.type == SDL_KEYUP) { |
Wiemy już jak wygląda klasa bazowa dla poleceń wykonywanych w edytorze. Umiemy także wycofywać kolejne zadania. Potrzebne jest nam jeszcze uzyskiwanie poleceń z pędzla. Chcemy, żeby pędzle mogły zwracać różne zadania (definiowane jako podklasy EditorCommand), dlatego nową metodę Brush::GetCommand czynimy wirtualną. Klasa MultiBrush będzie zwracała polecenie PlatformEditorCommand, które zdefiniujemy niebawem.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Plik Brush.h, klasa Brush #include "EditorCommand.h" // (...) public: virtual EditorCommandPtr GetCommand() const { return EditorCommandPtr(); } // (..) // klasa MultiBrush public: virtual EditorCommandPtr GetCommand() const { if (IsActive()) { std::cerr << "UWAGA: Pobranie akcji z aktywnego pędzla multibrush. " << "Akcja jest gotowa dopiero, gdy pędzel jest nieaktywny"; } return PlatformEditorCommandPtr( new PlatformEditorCommand(GetStart(), GetEnd())); } |
Jak widać w metodzie Brush::GetCommand, domyślnie zwracana jest pusta komenda. Do tej pory akcja, którą należy wykonać dla instancji typu Brush była podejmowana w metodzie Editor::ActionAtCoords. Dlatego to ją poddamy refaktoringowi definiując jednocześnie następujące podklasy EditorCommand:
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 | void Editor::ActionAtCoords(double x, double y) { BrushPtr brush = m_gui->GetActiveBrush(); if (brush) { if (InPaintingFieldMode()) { RegisterAndExecuteCommand( SetFieldCommandPtr(new SetFieldCommand(static_cast<size_t>(x), static_cast<size_t>(y), brush->GetFieldType())) ); } else if (InPaintingEntityMode()) { const ET::EntityType entity_type = brush->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); RegisterAndExecuteCommand( AddEntityCommandPtr(new AddEntityCommand(entity_data)) ); } else if (InPaintingSpecialMode()) { const Brush::ST::SpecialType special_type = brush->GetSpecialType(); if (special_type == Brush::ST::Player) { m_player_data = LevelEntityData("player", x, y); } else if (special_type == Brush::ST::Eraser) { RegisterAndExecuteCommand( SetFieldCommandPtr(new SetFieldCommand(static_cast<size_t>(x), static_cast<size_t>(y), FT::None)) ); } else if (special_type == Brush::ST::Multi) { MultiBrushPtr mb = boost::dynamic_pointer_cast<MultiBrush>(brush); mb->StartAt(x, y); // Polecenie zostanie dodane po zwolnieniu klawisza (-> ReleaseAt) } else { std::cerr << "Niezdefiniowana akcja w trybie specjalnym\n"; } } else { std::cerr << "Nie odnaleziono trybu rysowania" << std::endl; assert(false && "Nie odnaleziono trybu rysowania"); } } } |
Jak sugeruje komentarz w kodzie, polecenie dla multipędzla pobierane jest w
metodzie ReleaseAt. Wynika to z trójfazowego dostarczania informacji do tego zaawansowanego pędzla
(StartAt, MoveTo, FinishAt).
Oczywiście moglibyśmy przenieść także pozostałe
akcje do metody ReleaseAt. Jednak pozostaje kwestią gustu czy komenda powinna zostać wykonana w
momencie naciśnięcia czy zwolnienia klawisza myszy. Pamiętajmy, że nawet jeżeli użytkownik się pomyli i
wykona złą akcję, to będzie mógł wycofać wprowadzone zmiany.
Zaktualizowana definicja metody
Editor::ReleaseAt wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // Plik: editor/Editor.cpp void Editor::ReleaseAtCoords(double x, double y) { BrushPtr brush = m_gui->GetActiveBrush(); if (! brush) { return; } if (brush->GetSpecialType() == Brush::ST::Multi) { MultiBrushPtr multibrush = boost::dynamic_pointer_cast<MultiBrush>(brush); multibrush->FinishAt(x, y); } EditorCommandPtr command = brush->GetCommand(); if (command && command->IsReady()) { command->Execute(this); m_commands.push_back(command); } } |
Zaprezentujemy teraz kod klas dla nowych poleceń. Na pierwszy ogień pójdzie SetFieldCommand. Aby zmienić któreś z pól planszy, ta klasa potrzebuje znać jego współrzędne oraz rodzaj pola, które należy pod nimi umieścić. Dodatkowo powinna zapamiętać, jakie pole znajdowało się tam poprzednio. Dzięki temu będzie możliwe wycofanie tego polecenia. Przyjrzyjmy się deklaracji klasy SetFieldCommand:
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 | // Plik: editor/EditorCommand.h class SetFieldCommand; typedef boost::shared_ptr<SetFieldCommand> SetFieldCommandPtr; class SetFieldCommand : public EditorCommand { public: explicit SetFieldCommand(const Position& pos, FT::FieldType field) : m_is_ready(false), m_pos(pos), m_field(field) { } explicit SetFieldCommand(size_t x, size_t y, FT::FieldType field) : m_is_ready(false), m_pos(Position(x, y)), m_field(field) { } virtual void Execute(Editor* editor); virtual void Undo(Editor* editor); virtual bool IsReady() const; private: bool m_is_ready; // Czy polecenie jest gotowe do wykonania Position m_pos; // FT::FieldType m_field, m_saved_field; }; |
Jak już zostało wspomniane, implementacja tego polecenia polega na prostej manipulacji wartością pola na wskazanych współrzędnych:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // Plik: editor/SetFieldCommand.cpp #include "Editor.h" #include "EditorCommand.h" void SetFieldCommand::Execute(Editor* editor) { m_saved_field = editor->GetFieldAt(m_pos); editor->SetFieldAt(m_pos, m_field); } void SetFieldCommand::Undo(Editor* editor) { editor->SetFieldAt(m_pos, m_saved_field); } bool SetFieldCommand::IsReady() const { if (m_pos.X() < 0 || m_pos.Y() < 0) return false; if (m_field == FT::COUNT) return false; return true; } |
Deklaracja klasy opisującej drugie polecenie również jest bardzo prosta. AddEntityCommand otrzymuje w konstruktorze informacje o jednostce którą należy dodać do edytowanego poziomu. Przekazane dane zostaną wykorzystane do utworzenia instancji jednostki, która także zostanie zapamiętana w poleceniu. Dzięki temu możliwe będzie wykonanie operacji Undo w celu usunięcia z poziomu zarówno opisu jak i samej jednostki.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // Plik: editor/EditorCommand.h class AddEntityCommand; typedef boost::shared_ptr<AddEntityCommand> AddEntityCommandPtr; class AddEntityCommand : public EditorCommand { public: explicit AddEntityCommand(const LevelEntityData& entity_data) : m_entity_data(entity_data) { } virtual void Execute(Editor* editor); virtual void Undo(Editor* editor); virtual bool IsReady() const; private: LevelEntityData m_entity_data; EntityPtr m_entity; }; |
Metoda AddEntityCommand::Execute deleguje utworzenie jednostki do fabryki. Następnie dodaje do tworzonego poziomu zarówno opis jak i samą jednostkę.
1 2 3 4 5 6 7 8 9 10 11 12 | // Plik: editor/AddEntityCommand.cpp #include "Editor.h" #include "EditorCommand.h" #include "../EntityFactory.h" #include <algorithm> void AddEntityCommand::Execute(Editor* editor) { m_entity = EntityFactory().CreateEntity(m_entity_data); editor->m_entities_to_create.push_back(m_entity_data); editor->m_entities.push_back(m_entity); } |
W funkcji Undo musimy wykonać operacje odwrotne do tych z Execute. Dlatego usuwamy opis jednostki z listy jednostek do utworzenia a samą jednostkę (przy użyciu idiomu erase-remove) z kontenera jednostek. Wiemy, że każda jednostka występuje jako osobny obiekt, więc poniższe usunięcia zadziałają poprawnie. Aby usuwanie z kontenera m_entities_to_create było możliwe, musimy jeszcze dopisać funkcję stwierdzającą równość dwóch obiektów typu LevelEntityData.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // Plik: editor/AddEntityCommand.cpp void AddEntityCommand::Undo(Editor* editor) { editor->m_entities_to_create.remove(m_entity_data); editor->m_entities.erase(std::remove(editor->m_entities.begin(), editor->m_entities.end(), m_entity), editor->m_entities.end()); } // Plik: Level.h, struktura LevelEntityData bool operator== (const LevelEntityData& other) const { return name == other.name && abs(x-other.x) < 0.001 && abs(y-other.y) < 0.001; } |
Polecenie typu AddEntityCommand jest zawsze gotowe do wykonania. Ewentualnie moglibyśmy sprawdzić poprawność przekazanego opisu jednostki.
1 2 3 4 5 | // Plik: editor/AddEntityCommand.cpp bool AddEntityCommand::IsReady() const { return true; } |
Podczas wykonywania polecenia, czyli wywołania wirtualnej metody Execute z EditorCommand, przekazujemy do zadania wskaźnik do klasy Editor. Aby konkretne polecenie mogło modyfikować tę klasę, powinno mieć dostęp do metod typu ClearFieldAt, SetField, itp. Ponieważ są one prywatne, to najprostszym sposobem na ich udostępnienie będzie nawiązanie przyjaźni między klasami. Robimy to dopisując gdzieś w deklaracji klasy Brush następujące wiersze:
1 2 3 4 5 | // Plik editor/Brush.h friend class SetFieldCommand; friend class AddEntityCommand; friend class PlatformEditorCommand; |
Przy okazji modyfikacji pliku Brush.h, dopiszemy dodatkowe warianty wspomnianych funkcji, tak, aby zamiast dwóch parametrów (czyli x oraz y) można było przekazać jeden typu Position. Deklaracje podajemy w pliku editor/Editor.h:
1 2 3 4 5 6 7 8 9 10 | // Plik: editor/Editor.h, klasa Editor void ClearFieldAt(double x, double y); void ClearFieldAt(const Position& pos); void SetFieldAt(double x, double y, FT::FieldType ft); void SetFieldAt(const Position& pos, FT::FieldType ft); FT::FieldType GetFieldAt(double x, double y) const; FT::FieldType GetFieldAt(const Position& pos) const; |
Definicje powyższych deklaracji podajemy w pliku editor/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 | // Plik: editor/Editor.cpp void Editor::ClearFieldAt(double x, double y) { SetFieldAt(x, y, FT::None); } void Editor::ClearFieldAt(const Position& pos) { ClearFieldAt(pos.X(), pos.Y()); } 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); } void Editor::SetFieldAt(const Position& pos, FT::FieldType ft) { SetFieldAt(pos.X(), pos.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))); } FT::FieldType Editor::GetFieldAt(const Position& pos) const { return GetFieldAt(pos.X(), pos.Y()); } |
Na koniec zajmiemy się implementacją najciekawszego polecenia w naszym edytorze, to znaczy tego, które wspólnie z multipędzlem było motywacją do powstania tego artykułu.
W konstruktorze przekazujemy początkowy oraz końcowy punkt zaznaczenia. Są to dwa dowolne punkty we współrzędnych świata, które wyznaczają prostokątny obszar. Poza tym w klasie przechowujemy pola, które wcześniej były na obszarze, który wypełnimy podczas wywołania metody Execute. Oto definicja klasy PlatformEditorCommand:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Plik: editor/EditorCommand.h class PlatformEditorCommand; typedef boost::shared_ptr<PlatformEditorCommand> PlatformEditorCommandPtr; class PlatformEditorCommand : public EditorCommand { public: explicit PlatformEditorCommand(const Position& start, const Position& end) : m_is_ready(false), m_beg(start), m_end(end) { } virtual void Execute(Editor* editor); virtual void Undo(Editor* editor); virtual bool IsReady() const; private: bool m_is_ready; // Czy polecenie jest gotowe do wykonania Position m_beg, m_end; // Początek i koniec zaznaczenia. Wsp.świata std::vector<FT::FieldType> m_saved_fields; // zapisane pola planszy }; |
Przejdźmy do implementacji. Metoda Execute działa w trzech fazach:
Zerowa oraz pierwsza faza są bardzo proste. Kod, który widzimy poniżej, pojawił się już podczas rysowania szkicu multipędzla. Ponownie wykorzystujemy klasę TileGridHelper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // Plik: editor/PlatformEditorCommand.cpp #include "Editor.h" #include "EditorCommand.h" #include "../TileGridHelper.h" void PlatformEditorCommand::Execute(Editor* editor) { if (IsReady() == false) { throw std::logic_error("PlatformEditorCommand: Nie można uruchomić " "polecenia, które nie jest gotowe."); } TileGridHelper tgh(m_beg, m_end); tgh.SnapToGrid().SortCoordsOfBox(); const unsigned tiles_hor = tgh.TilesHorizontally(); const unsigned tiles_ver = tgh.TilesVertically(); |
Druga faza, to zapamiętanie aktualnego stanu planszy, dzięki czemu będzie możliwość wycofania wykonanej komendy. Pola zapisujemy w jednowymiarowym wektorze. Zapisywanie w dwuwymiarowej siatce nie jest potrzebne, gdyż znamy szerokość prostokąta do wypełnienia, więc możemy łatwo wyliczyć, kiedy zaczyna się nowy wiersz.
1 2 3 4 5 6 7 8 9 | // Plik: editor/PlatformEditorCommand.cpp, kontynuacja kody powyżej // Zapisz kafelki, które aktualnie występują na planszy for (unsigned ver = 0; ver < tiles_ver; ver++) { // wypełnienie for (unsigned hor = 0; hor < tiles_hor; hor++) { m_saved_fields.push_back( editor->GetFieldAt(tgh.Beg() + Position(hor, ver))); } } |
Trzecia faza wykonywania polecenia polega na wypełnieniu zaznaczonego prostokąta odpowiednimi polami. Poniższy kod najpierw wypełnia całość środkowym kaflem (PlatformMid), a następnie nadpisuje krawędzie. Lewa krawędź jest wypełniana przez kafel PlatformLeft, prawa przez PlatformRight, a górna -- PlatformTop. W ostatnim kroku umieszczamy odpowiednie kafle w górnych narożnikach.
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 | // Plik: editor/PlatformEditorCommand.cpp, kontynuacja kody powyżej // Prostokąt for (unsigned ver = 0; ver < tiles_ver; ver++) { // wypełnienie for (unsigned hor = 0; hor < tiles_hor; hor++) { editor->SetFieldAt( tgh.Beg() + Position(hor, ver), FT::PlatformMid); } } for (unsigned hor = 0; hor < tiles_hor; hor++) { // górna krawędź editor->SetFieldAt( tgh.Beg() + Position(hor, tiles_ver - 1), FT::PlatformTop); } for (unsigned ver = 0; ver < tiles_ver; ver++) { // boki editor->SetFieldAt( tgh.Beg() + Position(0, ver), FT::PlatformLeft); editor->SetFieldAt( tgh.Beg() + Position(tiles_hor - 1, ver), FT::PlatformRight); } // narożniki (górny lewy i górny prawy) editor->SetFieldAt( tgh.Beg() + Position(0, tiles_ver - 1), FT::PlatformTopLeft); editor->SetFieldAt( tgh.Beg() + Position(tiles_hor - 1, tiles_ver - 1), FT::PlatformTopRight); } |
Przejdźmy teraz do wycofywania ostatnio wykonanego polecenia. Na początku sprawdzamy czy było ono gotowe do
wykonania. Jeżeli nie, to próba wycofania go prawdopodobnie oznacza błąd w aplikacji (gdyż nie powinno się
ono znaleźć na liście wykonanych poleceń), więc chcemy to zasygnalizować. Następnie (po raz trzeci)
wykorzystujemy TileGridHelper do uporządkowania punktów oraz uzyskania informacji o rozmiarach
prostokątnego zaznaczenia.
Po upewnieniu się, że zapamiętana liczba pól zgadza się z wymiarami
prostokąta, przystępujemy do odtwarzania dawnej zawartości. Cała trudność tego zadania polega na dobrym
skonstruowaniu dwóch zagnieżdżonych pętli (choć można uzyskać równoważny efekt posługując się tylko jedną
pętlą - jak?). Kod całej funkcji Undo 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 | void PlatformEditorCommand::Undo(Editor* editor) { if (IsNotReady()) { throw std::logic_error("PlatformEditorCommand: Nie można wycofać " "polecenia, które nie jest gotowe."); } if (m_saved_fields.empty()) { return; } TileGridHelper tgh(m_beg, m_end); tgh.SnapToGrid().SortCoordsOfBox(); unsigned tiles_hor = tgh.TilesHorizontally(); unsigned tiles_ver = tgh.TilesVertically(); assert(tiles_hor * tiles_ver == m_saved_fields.size() && "Zła liczba pól"); std::vector<FT::FieldType>::const_iterator it = m_saved_fields.begin(); for (unsigned ver = 0; ver < tiles_ver; ver++) { // wypełnienie for (unsigned hor = 0; hor < tiles_hor; hor++) { editor->SetFieldAt(tgh.Beg() + Position(hor, ver), *it); ++it; } } } |
Ostatnią funkcją, której dostarcza każde polecenie w edytorze jest IsReady. W przypadku PlatformEditorCommand jest to kilka warunków, które doprowadziłyby do powstania nieciekawego wypełnienia. Poniżej znajduje się cały kod metody PlatformEditorCommand::IsReady:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | bool PlatformEditorCommand::IsReady() const { if (m_beg.X() < 0 || m_beg.Y() < 0 || m_end.X() < 0 || m_end.Y() < 0) { return false; } TileGridHelper tgh(m_beg, m_end); tgh.SnapToGrid().SortCoordsOfBox(); unsigned tiles_hor = tgh.TilesHorizontally(); unsigned tiles_ver = tgh.TilesVertically(); if (tiles_hor == 1) return false; // szerokość == 1 if (tiles_hor < 1 || tiles_ver < 1) return false; // mniej niż 1x1 if (tiles_ver == 1 && tiles_hor == 1) return false; // dokładnie 1x1 return true; } |
Powyższa zmiana jest ostatnią wprowadzoną w tym artykule. Teraz możemy w pełni cieszyć się nową funkcjonalnością w naszym edytorze. Nasz kod jest gotowy na dodawanie kolejnych poleceń do naszego edytora. W tym celu należy zrobić dwie rzeczy:
Dzięki zastosowaniu wzorca projektowego polecenie, każda klasa odpowiadająca zadaniu ma bardzo dobrze określony zakres swoich obowiązków. Dzięki temu jej kod jest krótki, łatwy do napisania i z dużym prawdopodobieństwem nie zawiera poważnych błędów. Składając większy kawałek kodu z małych, dobrze zdefiniowanych cegiełek nie tylko zwiększamy przejrzystość naszego projektu, ale także dajemy możliwość poskładania klocków w inny sposób. Takich zalet na pewno nie mają długie na kilka ekranów funkcje (por. ang. god method) czy wszystkorobiące klasy (tzw. god class lub god object).
Masz pytanie, uwagę? Stworzyłeś poziom, którym chcesz się podzielić? A może zauważyłeś błąd? Powiedz o tym na forum [5].
Pobierz [6] końcowy kod źródłowy do tego artykułu.
Odnośniki:
[1] http://marcindev.blogspot.com/
[2] http://informatyka.wroc.pl/node/918
[3] http://informatyka.wroc.pl/node/1333
[4] http://informatyka.wroc.pl/upload/mmi/platf/13/13_start.zip
[5] http://informatyka.wroc.pl/forum/viewtopic.php?f=55&t=358
[6] http://informatyka.wroc.pl/upload/mmi/platf/13/13_final.zip