SFML-owe zabawy #8 - Klikaj i zwyciężaj!


2018-10-11, 00:06

Chyba nikt nie ma wątpliwości, że jest to przełomowy moment dla naszego projektu! :) Od teraz, zamiast dumać nad tym, jak to może wyglądać - możemy klikać, tworzyć, działać! :) Zapraszam na ósmą, zwycięską część SFML-owych zabaw.


Uwaga. Wszystko, o czym piszę poniżej znajduje się na branchu iteration-7 w repozytorium projektu, o tutaj: https://github.com/CppPolska/MarioEdit/tree/iteration-7. 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?

Jestem mega zaskoczony tym, co wydarzyło się ostatnio w kontekście Mario::Edit. Nowy mechanizm klikania - dwa dni i jest. Nowy algorytm animacji - dwa dni i jest. Trzeba było tylko znaleźć czas, by do tego przysiąść. Potwierdziła się moja (pozytywna) obawa - teraz na prawdę ruszam z kopyta! :) Wisienką na torcie okazuje się bonusowa ikonka gry, której jeszcze nie planowałem wrzucać.

Pojawiła się ikonka projektu

Drobnym, ale za to jak wyrazistym punktem dzisiejszego dnia okazuje się… zwykła ikonka :) Postanowiłem, że będzie to symbol, od którego rozpocząłem przygodę z Mario::Edit. Patrzie, jak ładnie się prezentuje! :)

Ikonka gry

W SFML-u wstawienie ikony projektu jest na prawdę bardzo proste. Potrzebna jest tylko ścieżka do pliku i rozmiar ikony:

    sf::Image iconImage;
    iconImage.loadFromFile("resources/icon.png");
    window->setIcon(710, 710, iconImage.getPixelsPtr());

Jak widać, do samego okna przekazujemy reprezentację pikseli, tak więc nic nie stoi na przeszkodzie, byśmy mogli w locie modyfikować ikonę poprzez zmiany dokonywane na tym pointerze. Jeżeli to działa tak, jak myślę, to mruganie to kwestia paru chwil.

PS. Pamiętajmy, że jak tworzymy nowe okno (na przykład przy fullscreenie), to dobrze jest ustawić ikonkę ponownie. Popatrzcie w kod - przecież ikonkę ustawiamy per okno! :)

Przeprojektowany mechanizm animacji

A teraz usiądźcie drogie dzieci, opowiem Wam bajkę o wątkach.

Z animacjami to było tak: miałem jeden puzelek, który miał za zadanie ładnie się animować. Stwierdziłem - użyję wątków! Jak zaplanowałem, tak i zrobiłem. Byłem z siebie bardzo zadowolony, ponieważ kod, który napisałem, był stosunkowo ładny. Po opublikowaniu wpisu kilka osób poradziło mi, abym nie robił animacji w osobnym wątku. Myślę sobie - pożyjemy, zobaczymy.

Dorzuciłem easing na puzelku, który również oparłem, podobnie jak animację mrugania, o nowy wątek. Opublikowałem wpis, i znowu komentarze o wątkach - że nie, że po co. Dopiero jeden komentarz na wykopie dał mi do myślenia:

Lubię konstruktywną krytykę, więc poszperałem trochę tu i tam. Przypomniałem sobie, że Wojtek posyłał mi link o wątkach tuż przed publikacją posta z pierwszą animacją. Następnie poszperałem w biblioteczce filmów wujka Gynvaela, i znalazłem cudo wyjaśniające wiele na temat wątków. Swoją wiedzę dopełniłem jeszcze czytając ten wpis. Bogaty o nową wiedzę, stwierdziłem że tak być nie może - trzeba coś z tym zrobić, i to natychmiast.

W następnej iteracji ogarnąłem temat z grubsza, używając mutex-ów. Działać, działało, ale nowo nabyta wiedza na temat pracy z wątkami nie pozwoliła mi tego tak zostawić. Po pierwsze, przy większej ilości puzelków uruchamiałaby się większa liczba wątków. Po drugie, patrząc perspektywistycznie w przyszłość, musiałbym bawić się w zabezpieczanie zasobów (czyli musiałbym dbać o odpowiednią synchronizację wątków), co na pewno byłoby kosztowne czasowo.

Postanowiłem, co też uczyniłem: wszystkie animacje przeniosłem do głównego wątku programu. Przy okazji trochę kodu się uprościło, więc również z tej strony wyszło na plus :)

W tej chwili mam jedną instancję klasy AnimationPerformer, do której dorzucam animacje, które mają się wykonać. Co obrót pętli w głównym wątku programu wykonuję funkcję, która przechodzi przez wszystkie niezakończone animacje:

void AnimationPerformer::process() {
    for (auto it=animations.begin(); it != animations.end();) {
        (*it)->animate();
        if ((*it)->isFinished()) {
            remove(it);
            continue;
        }
        it++;
    }
}

Każda animacja dziedziczy po bazowej klasie Animation, która wymaga utworzenia metody animate(), w której to obliczane są bieżące wartości dla właściwości objętych animacją. Na przykład implementacja animacji po najechaniu kursora na puzelek wygląda następująco:

HighlightAnimation::HighlightAnimation(DynamicTile* tile) : Animation(600, false) {
    this->tile = tile;

    upFunction = std::make_shared<SmoothStepFunction>(200, tile->scalePromotion, 1.8f);
    downFunction = std::make_shared<SmoothStepFunction>(100, 1.8f, 1.5f);
}

void HighlightAnimation::animate() {
    auto animationPointInTime = getAnimationPointInTime();

    if (animationPointInTime < 200) {
        tile->scalePromotion = upFunction->getValue(animationPointInTime);
    } else {
        tile->scalePromotion = downFunction->getValue(animationPointInTime-200);
    }

    tile->snapToCenterPoint();
    tile->correctCorners();
}

Jest kilka niepodpisanych stałych, ale w IDE ładnie widać, co jest czym. Możecie sobie porównać ten kod do poprzedniej wersji, klikając tutaj. Dużo ładniej, nieprawdaż? :)

Jest nowy mechanizm klikania! :)

Mam taką zasadę, że najsmaczniejsze zawsze zostawiam na koniec :) Właśnie zaimplementowałem chyba najważniejszą funkcjonalność edytora! :) W końcu możemy układać puzelki tak, jak chcemy. Cała magia dzieje się w klasie Editor, a dokładniej w dwóch jej metodach: Editor::handleButtonTilesEvents() oraz Editor::handleSceneTilesEvents(). Pierwsza funkcja obsługuje zdarzenia dla puzelków zandujących się na zasobniku, a druga dla puzelków poza zasobnikiem. Było tutaj trochę zabawy, bo sporo przypadków klikania musiało zostać obsłużonych. Jak zaraz zobaczycie, pomyślałem o tym, aby można było w sposób możliwie szybki tworzyć plansze.

Jeżeli ktoś jest ciekawy kodu, to z grubsza wygląda to tak:

void Editor::handleButtonTilesEvents(Keyboard& keyboard, Cursor& cursor) {
    auto buttons = ObjectRegistry::getButtonTiles();
    if (!isDraggingNewTile) {
        for (auto &button: buttons) {
            if (cursor.isOver(button)) {
                doButtonMouseOver(cursor, button);

                if (cursor.isMousePressed()) {
                    doButtonMouseClick(cursor, button);
                    lastUsedTileButton = button;
                    isDraggingNewTile = true;
                    clickedOnTileButton = true;
                }
                break;
            } else if (cursor.isOverRegistered(button)) {
                doButtonMouseOut(cursor, button);
                break;
            }
        }
        return;
    }

    for (auto &button: buttons) {
        if (cursor.isOver(button) && cursor.isMouseReleased()) {
            dismissTileDrop = true;
        } else if (cursor.isOver(button) && cursor.isMousePressed() && lastUsedTileButton != button) {
            cancelDragging(cursor);
        }
    }
}
void Editor::handleSceneTilesEvents(Keyboard& keyboard, Cursor& cursor) {
    if (isDraggingNewTile) {
        bool isLeftClick = cursor.getClickType() == sf::Mouse::Button::Left;
        bool isEscape = keyboard.isPressed(sf::Keyboard::Key::Escape);

        if (cursor.isClick() && cursor.isLongClick() && !clickedOnTileButton) {
            performLongClickDrop(cursor);
        } else if (cursor.isMouseReleased() && isLeftClick) {
            performQuickClickDrop(cursor);
        } else if (!isLeftClick || isEscape) {
            cancelDragging(cursor);
        }
    } else {
        auto tiles = ObjectRegistry::getDynamicTiles();

        for (auto &tile : tiles) {
            performHover(cursor, tile);
        }

        for (auto &tile : tiles) {
            performDragDrop(cursor, tile);
        }
    }
}

Po implementację pomniejszych metod prywatnych zapraszam do repozytorium projektu :)

Bieżący efekt

Zgodnie z przeciekami (udostępnionymi na naszej grupie facebookowej), wszystko ładnie działa :) Poniżej aktualny efekt:

Nowy mechanizm klikania

Już teraz całość prezentuje się genialnie, a to nie koniec pomysłów w mojej głowie! :) Największym sukcesem dzisiejszego dnia jest to, że projekt uzyskał aprobatę u moich małych córeczek, które codziennie przychodzą do mnie i pytają “tato, mogę poklikać?” :)

Czego nie udało się zrobić?

Ciężko tutaj mówić o tym, że czegoś nie udało się zrobić - bo udało się zrobić bardzo wiele. To chyba najlepsza iteracja do tej pory :).

Plany na następną iterację

Nie planuję zbyt daleko - mam zamiar skupić się na mechanizmie klikania związanym z figurami (krzaczki, wzgórza, chmurki). Kto wie, może uda się również zrobić zmiany ich rozmiaru? :)

To tyle na dziś, 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.