Blog (35)
Komentarze (574)
Recenzje (0)
@biomenNotatki programisty: Czuj się Disneyem, czyli o animacji słów kilka

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

26.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:

613773

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.

613782

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:

613793

Jak zawsze, dzięki za uwagę!

Wybrane dla Ciebie
Komentarze (0)