Blog (35)
Komentarze (574)
Recenzje (0)

Notatki programisty: Czuj się Disneyem, czyli o animacji słów kilka

@biomenNotatki programisty: Czuj się Disneyem, czyli o animacji słów kilka26.01.2017 10:03

Niniejszy wpis zaczniemy od omówienia tego jak działają animowane sprite’y w grach. Przechodząc do sedna sprawy: mechanizm animacji w prostych grach 2D polega na tym samym co i w kreskówkach. Cały sekret i magia. ;) Mamy animacje która podzielona jest na pojedyncze klatki które zmieniane są co określony przedział czasowy. W przypadku aplikacji, animację przeważnie zapisujemy do tekstury która później w kodzie jest odpowiednio obsługiwana. W niniejszym wpisie zaprezentuje jak będzie wyglądać nasza klasa do obsługi takiej tekstury. Plik graficzny prezentuje się jak poniżej:

Jak widać na powyższym obrazku, w jednym pliku zapisaliśmy więcej niż jedną animację. Oczywiście moglibyśmy każdą animację zapisywać w oddzielnych plikach graficznych aczkolwiek takie rozwiązanie, moim zdaniem, jest bardzo pracochłonne. Oto jak będzie wyglądać nasza klasa do obsługi tekstury:


class Animator
{
public:
    Animator(
        sf::Shape* shape,
        sf::Vector2f frameSize);

	void work();
    void work(double externalIterator);

    void setAnimation(const int index);
    void setAnimationSpeed(const double newSpeed);

    sf::Shape* getShape() const;

private:
    double m_animationSpeed;
    double m_frameIterator;

    int m_selectedAnimation;
    int m_frameCount;
    int m_animationCount;

    sf::Vector2f m_frameSize;
    sf::Shape* m_shape;

    void updateShape(const int multiplier);
};

I implementacja:


Animator::Animator(
        sf::Shape* shapeObj,
        sf::Vector2f frameSize) :
            m_animationSpeed(1),
            m_frameIterator(0.0),
            m_selectedAnimation(0),
            m_frameCount(0),
            m_animationCount(0),
            m_frameSize(frameSize),
            m_shape(shapeObj)
{
    const sf::Texture* shapeTexture = m_shape->getTexture();
    if (shapeTexture != nullptr)
    {
        const sf::Vector2u textureSize = shapeTexture->getSize();
        m_frameCount = textureSize.x / m_frameSize.x;
        m_animationCount = textureSize.y / m_frameSize.y;
        std::cout
            << "m_frameCount: "
            << m_frameCount
            << " m_animationCount: "
            << m_animationCount
            << "\n";

    } else {
        std::cout
            << __func__
            <<" -Texture of shape is NULL\n";
    }

}

void Animator::work()
{
    const int multiplier =
            (int)(m_frameIterator / this->m_frameCount);

    if (multiplier < this->m_frameCount)
	{
        updateShape(multiplier);

    } else {
        m_frameIterator = 0;
	}
    m_frameIterator += m_animationSpeed;
}

void Animator::work(double externalIterator)
{
    const int multiplier =
            (int)(externalIterator / this->m_frameCount);

    if (multiplier < this->m_frameCount)
	{
        updateShape(multiplier);

    } else {
		externalIterator = 0;
	}
    externalIterator += m_animationSpeed;
}

void Animator::updateShape(const int multiplier)
{
    m_shape->setTextureRect(
                    sf::IntRect(
                        multiplier * m_frameSize.x,
                        m_selectedAnimation*m_frameSize.y,
                        m_frameSize.x,
                        m_frameSize.y));
}

void Animator::setAnimation(const int index)
{
    if(index < m_animationCount){
        this->m_selectedAnimation = index;
    } else {
        std::cout << __func__ << " -index to hight";
    }
}

void Animator::setAnimationSpeed(const double newSpeed)
{
    if(newSpeed > 0){
        this->m_animationSpeed = newSpeed;
    } else {
        std::cout << __func__ << " -newSpeed to hight";
    }
}

sf::Shape* Animator::getShape() const
{
    return this->m_shape;
}

Mając przed oczyma szczegóły implementacji możemy przejść do objaśnienia jak klasa ma działać. Mianowicie będziemy posiadać obiekt graficzny z biblioteki SFML który będzie posiadać wyżej wspomnianą teksturę a który jednocześnie będzie pełnił roli powierzchni na której wyświetlana jest animacja. Coś jak ekran w starym kinie. Zadaniem naszej klasy będzie przycinanie tekstury i wyświetlanie odpowiedniej klatki we właściwym momencie. Przedział czasowy między jedną a drugą klatką będzie determinowany przez wyliczaną zmienną multiplier. Oczywiście można to przerobić na używanie struktury time_t aczkolwiek na chwilę obecną proponowane rozwiązanie okaże się dla nas wystarczające.

Do działania naszej klasy potrzebujemy: wyżej wymienionego obiektu przestrzeni jak i informacji jakiej wielkości jest pojedyncza klatka/ramka animacji. W przypadku naszego pliku rozmiar pojedynczej klatki wynosi 104 na 150 pikseli. Klasa posiada dość „sprytny” mechanizm. Mianowicie na podstawie informacji o wielkości pojedynczej klatki i wielkości tekstury, potrafi ona określić z ilu klatek składa się nasza animacja oraz to, ile w naszym pliku tych animacji się znajduje. Dzięki czemu mamy do dyspozycji mechanizm przełączania animacji, indeksowanych od zera w górę. Kod funkcji głównej prezentuje się następująco:


int main()
{
    sf::RenderWindow window(
                sf::VideoMode(500, 500, 32),
                std::string("Animator Demo"),
                sf::Style::Close);

    sf::Texture playerTexture;
    playerTexture.loadFromFile("walk.png");

    sf::Vector2f oneFrameSize(104.f, 150.f);

    sf::RectangleShape shapeForAnimation;
    shapeForAnimation.setSize(sf::Vector2f(200, 250.f));
    shapeForAnimation.setOrigin(
            shapeForAnimation.getSize().x/2,
            shapeForAnimation.getSize().y/2);
    shapeForAnimation.setPosition(sf::Vector2f(250, 250.f));
    shapeForAnimation.setTexture(&playerTexture);

    Animator animator(
                &shapeForAnimation,
                oneFrameSize);

    FpsStabilizer stabilizer(60);

    double animationSpeed = 1;
    while (window.isOpen())
    {
        stabilizer.work();
        animator.work();

        sf::Event eventObj;
        while (window.pollEvent(eventObj))
        {
            if (eventObj.type == sf::Event::Closed) {
                window.close();
            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::E)){
                animator.setAnimation(0);
            }
            if(sf::Keyboard::isKeyPressed(sf::Keyboard::Q)){
                animator.setAnimation(1);
            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::A)){
                animationSpeed += 0.05;
                animator.setAnimationSpeed(animationSpeed);
            }
            if(sf::Keyboard::isKeyPressed(sf::Keyboard::D)){
                animationSpeed -= 0.05;
                animator.setAnimationSpeed(animationSpeed);
            }
        }

        window.clear();

        window.draw(*animator.getShape());

        window.display();
    }
    return 0;
}

Wszystko o czym pisałem wyżej zostało wykorzystane w kodzie. Tworzymy obiekt graficzny będącym płótnem na naszą animację, ładujemy teksturę, tworzymy animatora i viola, efekt wygląda tak jak poniżej. Oczywiście statyczny obrazek nie oddaje „skoku jakości” jakiego doświadczymy w przypadku dodania obsługi animacji do naszego dema.

Integracja z aplikacją

Klasę animującą tekstury mamy gotową, czas na zintegrowanie rozwiązania z istniejącą aplikacją SFML/Box2D. Dotychczas proces tworzenia obiektu renderującego wyglądał tak: mamy ciało fizyczne i na jego podstawie, generujemy obiekt do renderowania. Teraz sytuacja delikatnie się komplikuje gdyż obiekt graficzny który będzie reprezentować ciało definiujemy w miejscu tworzenia animatora. Dodatkowo, do tej pory mamy już napisany spory kawałek kodu stąd chcemy aby nasze nowe rozwiązanie było kompatybilne z dotychczasowymi osiągnięciami. Po kilku przemyśleniach stwierdziłem że najlepszym rozwiązaniem będzie:

  1. Zmiana nazwy DrawablePolygonBody na DrawableBody (śmieszna ale istotna zmiana) i zrobienie z niej klasy abstrakcyjnej.
  2. Utworzenie dwóch klas pochodnych od DrawableBody: DrawableBodyGenerated i DrawableBodyAnimated, pierwsza będzie przechowywać mechanizm używany dotychczas w przykładach, druga zaś będzie korzystać z nowej klasy animatora.
  3. Zmiana nazwy funkcji getRenderShape() na getRenderBody() w klasie IdentifiedBody.

Oto jak będą wyglądać deklaracje naszych nowych klas:



class DrawableBody
{
    
protected:
    const int METERES_TO_PIXELS = 50;
    const float PIXELS_TO_METERES = 0.02f;

    const float RADIANS_TO_PIXELS = 180 / b2_pi;
    const float PIXELS_TO_RADIANS = b2_pi / 180;

public:
    DrawableBody(b2Body* baseBody);

    virtual void render(sf::RenderWindow& window) const = 0;

    void setColor(sf::Color newColor);
    void setRotate(const float32 angle);
    void setTexture(sf::Texture& texture);
    void setPosition(const float32 x, const float32 y);

    void setVisable(const bool value);
    bool isVisable() const;

    float   getRotate() const;
    b2Body* getBody();
    sf::Vector2f getPosition() const;

    void update();

protected:
    bool m_isVisable;
    b2Body* m_bodyPtr;

    std::shared_ptr<sf::Shape> m_renderObj;

    void synchronize(sf::Shape* view, b2Body* model) const;
};

class DrawableBodyGenerated : public DrawableBody
{
public:
    DrawableBodyGenerated(b2Body* baseBody);

    void render(sf::RenderWindow& window) const;

private:
    sf::ConvexShape* generateView(b2Body* body) const;
};

class DrawableBodyAnimated : public DrawableBody
{
public:
    DrawableBodyAnimated(
            b2Body* baseBody,
            Animator* animator);

    void render(sf::RenderWindow &window) const;

    Animator* getAnimator() const;

private:
    std::shared_ptr<Animator> m_animator;

};

I implementacja klas pochodnych:


DrawableBodyAnimated::DrawableBodyAnimated(
        b2Body* baseBody,
        Animator* animator)
            : DrawableBody(baseBody), m_animator(animator)
{
    m_renderObj.reset(m_animator->getShape());
}

void
DrawableBodyAnimated::render(sf::RenderWindow &window) const
{
    if(isVisable()){
        m_animator->work();
        window.draw(*m_animator->getShape());
    }
}

Animator*
DrawableBodyAnimated::getAnimator() const
{
    return m_animator.get();
}

DrawableBodyGenerated::DrawableBodyGenerated(b2Body* baseBody):
    DrawableBody(baseBody)
{
    m_renderObj =
            std::make_shared<sf::ConvexShape>(
                        *generateView(m_bodyPtr));

}

void
DrawableBodyGenerated::render(sf::RenderWindow& window) const
{
    if(isVisable()){
        window.draw(*m_renderObj.get());
    }
}

sf::ConvexShape*
DrawableBodyGenerated::generateView(b2Body* body) const
{

    sf::ConvexShape* bodyView = new sf::ConvexShape();
    bodyView->setFillColor(sf::Color::White);

    for (b2Fixture* fixturePtr = body->GetFixtureList();
            fixturePtr != nullptr; fixturePtr = fixturePtr->GetNext())
    {

        b2Shape* shapeBuffer = fixturePtr->GetShape();
        if(shapeBuffer->m_type == b2Shape::Type::e_polygon)
        {
            b2PolygonShape* realBodyShape = static_cast<b2PolygonShape*>(shapeBuffer);

            const int vertexCount = realBodyShape->GetVertexCount();
            bodyView->setPointCount(vertexCount);

            for (int i = 0; i < vertexCount; ++i)
            {
                b2Vec2 currVerts = realBodyShape->GetVertex(i);

                float posX = currVerts.x * METERES_TO_PIXELS;
                float posY = currVerts.y * METERES_TO_PIXELS;

                bodyView->setPoint(i, sf::Vector2f(posX, posY));
            }

            bodyView->setOrigin(0, 0);
        }
    }

    return bodyView;
}

Ot proste wydzielenie konkretnej funkcjonalności i przeniesienie ich do nowych klas. Dodatkowo DrawableBody jest teraz abstrakcyjną przez co musimy zmodyfikować funkcje createArea() w której to używaliśmy DrawablePolygonBody/DrawableBody tak aby od teraz używać w niej nowej klasy DrawableBodyGenerated. Funkcja startowa naszego przykładu będzie wyglądać tak:


int startExample()
{
    sf::RenderWindow* windowItem =
                        new sf::RenderWindow(
                            sf::VideoMode(800, 600, 32),
                            std::string("SFML/Box2D - tech demo"),
                            sf::Style::Default);

    std::shared_ptr<sf::RenderWindow> window(windowItem);
    std::shared_ptr<ControlKeys>playerControl(new ControlKeys());

    FpsStabilizer stabilizer(60);

    /* Map section. */
    ContactDetector myContactLister;
    std::unique_ptr<b2World> myWorld(createWorld());
    myWorld.get()->SetContactListener(&myContactLister);

    Assets::Resources resources("data.zip");

    std::vector<IdentifiedBody> listWorldBodies;

    createArea(myWorld.get(), listWorldBodies, resources);

    /* Player item. */
    sf::Texture playerTexture;
    if(!playerTexture.loadFromFile("walk.png")){
        std::cout << __func__ << " -walki.png problem\n";
    }

    sf::Vector2f oneFrameSize(104.f, 150.f);
    sf::Vector2f playerBodySize(75.f, 100.f);

    sf::RectangleShape shapeForAnimation;
    shapeForAnimation.setSize(playerBodySize);
    shapeForAnimation.setOrigin(
            shapeForAnimation.getSize().x/2,
            shapeForAnimation.getSize().y/2);
    shapeForAnimation.setPosition(sf::Vector2f(250, 250.f));
    shapeForAnimation.setTexture(&playerTexture);

    MovableBody playerItem(
                    new DrawableBodyAnimated(
                        createDynamicBody(
                            myWorld.get(),
                            playerBodySize.x,
                            playerBodySize.y),
                        new Animator(
                                &shapeForAnimation,
                                oneFrameSize)),
                BodyUserData::Type::Player);

    playerItem.getRenderBody()->setPosition(400.f, 10.f);
    playerItem.getMover()->setJumpForce(4.5f);

    /* Enemy A. */
    MovableBody enemyItem(
                new DrawableBodyGenerated(
                        createDynamicBody(myWorld.get(), 90, 60)),
                BodyUserData::Type::Enemy);

    enemyItem.getRenderBody()->setTexture(
                *resources.getTexture(Assets::Textures::Enemy));

    enemyItem.getRenderBody()->setPosition(600.f, 50.f);
    enemyItem.getMover()->setJumpForce(3.f);
    enemyItem.getMover()->setMaxSpeed(2.5f);

    /* Enemy B. */
    MovableBody enemyItemB(
                new DrawableBodyGenerated(
                        createDynamicBody(myWorld.get(), 90, 60)),
                BodyUserData::Type::Enemy);

    enemyItemB.getRenderBody()->setTexture(
                *resources.getTexture(Assets::Textures::Enemy));

    enemyItemB.getRenderBody()->setPosition(150.f, 50.f);
    enemyItemB.getMover()->setJumpForce(3.f);
    enemyItemB.getMover()->setMaxSpeed(2.5f);

    /* Ewidencja wrogow. */
    std::vector<MovableBody> listEnemies;
    listEnemies.push_back(enemyItem);
    listEnemies.push_back(enemyItemB);

    sf::Color backgroundColor;
    while(window->isOpen())
    {

        /* OTHER */
        stabilizer.work();
        backgroundColor = sf::Color::Black;
        for(MovableBody& item : listEnemies){
            item.getRenderBody()->setColor(sf::Color::White);
        }

        /* EVENTS */
        sf::Event myEvent;
        while(window->pollEvent(myEvent))
        {
            if(myEvent.type == sf::Event::Closed){
                window->close();
            }
        }

        if(sf::Keyboard::isKeyPressed(playerControl->MOVE_JUMP))
        {
            playerItem.getMover()->move(
                        BodyMover::Direction::Jump);
        }

        if(sf::Keyboard::isKeyPressed(playerControl->MOVE_RIGHT))
        {
            playerItem.getMover()->move(
                        BodyMover::Direction::Right);

            DrawableBodyAnimated* ptr =
                    (DrawableBodyAnimated*)playerItem.getRenderBody();

            ptr->getAnimator()->setAnimation(0);

        }

        if(sf::Keyboard::isKeyPressed(playerControl->MOVE_LEFT))
        {
            playerItem.getMover()->move(
                        BodyMover::Direction::Left);

            DrawableBodyAnimated* ptr =
                    (DrawableBodyAnimated*)playerItem.getRenderBody();

            ptr->getAnimator()->setAnimation(1);
        }

        /* BOX2D */
        beforeGameLoop(*myWorld.get());

        for(auto& enemyItem : listEnemies)
        {
            break;
        }

        const bool contactCondition =
                (!myContactLister.isContactListIsEmpty()) &&
                    myContactLister.isContactListContains(
                        ContactDetector::Contact::Type::PlayerTouchEnemy);

        if(contactCondition)
        {
            std::vector<ContactDetector::Contact::Info> enemyContacts =
                    myContactLister.getContactList(
                        ContactDetector::Contact::Type::PlayerTouchEnemy);

            if(!enemyContacts.empty()){

                for(auto& contact : enemyContacts)
                {
                    break;
                }
            }
        }

        /* RENDER */
        window->clear(backgroundColor);

        playerItem.getRenderBody()->update();
        playerItem.getRenderBody()->render(*window);

        for(auto& item : listEnemies){
            item.getRenderBody()->update();
            item.getRenderBody()->render(*window);
        }

        for(auto& item : listWorldBodies){
            item.getRenderBody()->render(*window);
        }

        window->display();
    }
    return 0;
}

Po analizie nowych klas i poprzedniego przykładu duża cześć kodu powinna być zrozumiała. Jako że to pierwsze podejście integracji mechanizmów: plik tekstury na "żywcem" pobieramy z pliku a nie przez Assets::Resources. Co może budzić pewne zastrzeżenie to sposób w jaki pobieramy obiekt animatora. W ciemno zakładamy że obiekt DrawableBody jaki zawarty jest w playerItem jest na pewno typu DrawableBodyAnimated. Niestety żaden intaceof nas w tej sytuacji nie poratuje. Dodatkowo animacje zmieniamy też z dużym kredytem zaufania gdyż nie wiemy jaka animacja kryje się pod indeksem 1 czy 0. W tym miejscu pojawia się wymóg unifikowania i świadomego wykorzystania plików tekstur do animacji. Jeżeli w tekście nie zapomniałem o czymś wspomnieć, efekt naszej aplikacji powinien wyglądać następująco:

Jak zawsze, dzięki za uwagę!

Szanowna Użytkowniczko! Szanowny Użytkowniku!
×
Aby dalej móc dostarczać coraz lepsze materiały redakcyjne i udostępniać coraz lepsze usługi, potrzebujemy zgody na dopasowanie treści marketingowych do Twojego zachowania. Twoje dane są u nas bezpieczne, a zgodę możesz wycofać w każdej chwili na podstronie polityka prywatności.

Kliknij "PRZECHODZĘ DO SERWISU" lub na symbol "X" w górnym rogu tej planszy, jeżeli zgadzasz się na przetwarzanie przez Wirtualną Polskę i naszych Zaufanych Partnerów Twoich danych osobowych, zbieranych w ramach korzystania przez Ciebie z usług, portali i serwisów internetowych Wirtualnej Polski (w tym danych zapisywanych w plikach cookies) w celach marketingowych realizowanych na zlecenie naszych Zaufanych Partnerów. Jeśli nie zgadzasz się na przetwarzanie Twoich danych osobowych skorzystaj z ustawień w polityce prywatności. Zgoda jest dobrowolna i możesz ją w dowolnym momencie wycofać zmieniając ustawienia w polityce prywatności (w której znajdziesz odpowiedzi na wszystkie pytania związane z przetwarzaniem Twoich danych osobowych).

Od 25 maja 2018 roku obowiązuje Rozporządzenie Parlamentu Europejskiego i Rady (UE) 2016/679 (określane jako "RODO"). W związku z tym chcielibyśmy poinformować o przetwarzaniu Twoich danych oraz zasadach, na jakich odbywa się to po dniu 25 maja 2018 roku.

Kto będzie administratorem Twoich danych?

Administratorami Twoich danych będzie Wirtualna Polska Media Spółka Akcyjna z siedzibą w Warszawie, oraz pozostałe spółki z grupy Wirtualna Polska, jak również nasi Zaufani Partnerzy, z którymi stale współpracujemy. Szczegółowe informacje dotyczące administratorów znajdują się w polityce prywatności.

O jakich danych mówimy?

Chodzi o dane osobowe, które są zbierane w ramach korzystania przez Ciebie z naszych usług, portali i serwisów internetowych udostępnianych przez Wirtualną Polskę, w tym zapisywanych w plikach cookies, które są instalowane na naszych stronach przez Wirtualną Polskę oraz naszych Zaufanych Partnerów.

Dlaczego chcemy przetwarzać Twoje dane?

Przetwarzamy je dostarczać coraz lepsze materiały redakcyjne, dopasować ich tematykę do Twoich zainteresowań, tworzyć portale i serwisy internetowe, z których będziesz korzystać z przyjemnością, zapewniać większe bezpieczeństwo usług, udoskonalać nasze usługi i maksymalnie dopasować je do Twoich zainteresowań, pokazywać reklamy dopasowane do Twoich potrzeb. Szczegółowe informacje dotyczące celów przetwarzania Twoich danych znajdują się w polityce prywatności.

Komu możemy przekazać dane?

Twoje dane możemy przekazywać podmiotom przetwarzającym je na nasze zlecenie oraz podmiotom uprawnionym do uzyskania danych na podstawie obowiązującego prawa – oczywiście tylko, gdy wystąpią z żądaniem w oparciu o stosowną podstawę prawną.

Jakie masz prawa w stosunku do Twoich danych?

Masz prawo żądania dostępu, sprostowania, usunięcia lub ograniczenia przetwarzania danych. Możesz wycofać zgodę na przetwarzanie, zgłosić sprzeciw oraz skorzystać z innych praw wymienionych szczegółowo w polityce prywatności.

Jakie są podstawy prawne przetwarzania Twoich danych?

Podstawą prawną przetwarzania Twoich danych w celu świadczenia usług jest niezbędność do wykonania umów o ich świadczenie (tymi umowami są zazwyczaj regulaminy). Podstawą prawną przetwarzania danych w celu pomiarów statystycznych i marketingu własnego administratorów jest tzw. uzasadniony interes administratora. Przetwarzanie Twoich danych w celach marketingowych realizowanych przez Wirtualną Polskę na zlecenie Zaufanych Partnerów i bezpośrednio przez Zaufanych Partnerów będzie odbywać się na podstawie Twojej dobrowolnej zgody.