SFML-owe zabawy #7 - Zabawa w architekta


2018-09-27, 00:00

Refactoringu ciąg dalszy. Chyba udało mi się zrobić wszystko tak, by móc ruszyć z kopyta w następnej iteracji. Zapraszam na siódmy już wpis z serii SFML-owe zabawy! :)


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


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

Funkcjonalnie nie posunąłem się dużo do przodu, ponieważ w dalszym ciągu zająłem się dostosowywaniem kodu tak, by móc ruszyć z kopyta dalej. Tak więc, sporo się pozmieniało w środku. Oprócz refactoringu, zrobiłem dużo poprawek, między innymi do mechanizmu klikania na puzelkach. Do tego dorzuciłem kilka nowych funkcji easingowych, które będę mógł wykorzystać w przyszłości.

Jedyna funkcjonalna nowość to panel, z którego będziemy mogli ściągać puzelki. Ot taki drobiazg, by coś nowego mogło cieszyć oko :)

Refactoringi

Oj, sporo tutaj się działo. Podobnie jak poprzednio, na samym początku naszło mnie na żonglowanie klasami. Po poprzednim refactoringu miałem wszystkie klasy ładnie powydzielane, ale zrobił mi się niezły bałagan: coraz częściej zadawałem sobie pytanie: do którego katalogu wrzucić tą klasę? Jak wiadomo, w takiej sytuacji należy rozglądnąć się dookoła, i zastanowić nad bieżącą architekturą.

Nowa struktura katalogów

W miarę dokładania nowych klas, trzeba dbać o to, aby były one odpowiednio pogrupowane. Jest to ważne, ponieważ im częściej będziemy dbać o ich położenie, tym mniej po głowie będzie nam chodziło pytanie: “a gdzie to było?”. Odpowiednio porozdzielane odpowiedzialności klas oraz dobre ich rozmieszczenie w projekcie szybko owocuje potężnym boostem w pracy.

W chwili obecnej cały projekt został podzielony na trzy części:

  1. Infrastruktura, czyli zestaw klas wspólnych, coś na wzór frameworka. Wszystkie klasy infrastruktury znajdują się w katalogu System/.
  2. Editor - jeden z dwóch kluczowych komponentów znajdujących się w systemie. Jest to reprezentacja funkcjonalności, którą opracowywuję od samego początku: edytora, dzięki któremu możemy tworzyć własne plansze w uniwersum Super Mario. Wszystko, co jest związane z tym komponentem znajduje się w katalogu o nazwie Editor/.
  3. Game - drugi kluczowy komponent programu. Jest to niezaimplementowana jeszcze część projektu, za którą zabiorę się, jak tylko skończę prace nad edytorem. Pusta klasa i katalog Game/ mają za zadanie w chwili obecnej zmuszać mnie do refleksji nad tym, jak w przyszłości ma wyglądać implementacja tego komponentu, oraz abym już w trakcie tworzenia modułu Editor myślał o wszystkich rozwiązaniach tak, by można było je wykorzystać również i tutaj.

Przerzucenie odpowiedzialności

Cofnijmy się o trzy iteracje wstecz. Mamy iterację nr 5 oraz klasę Game. Jest to potwór, który powydzielałem ostatnio na mniejsze odpowiedzialności. Utworzyłem klasę EventHandler, do której wrzuciłem cały kod związany z obsługą wydarzeń. Mimo tego zabiegu, tam i tak za wiele się działo. Ta klasa obsługiwała zarówno zdarzenia systemowe (klawiatura, kursor myszy), jak i zdarzenia związane z edytorem (MouseEnter i MouseLeave na obiektach DynamicTiles, drag-n-drop). Jak się domyślacie, ta druga odpowiedzialność to odpowiedzialność dotycząca edytora gry. Kod realizujący ją trafił ostatecznie do klasy Editor, która jest fasadą dla całego komponentu.

Interfejsy rządzą światem!

Bardzo dużą pomocą okazały się… interfejsy! :) Gdyby ktoś nie wiedział, to w C++ implementujemy interfejsy jako klasy abstrakcyjne z niezaimplementowanymi metodami wirtualnymi. Zauważyłem pewne regularności w kodzie. Miałem niby podobne do siebie funkcje, ale nie do końca. W jednej klasie do funkcji skalującej przekazywałem obiekt klasy Scale, a w innej przekazywałem wartość skalarną, de facto przechowywaną wewnątrz obiektu klasy Scale. Podobnie było z funkcją draw(). Raz przekazywałem obiekt klasy RenderWindow do metody draw(), a raz przekazywałem ten obiekt w konstruktorze rysowanego obiektu. Postanowiłem ustandaryzować co nieco. Tak powstało kilka interfejsów, które staram się implementować wewnątrz klas wykazujących zachwania podobne do tych, które już istnieją w systemie. Ponieważ kod każdego interfejsu jest bardzo krótki, to mogę zamieścić je wszystkie poniżej:

class ClickableInterface {

public:

    virtual void clickLeft()=0;
    virtual void clickRight()=0;
    virtual void doubleClick()=0;

    virtual void isLeftClicked()=0;
    virtual void isRightClicked()=0;
    virtual void isDoubleClick()=0;
};
class DraggableInterface {

public:

    virtual void startDrag()=0;
    virtual void drag()=0;
    virtual void drop()=0;

    virtual bool isDragging()=0;
};
class HoverableInterface {

public:

    virtual void mouseEnter()=0;
    virtual void mouseOver()=0;
    virtual void mouseLeave()=0;

    virtual bool isMouseOver()=0;
};
class RescalableInterface {

public:

    virtual void rescale(std::shared_ptr<Scale> scale)=0;
};
class DrawableInterface {

public:

    virtual void draw(std::shared_ptr<sf::RenderWindow> window)=0;
};
class EventReceiverableInterface {

public:

    virtual void handleEvents(Keyboard& keyboard, Cursor& cursor)=0;
};
class RunnableInterface {

public:

    virtual void start()=0;
    virtual bool isStarted()=0;
};

Stosowanie interfejsów pomaga nie tylko przy tworzeniu nowych funkcjonalności, ale również w refactoringu kodu. Wystarczy, że zmienię deklarację funkcji wewnątrz interfejsu, a kod projektu mi się nie skompiluje, dopóki nie zastosuję zmian wewnątrz wszystkich klas implementujących ten interfejs. Bosko! :)

Bugfixing

Oprócz zmian w architekturze, ponaprawiałem trochę bugów, o których wiedziałem, ale nie miałem chwili by nad nimi przysiąść. Kilka bugów jeszcze zostało, ale poprawek na razie musi wystarczyć tyle, ile jest :) Poniżej wrzucam krótkie filmiki przedstawiające przykłady błędów, które poprawiłem:


Bug1 - przed poprawką:

Bug 1 - before


Bug1 - po poprawce:

Bug 1 - after


Bug2 - przed poprawką:

Bug 2 - before


Bug2 - po poprawce:

Bug 2 - after

Takich błędów było więcej, niektóre z nich wyszły dopiero po zmianach architektury. Mam nadzieję, że w następnej iteracji zrobię upgrade tego mechanizmu, który będzie odporny na wszystkie możliwe bugi ;)

Nowe funkcje easingowe!

Jestem na etapie kolekcjonowania funkcji easingowych :) Znalazłem stronę z nowymi okazami, które z chęcią dołączyłem do mojej biblioteczki implementacyjno-kodowej :). Jak przyjdzie czas na juicing w projekcie, to będę miał jak znalazł! :P


Funkcja Gamma

Implementacja tej funkcji wygląda następująco:

float GammaFunction::getValue(sf::Int32 time) {
    float timePart = (float)time/(float)duration;
    float value = pow(timePart, 1.0f/deviation);
    value *= targetValue-startValue;
    value += startValue;
    return value;
}

Funkcja ta daje wartości takie jak przedstawiłem na wykresie poniżej (w zależności od wartości zmiennej deviation):

Funkcja Gamma


Funkcja Bias

Implementacja tej funkcji wygląda następująco:

float BiasFunction::getValue(sf::Int32 time) {
    float timePart = (float)time/(float)duration;
    float value = pow(timePart, log(deviation)/log(0.5f));
    value *= targetValue-startValue;
    value += startValue;
    return value;
}

Funkcja ta daje wartości takie jak przedstawiłem na wykresie poniżej (w zależności od wartości zmiennej deviation):

Funkcja Bias


Funkcja Gain

Implementacja tej funkcji wygląda następująco:

float GainFunction::getValue(sf::Int32 time) {
    float timePart = (float)time/(float)duration;
    float value;
    if (timePart < 0.5f) {
        value = bias(1-deviation, 2*timePart)/2;
    } else {
        value = 1 - bias(1-deviation, 2 - 2*timePart)/2;
    }
    value *= targetValue-startValue;
    value += startValue;
    return value;
}

float GainFunction::bias(float deviation, float timePart) {
    return pow(timePart, log(deviation)/log(0.5f));
}

Funkcja ta daje wartości takie jak przedstawiłem na wykresie poniżej (w zależności od wartości zmiennej deviation):

Funkcja Gain

Pojawił się zasobnik!

I to jaki! :) A tak serio, obiecałem sobie, że z każdą iteracją będę dorzucał coś, co posunie projekt do przodu. Nie można przecież skupić się na samym kodzie - funkcjonalności też muszą iść do przodu, choćby odrobinkę :)

Nasz zasobnik wygląda następująco:

Na razie jest on całkowicie statyczny, ale już trzeba było chociażby zaimplementować choćby funkcję skalowania, która kosztowała mnie odrobinę czasu. Powiedzcie, nie jest on piękny? :D

Czego nie udało się zrobić?

Niestety, nie wystarczyło mi czasu na najważniejszy element tej iteracji - zmianę mechanizmu klikania, który został przeze mnie zapowiedziany ostatnio. Na szczęście zrobiłem sporo roboty, która powinna pomóc mi zrobić to w miarę bezboleśnie już teraz :)

Plany na następną iterację

Na sam początek pójdzie nowy mechanizm klikania na tilesy. Dochodzi do tego również ściąganie puzelków z panelu i możliwość masowego dodawania ich na planszę. Jeżeli wystarczy mi czasu, to zajmę się tematem animacji, który nie może wyglądać tak, jak to jest w chwili obecnej. Następnym krokiem będzie możliwość ściągania i przemieszczania obiektów typu Figure na planszy. W skrócie:

  1. Nowy mechanizm klikania w puzelki
  2. Nowy mechanizm zarządzania animacjami
  3. Ściąganie i przemieszczanie obiektów figur.

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.