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

15.03.2011 - Marcin Milewski
TrudnośćTrudność

Przedstawienie pędzla

Zanim jednak przejdziemy do uaktualnienia klasy Editor, wyjaśnijmy, czym jest pędzel. Po raz pierwszy pojawił się w poprzednim artykule, a w tym wykorzystaliśmy go podczas tworzenia specjalnej kontroli. Otóż, pędzel ten ma takie samo zadanie jak w edytorze grafiki. To znaczy, że wybieramy pędzel z podręcznej palety, a następnie nim rysujemy. W naszym edytorze będzie dokładnie tak samo. Dokonamy jednak pewnego uproszczenia i uznamy gumkę za specjalny rodzaj pędzla.

Pędzle w naszym edytorze dzielimy na 3 rodzaje:

  1. Field brush -- służący do stawiania pól planszy (czyli przede wszystkim podłoża).
  2. Entity brush -- odpowiadający za dodawanie jednostek oraz bonusów.
  3. Special brush -- pędzel do zadań specjalnych, np. gumka czy ustalanie początkowej pozycji gracza.

Elementy dodawane przez konkretne rodzaje pędzli.
a) Pędzel do dodawania pól; b) Pędzel do dodawania jednostek; c) Pędzel do zadań specjalnych

Pędzel będzie przechowywał (można o tym myśleć w kategorii opakowania -- pędzel opakowuje sprite'a) instancję klasy Sprite. Obiekt zawierający instancję typu Brush będzie mógł ją odpytać o posiadany obrazek, a następnie wykorzystać go do własnych celów. Zwykle oznacza to, iż sprawdzi on jakiego obiektu reprezentacją jest sprite, a następnie doda opis odpowiedniego elementu do poziomu, który potem zostanie zapisany do pliku lvl lub ents. Zauważ, że ten mechanizm jest bardziej ogólny niż przedstawione tu zastosowanie.

Poniżej znajduje się implementacja omawianej klasy. Dla skrócenia kodu wszystkie trzy typy pędzla są reprezentowane przez tę samą klasę. W przypadku rozwoju gry o kolejne rodzaje pędzli, jest to dobre miejsce, żeby poćwiczyć refaktoryzację kodu. Oto kod klasy Brush:

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
51
52
53
54
55
56
57
58
// Plik: editor/Brush.h
#ifndef __BRUSH_H_INCLUDED__
#define __BRUSH_H_INCLUDED__
 
class Brush;
typedef boost::shared_ptr<Brush> BrushPtr;
 
class Brush {
public:
    struct ST {
        enum SpecialType { UNKNOWN, Player, Eraser };
    };
    
    explicit Brush(SpritePtr sprite, FT::FieldType ft)
        : m_sprite(sprite),    m_is_field(true),
          m_is_entity(false),  m_is_special(false),
          m_field_type(ft)
        {
    }
    explicit Brush(SpritePtr sprite, ET::EntityType et)
        : m_sprite(sprite),    m_is_field(false),
          m_is_entity(true),   m_is_special(false),
          m_entity_type(et)
        {
    }
    explicit Brush(SpritePtr sprite, ST::SpecialType st)
        : m_sprite(sprite),    m_is_field(false),
          m_is_entity(false),  m_is_special(true),
          m_special_type(st)
        {
    }
 
    bool IsField()   const { return m_is_field; }
    bool IsEntity()  const { return m_is_entity; }
    bool IsSpecial() const { return m_is_special; }
 
    // Zwraca typ pola/encji/specjalny.
    // Wartość jest poprawna, gdy IsField/IsEntity/IsSpecial()==true
    FT::FieldType GetFieldType() const { return m_field_type; }
    ET::EntityType GetEntityType() const { return m_entity_type; }
    ST::SpecialType GetSpecialType() const { return m_special_type; }
 
    SpritePtr GetSprite() const { return m_sprite; }
 
private:
    SpritePtr m_sprite;
 
    bool m_is_field;
    bool m_is_entity;
    bool m_is_special;
 
    FT::FieldType m_field_type;
    ET::EntityType m_entity_type;
    ST::SpecialType m_special_type;
};
 
#endif
  

W celu ułatwienia tworzenia instancji tej klasy, dodamy metody statyczne tworzące każdy z typów pędzla. Nazywamy je New, sugerując tym samym, że zwrócony zostanie wskaźnik (w naszym przypadku inteligentny wskaźnik, bo takimi się posługujemy), a nie obiekt klasy Brush. Poniższy kod powinien znaleźć się w sekcji publicznej tej klasy.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Plik: editor/Brush.h
    static BrushPtr New(SpritePtr sprite, FT::FieldType ft) {
        return BrushPtr(new Brush(sprite, ft));
    }
    
    static BrushPtr New(SpritePtr sprite, ET::EntityType et) {
        return BrushPtr(new Brush(sprite, et));
    }
    
    static BrushPtr New(SpritePtr sprite, ST::SpecialType st) {
        return BrushPtr(new Brush(sprite, st));
    }
  

Korzystanie z takich statycznych metod jest bardzo wygodne, jeżeli często tworzymy wskaźniki na obiekty. Bez nich, pisalibyśmy:

1
2
    BrushPtr(new Brush(sprite, type));
  

Dzięki nim możemy napisać krótszy i lepiej oddającą intencje programisty następujący kod:

1
2
    Brush::New(sprite, type);
  

Upgrade klasy Editor

Utworzyliśmy już kompletny mechanizm obsługujący graficzny interfejs użytkownika. W naszym edytorze użytkownik może w prosty sposób wybrać, gdzie i jaki element (jednostka, bonus, przeciwnik, pole planszy,...) chce dodać do poziomu. Umieszczenie nowego obiektu odbywa się poprzez kliknięcie myszką. Aby jednak odniosło swój skutek, sterowanie w aplikacji musi dotrzeć w odpowiednie miejsce kodu obsługującego GUI. Dlatego też swoje kroki kierujemy teraz w stronę integracji systemu GUI z dotychczas rozwiniętym edytorem.

Pierwszym polem jakie należy dodać jest wskaźnik (oczywiście inteligentny) na klasę bazową Gui. To właśnie do niej klasa Editor będzie przekazywała m.in. komunikaty z urządzeń wejścia. Będzie to możliwe jedynie wtedy gdy GUI będzie prezentowane na ekranie. Aby obsłużyć pokazywanie i ukrywanie graficznego interfejsu użytkownika dodajemy pole (nazywane czasem flagą lub znacznikiem ze względu na swój charakter, czyli możliwość przyjęcia tylko jednej z dwóch wartości) m_is_gui_visible. Konstruktor klasy Editor prezentuje się teraz następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Należy także pamiętać o dołączeniu nagłówka EditorGui.h
 
    explicit Editor(LevelPtr level) 
        : m_next_app_state(),
          m_in_game(false),
          m_gui(new EditorGui),              // NOWE!
          m_is_gui_visible(true),            // NOWE!
          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
        {
            // nop
            SetDone(false);
    }
  

Nowe (prywatne) pola tej klasy:

1
2
3
    EditorGuiPtr m_gui;              // kontrolki do wybierania stawianych pól
    bool m_is_gui_visible;           // czy kontrolki są widoczne?
  

Publiczne metody do włączania/wyłączania oraz odpytywania GUI o widoczność:

1
2
3
4
5
    // pokazuje/ukrywa gui
    void ToggleGui()          { m_is_gui_visible = !m_is_gui_visible; }
    bool IsGuiVisible() const { return m_is_gui_visible; }
    bool IsGuiHidden()  const { return !IsGuiVisible(); }
  

Jak już wspominaliśmy przy okazji prezentacji pędzla, może być on jednego z trzech rodzajów, co związane jest z akcją zleconą przez użytkownika:

  • 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.

Poniżej znajduje się kod usprawniający sprawdzanie, czy wskazany tryb jest aktualnie aktywny. Są to metody publiczne.

1
2
3
4
5
6
7
8
   // w jakim trybie jest rysowany pędzel
   bool InPaintingFieldMode()   const { return m_brush && m_brush->IsField(); }
   bool InPaintingEntityMode()  const { return m_brush && m_brush->IsEntity(); }
   bool InPaintingSpecialMode() const { return m_brush && m_brush->IsSpecial();}
 
   BrushPtr GetBrush()          const { return m_brush; }
   Editor* SetBrush(BrushPtr brush)   { m_brush = brush; return this; }
  

Ostatnią metodą, która będzie przydatna podczas implementacji nowych funkcjonalności jest ShouldSnapToGrid. Będzie ona odpowiadać "tak", gdy dodawany element powinien zostać wyrównany do (niewidzialnej) siatki, oraz "nie" w przeciwnym razie. Pierwszą wartość będzie zwracała przede wszystkim dla pól planszy. Będzie to jednak właściwe miejsce na dodanie kolejnych "wyjątków", na przykład jeżeli zażyczymy sobie, aby trzymanie klawisza "CTRL" wymuszało takie zachowanie edytora. Poniżej znajduje się prosta deklaracje omówionej metody:

1
2
3
    // czy rysowany obiekt (pod pędzlem) powinien być przyciągane do siatki
    bool ShouldSnapToGrid()      const;
  
0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com