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

02.06.2011 - Marcin Milewski
TrudnośćTrudność

Akcje, czyli jak wykorzystać wzorzec polecenia

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:

  • Dodanie jednostki do planszy.
  • Dodanie pojedynczego kafla podłoża.
  • Dodanie wielu kafli przy pomocy multipędzla.
Aby ten cel osiągnąć, wykonamy na obecnym kodzie edytora mały refaktoring. Pozwoli to na dostosowanie jego obecnego kształtu do nowych warunków wykorzystujących wzorzec polecenie.

Polecenie - widok z lotu ptaka

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.

Ilustracja efektu dodawania kilku poleceń do edytora. Można wycofać tylko zadanie znajdujące się na szczycie.

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) {
  
0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com