SFML-owe zabawy #11 - Warianty


2018-12-13, 00:00

Kolejna iteracja za nami, a wraz z nią nowa funkcjonalność! :) Dzisiaj opowiem Wam, jak w praktyce działa zasada KISS na przykładzie funkcjonalności “Zmiana rozmiaru chmurki”.


Uwaga. Wszystko, o czym piszę poniżej znajduje się na branchu iteration-10 w repozytorium projektu, o tutaj: https://github.com/CppPolska/MarioEdit/tree/iteration-10. Jeżeli masz uwagi odnośnie kodu, bądź chcesz dołączyć do projektu - daj znać w komentarzu :)


Dodatkowo informuję, że wszystkie prawa do grafik używanych w projekcie należą do firmy Nintendo. Projekt ma na celu jedynie propagowanie wiedzy o języku C++.


Co udało się zrobić w tej iteracji?

Do samego końa nie wiedziałem, że aż tyle uda mi się zrobić. Wpadło sporo refactoringu :) Dodatkowo powrzucałem bardzo pomocne mi logi do zdarzeń występujących w aplikacji. Wpadła też nowa funkcjonalność: zmiany rozmiarów chmurek, krzaczków oraz wzgórz!

Refactoring

Tak jak wspomniałem wyżej, kod znowu zyskał na jakości. Cieszę się, bo coraz częściej wprowadzam nowości ze standardu C++17 oraz wydzielam nowe klasy, które ukrywają się w różnych ciemnych zakamarkach. Kod w końcu zaczyna wyglądać profesjonalnie :)

Grid - zamiana std::shared_ptr na std::unique_ptr

W poprzednim wpisie pisałem o porządkach z pointerami - wszędzie gdzie mogłem zastąpiłem std::shared_ptr na std::unique_ptr. Zrobiłem to przede wszystkim ze względu na problem z zakleszczeniem wskaźników współdzielonych, które mogło prowadzić do wycieków pamięci. Dziś do grona unikalnych wskaźników trafia obiekt klasy Grid. Oby więcej takich zmian! :)

Cursor::dragItem - std::any zamienione na std::optional<std::vector>

Drugim, co udało mi się poprawić była zmiana typu obiektu aktualnie przeciąganego przez kursor (referencja do tego obiektu jest pamiętana w klasie Cursor właśnie). Wcześniej z wygody użyłem do tego typu std::any. Raziło mnie to, że tam może być dosłownie wszystko, włącznie z float-ami, int-ami i innymi cudami.

Obecnie mamy obiekt typu:

std::optional<
    std::variant<std::shared_ptr<DynamicTile>, std::shared_ptr<Figure>>
> draggedItem;

Po pierwsze, użyłem std::variant, ponieważ chcę mieć kontrolę nad tym, co będzie podczepione pod kursor. Po drugie std::optional, ponieważ możemy nie mieć nic podczepionego pod kursor myszki, co jest całkiem normalną sytuacją. Jak nam wiadomo, std::optional idealnie do tego się nadaje, gdyż nie jest to przypadek obsługi błędów. Więcej o samym std::optional możecie przeczytać tutaj i tutaj, a po informacje o std::variant zapraszam tu.

Myślę, że ta zmiana jest stanowczo na plus, bo patrząc na kod od razu widzimy, jakich konkretnie typów możemy się tutaj spodziewać.

Prywatny wizytator

Nawiązując do punktu wyżej, zazwyczaj z użyciem std::variant zachodzi konieczność użycia std::visit. Tak też poczyniłem, tworząc klasę ChangeVariantVisitator:

class ChangeVariantVisitator {

public:

    explicit ChangeVariantVisitator(sf::Uint8 variant) {
        this->variant = variant;
    }

    inline void operator()(std::shared_ptr<DynamicTile>& tile) {}

    inline void operator()(std::shared_ptr<Figure>& figure) {
        figure->changeVariant(variant);
    }

private:

    sf::Uint8 variant;
};

Tak zdefiniowanej klasy używamy sobie w ten sposób:

void EditorEventHandler::handleEvents(Keyboard &keyboard, Cursor &cursor) {
    // ...

    auto pressedNumeric = keyboard.getPressedNumeric();
    auto draggingItem = cursor.draggedItem;
    if (pressedNumeric != std::nullopt && draggingItem) {
        std::visit(ChangeVariantVisitator(pressedNumeric.value()), draggingItem.value());
    }

    // ...
}

Nie byłoby tutaj nic ciekawego do opisywania, gdyby nie to, że postanowiłem uczynić tą klasę… prywatną! :)
Żeby rozwiać wątpliwości: klasa prywatna to klasa w całości zadeklarowana i zdefiniowana w pliku źródłowym definiującym inną klasę. Dzięki takiemu zabiegowi mamy pewność, że klasa ta nie będzie widoczna poza tą jedyną jednostką kompilacji.

Modyfikatory const

Kolejną zmianą związaną z refactoringiem było nadanie modyfikatorów const do każdej metody niezmieniającej stanu obiektu (this). W deklaracji funkcji taki modyfikator wstawiamy tak:

float getScalePromotion() const;

Kiedy w tym miejscu mamy już jakiś inny modyfikator (np. override), to const wstawiamy tuż przed nim:

sf::Vector2f getPosition() const override;

Modyfikator const musimy również zawrzeć w definicji metody. Wstawiamy go pomiędzy nawias zamykający a klamrę:

sf::Vector2f Tile::getPosition() const {
    return sprite.getPosition();
}

Po co ogóle zdecydowałem się używać tego modyfikatora? Mam kilka argumentów za:

  1. Nie muszę analizować kodu wewnątrz metody aby wiedzieć, czy zmienia ona stan obiektu, czy nie.
  2. Zależy mi na tym, aby metoda X nie zmieniała stanu obiektu. Jeżeli wewnątrz użyję jakąkolwiek metodę, która mi ten stan zmienia, to kompilator od razu mnie o tym poinformuje. Można by powiedzieć, że stosuję tego jako dodatkowego zabezpieczenia, obok testów.
  3. Są to bardzo cenne instrukcje dla kompilatora, dzięki czemu kod wyjściowy może być lepiej zoptymalizowany.

Wydzielenie klasy Highlight

Ostatnią częścią refactoringu było wydzielenie klasy Highlight, która coraz to mocniej krystalizowała się wewnątrz klasy Grid. Klasa Grid posiadała już kilka zachowań, które nie dotyczyły się jej bezpośrednio.

Oto interfejs nowo powstałej klasy:

class Highlight : DrawableInterface, SquareableInterface, SquareableOnGridInterface {

public:

    explicit Highlight(sf::Color color, float lineDistance);

    void draw(std::shared_ptr<sf::RenderWindow> window) const override;

    void setPosition(sf::Vector2f position);
    sf::Vector2f getPosition() const override;

    void setSize(sf::Vector2u size);
    sf::Vector2u getSize() const override;

    sf::Vector2i getPointOnGrid() const override;
    sf::Vector2u getSizeOnGrid() const override;

    void setLineDistance(float lineDistance);

private:

    sf::RectangleShape area;
    sf::Vector2u size;
    float lineDistance;

    void recalculateSize();
};

Swoją drogą, klasa ta nie jest już taka mała - implementuje aż trzy interfejsy! Nono, dobrze, że ją wydzieliłem :)

Logowanie zdarzeń

Jedną z ważniejszych rzeczy jest logowanie wydarzeń tak, aby łatwo można było analizować błędy powstałe w trakcie procesu developmentu. Bez czaru, bez magii: służy mi do tego prosta klasa statyczna:

class Log {

public:

    static void out(std::string label);
    static void out(bool value, std::string label);
    static void out(sf::Vector2f vector, std::string label);
    static void out(sf::Rect<float> rect, std::string label);
    static void out(std::unique_ptr<Scale> &scale, std::string label);

    static void turnOnAutoLine(bool autoLine);

private:

    static void line();
    static bool autoLine;

};

Klasę tą wykorzystuję wszędzie tam, gdzie tego potrzebuję. Na moje aktualne potrzeby jest git, chociaż wiem że w przyszłości będzie potrzeba trochę to usprawnić, chociażby dodając przekierowanie logowania do pliku logów.

Warianty obiektów Figure

Yeeeeey! Mamy nową funkcjonalność :) Od dzisiaj możemy zmienić rozmiary figur, czyli krzaczków, chmurek i pagórków. Miałem kilka wizji tego, jak ta funkcja ma wyglądać, ostatecznie zdecydowałem się na bardzo alternatywny sposób działania. Ale zacznijmy od początku.

Na samym początku chciałem, aby każda figura miała swój zestaw zaczepów, po uchwyceniu których będziemy mogli zmienić jej rozmiar. Wyglądało to mniej więcej tak:

Indicators - 1

Wyobrażałem to sobie nieco inaczej, rzeczywistość zweryfikowała. Żółte “kółkowate” zaczepy zupełnie mi się nie podobały. Próbowałem znaleźć coś ładniejszego. Eksperymentowałem z czymś na wzór:

Indicators - 2

Niestety, ale dalej czułem niesmak. Mało zachęcało mnie to do dalszej pracy. Pomyślałem, że zrobię to tak, jak robi się to w programach graficznych:

Indicators - 3

Bum. Nie dość, że niesmak pozostał (choć już znacznie mniejszy), to jeszcze napotkałem problemy z implementacją przesuwania punktów w kierunku skośnym, które dodatkowo mnie zdemotywowały. Tego wieczora stwierdziłem, że nie wymyślę już nic lepszego i położyłem się spać.

Przed zaśnięciem znalazłem rozwiązanie mojego problemu. Niesamowicie proste (zasada KISS) i przyjazne dla użytkownika. Następnego dnia z samego rana przysiadłem i w pół godziny złożyłem prototyp, z którego w końcu byłem zadowolony.

Aktualny efekt

Efekt mojego wieczornego rozmyślania możecie zobaczyć na animacji poniżej. To czarne po lewej, to logi generowane podczas zdarzeń występujących na oknie i obiektach znajdujących się wewnątrz niego (bardzo przydatna rzecz). Coś, czego nie widać na animacji: wariant figury zmieniamy po naciśnięciu odpowiednio 1 lub 2 na klawiaturze (podczas drag-n-drop’u).

Warianty figur

Normalnie bomba! Po co kombinować z jakimiś uchwytami? Przecież można to samo osiągnąć bez nich! :) Nie sądziłem, że można to zrobić w tak… prosty sposób. W końcu, Keep It Simple, Silly! ;)

Czego nie udało się zrobić?

No niestety, nie udało mi się dowieźć wszystkiego, co zakładałem że dowiozę. Miałem w planie kilka wieczorów więcej dla Mario::Edit, ale brutalna rzeczywistość postanowiła nieco inaczej. Mowa o ściąganiu obiektów figur z poziomu belki górnej. Na szczęście refactoring, który poczyniłem aktualnie (związany z klasą Grid), mocno pomoże mi teraz.

Plany na następną iterację

Chciałbym usunąć klasę SceneGenerator, a obiekty chmurki, krzaczka i pagórka móc ściągać z górnej belki. To jest priorytet nr 1. Na drugim miejscu refactoring i dalsze wydzielanie obiektów - dzięki tego typu pracom jest coraz ładniej w projekcie. Punkt ostatni z cyklu “pobożne życzenia” to testy. Chciałbym mieć mocno przetestowany kod poza częścią infrastruktury.

W punktach:

  1. Ściąganie chmurki, krzaczka i pagórka z poziomu górnej belki.
  2. Wydzielanie odpowiedzialności z klas.
  3. Testy klas edytora.

Do następnego razu! :)



Marcin Kukliński

Zawodowo backend developer, hobbystycznie pasjonat języka C++. Po godzinach poszerza swoją wiedzę na takie tematy jak teorii kompilacji oraz budowa formatów plików. Jego marzeniem jest stworzyć swój własny język programowania.

Pssst! Używamy Cookies. Poprzez używanie naszego serwisu zgadzasz się na odczytywanie i zapisywanie Cookies w swojej przeglądarce.