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

02.06.2011 - Marcin Milewski
TrudnośćTrudność

Opis i deklaracja klasy multipędzla

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. Rozpoczęcie zaznaczenia/malowania -- moment, w którym użytkownik naciska lewy przycisk myszy.
  2. Malowanie -- okres, w którym użytkownik przesuwa myszą po ekranie (cały czas z wciśniętym przyciskiem).
  3. Zakończenie malowania -- moment zwolnienia przycisku.
Ponieważ interesują nas tylko początkowe oraz końcowe położenia myszy, do klasy dodajemy dwa pola: m_start oraz m_end typu Position. Ponadto chcemy mieć informację o tym, czy pędzel jest w danym momencie wykorzystywany (czyli rozpoczęto malowanie, ale go nie zakończono). Pędzel w takim stanie będziemy nazywać aktywnym. Akcja przechowywana w pędzlu będzie poprawna, gdy pędzel będzie nieaktywny (zakończono rysowanie) oraz punkty zaznaczenia będą zawierały współrzędne nieujemne (rozpoczęto rysowanie).

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

Rysowanie multipędzla

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:

  • SnapToGrid -- przyciąganie punktów do siatki kafelków (nasza plansza wygląda właśnie w ten sposób);
  • SortCoordsOfBox -- dla dowolnych dwóch punktów (przekazanych w konstruktorze), które wyznaczają prostokąt zmienia ich współrzędne tak, aby metoda GetStart() zwracała ten punkt prostokąta, który jest najbliżej początku układu współrzędnych (można o nim myśleć jako o lewym dolnym);
  • TilesHorizontally -- wylicza i zwraca szerokość zaznaczenia w liczbie kafli;
  • TilesVertically -- wylicza i zwraca wysokość zaznaczenia w liczbie kafli;
Dzięki tym funkcjom, możemy w prosty sposób przekształcić dostarczone przez użytkownika punkty, do postaci która jest dla nas najbardziej użyteczna. Zauważmy, że pętle rysujące kolejne kafle zakładają, iż pierwszy kafel w szkicu znajduje się w lewym dolnym rogu. A dzięki informacjom o rozmiarze prostokąta z podglądem, napisanie wspomnianych pętli jest bardzo proste.

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

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com