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

Notatki programisty: Czas na akcje, czyli piszemy postać gracza w aplikacji Box2D/SFML

@biomenNotatki programisty: Czas na akcje, czyli piszemy postać gracza w aplikacji Box2D/SFML09.01.2017 18:32

W poprzednich wpisach poruszyłem temat graficznego prezentowania ciał Box2D w SFML. Dotychczas operowaliśmy na ciałach statycznych czyli takich które posiadają stałe miejsce w przestrzeni świata fizycznego i na które nie działa żadna siła.. Co prawda wprawialiśmy je w ruch lecz odbywało się to w dość brutalny sposób przy użyciu transformacji. Ruch ten nie był związany z żadną silą fizyczną, np. grawitacją. W tym wpisie zmienimy ten stan rzeczy i poznamy ciała dynamiczne oraz wprawimy je w ruch bardziej „naturalnym” sposobem. Zapraszam do lektury.

Trochę modyfikacji

Pierwszą rzeczą jaką zrobimy jest napisanie nowej funkcji tworzącej ciało statyczne. Możemy tutaj zastosować znaną metodykę programowania „magic of copy-pase” a następnie zmodyfikować przedstawioną w poprzednich wpisach funkcję do tworzenia ciał. Po modyfikacjach, kod nowej funkcji prezentuje się następująco:


b2Body* createStaticBody(
            b2World* world, 
            const double height, 
            const double width)
{
    b2PolygonShape bodyShape;
    bodyShape.SetAsBox(
                (height/2)*G_PIXELS_TO_METERES,
                (width/2)*G_PIXELS_TO_METERES);

    b2FixtureDef bodyFixture;
    bodyFixture.density = 1.f;
    bodyFixture.friction = 0.2f;
    bodyFixture.shape = &bodyShape;

    b2BodyDef bodyDef;
    bodyDef.type = b2_staticBody;

    b2Body* myBody = world->CreateBody(&bodyDef);
    myBody->CreateFixture(&bodyFixture);
    return myBody;
}

Główna modyfikacja polega na tym że wielkość ciała przekazujemy w parametrach funkcji dzięki czemu dostajemy bardziej elastyczną funkcję do tworzenia klocków. Następnym krokiem jest stworzenie ciała które będzie naszą postacią. Modyfikacje w stosunku do dotychczasowych funkcji są dość subtelne. Komentarze wyjaśniają kluczowe zmiany.


b2Body* createPlayerBody(b2World* world)
{
    /* Umownie nasz bohater
     * bedzie kwadratem 40x40 px. */
    b2PolygonShape bodyShape;
    bodyShape.SetAsBox(20*G_PIXELS_TO_METERES, 20*G_PIXELS_TO_METERES);

    b2FixtureDef bodyFixture;
    bodyFixture.density = 0.1f;
    bodyFixture.friction = 0.2f;
    bodyFixture.shape = &bodyShape;

    /* Typ ciala definiujemy w b2BodyDef
     * w przypadku ruchomego obiektu gracza
     * zastosujemy cialo dynamiczne. */
    b2BodyDef bodyDef;
    bodyDef.type = b2_dynamicBody;

    b2Body* myBody = world->CreateBody(&bodyDef);
    myBody->CreateFixture(&bodyFixture);

    /* Domyslnie kazde cialo posiada
     * "naprawiona" rotacje, czyli
     * fizycznie sie nie obraca, w naszym
     * przykladzie chcemy jednak aby cialo
     * bylo bardziej *wyluzowane*. */
    myBody->SetFixedRotation(false);
    return myBody;
}

Ostatnią zmianą jaką wprowadzimy jest zmiana typu std::unique_ptr na std::shared_ptr w klasie DrawablePolygonBody. Zmiana podyktowana jest pewnym ułatwieniem które zaobserwujemy w funkcji głównej. Ale to za chwile, w tym momencie przejdźmy do omówienia sposobu poruszania ciała dynamicznego.

Grunt to akcja

Sposób w jaki wprawimy nasze ciało w ruch będzie następujący: najpierw pobierzemy obecną prędkość ciała, zwiększymy ją a następnie przypiszemy nową zwiększoną prędkość ciału. Jako że kod wyraża więcej niż tysiąc słów, czas się z nim zapoznać:


int main(int argc, char *argv[])
{
    /* Tworzenia swiata. */
    std::unique_ptr<b2World> myWorld(createWorld());

    /* Tekstury. */
    sf::Texture textureMap;
    if(!textureMap.loadFromFile("textureMap.png")){
        std::cout << "textureMap problem \n";
    }
    sf::Texture texturePlayer;
    if(!texturePlayer.loadFromFile("textureHero.png")){
        std::cout << "texturePlayer problem \n";
    }

    /* Ciala naszej mapy. */
    DrawablePolygonBody bodyPlatform(createStaticBody(myWorld.get(), 300, 40));
    bodyPlatform.setPosition(300.f, 450.f);
    bodyPlatform.setTexture(textureMap);

    DrawablePolygonBody bodySecondPlatform(createStaticBody(myWorld.get(), 100, 40));
    bodySecondPlatform.setPosition(600.f, 450.f);
    bodySecondPlatform.setTexture(textureMap);

    /* Cialo bohatera. */
    DrawablePolygonBody myPlayerBody(createPlayerBody(myWorld.get()));
    myPlayerBody.setTexture(texturePlayer);
    myPlayerBody.setPosition(300.f, 50.f);

    /* Dla latwiejszego rysowania wszystkie
     * obiekty dodajemy do wektora. */
    std::vector<DrawablePolygonBody> listWorldBodies;
    listWorldBodies.push_back(bodyPlatform);
    listWorldBodies.push_back(bodySecondPlatform);
    listWorldBodies.push_back(myPlayerBody);

    /* Okno SFML. */
    sf::RenderWindow window(
                sf::VideoMode(800, 600, 32),
                std::string("Box2d - SFML"),
                sf::Style::Default);
    window.setFramerateLimit(60);

    /* Petla glowna. */
    while(window.isOpen())
    {
        /* Zdarzenia. */
        sf::Event myEvent;
        while(window.pollEvent(myEvent))
        {
            if(myEvent.type == sf::Event::Closed){
                window.close();
            }

            /* Poruszanie cialem bohatera. */
            if(sf::Keyboard::isKeyPressed(sf::Keyboard::Space))
            {
                const b2Vec2 oldVelocity = myPlayerBody.getBody()->GetLinearVelocity();
                const b2Vec2 newVelocity = b2Vec2(oldVelocity.x, -5.25f);

                myPlayerBody.getBody()->SetLinearVelocity(newVelocity);
            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::D))
            {
                const b2Vec2 oldVelocity = myPlayerBody.getBody()->GetLinearVelocity();
                const b2Vec2 newVelocity = b2Vec2(oldVelocity.x + 0.12f, oldVelocity.y);

                myPlayerBody.getBody()->SetLinearVelocity(newVelocity);
            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::A))
            {
                const b2Vec2 oldVelocity = myPlayerBody.getBody()->GetLinearVelocity();
                const b2Vec2 newVelocity = b2Vec2(oldVelocity.x - 0.12f, oldVelocity.y);

                myPlayerBody.getBody()->SetLinearVelocity(newVelocity);
            }
        }

        /* Logika. */
        beforeGameLoop(*myWorld.get());
        myPlayerBody.update();

        /* Render. */
        window.clear(sf::Color::Black);

        for(DrawablePolygonBody& item : listWorldBodies){
            item.render(window);
        }

        window.display();
    }

    return 0;
}

Główną zmianą w kodzie jaką możemy zaobserwować jest część w sekcji obsługi zdarzeń. Zgodnie z powyżej przedstawionym założeniem dotyczącym wprawiania obiektów w ruch, pobieramy prędkość, modyfikujemy w zależności od potrzeb i akceptujemy. W tym przypadku przydaje się nam furtka jaką sobie zostawiliśmy w postaci funkcji getBody(). Wszystkie obiekty które rysujemy dodajemy do obiektu std::vector dzięki czemu w pętli głównej renderowanie odbywa się za pomocą prostej pętli for. Gdybyśmy w klasie DrawablePolygonBody zostawili typ std::unique_ptr operacja dodania obiektu do wektora by się nie powiodła. Dodatkowo, ustawiliśmy limit klatek na sekundę, okna na 60 by nasza symulacja nie przebiegała zbyt szybko. Po uruchomieniu programu, powita nas poniżej przedstawiony widok. Aby zobaczyć rezultat do czego nam się przydało wyłączenie „naprawionej” rotacji, wystarczy podjechać do krawędzi platformy i zobaczyć jak ciało będzie reagować.

Więcej elegancji

Aby zwiększyć czytelność naszego kodu, napiszemy prostą klasę opakowującą funkcjonalność ruchu która będzie dekoratorem ciała Box2D. Dzięki temu zyskamy wcześniej wspomnianą przejrzystość kodu oraz klasę która z powodzeniem będzie mogła nam w przyszłości posłużyć za dekorator dla ciał które będą naszymi przeciwnikami. Interfejs i implementacja przedstawia się następująco.


class BodyMover
{
public:
    enum Direction : int
    {
        Right,
        Left,
        Jump
    };

    BodyMover(b2Body* baseBody);

    void move(Direction moveDir);
    void setJumpForce(const float newBodySpeed);
    void setMoveSpeedChangeValue(const float newBodySpeed);

private:
    b2Body* m_body;
    float m_bodySpeedChangeValue;
    float m_bodyJumpForce;
};

BodyMover::BodyMover(b2Body* baseBody) :
    m_body(baseBody),
    m_bodySpeedChangeValue(0.12f),
    m_bodyJumpForce(5.25f) {}

void BodyMover::setMoveSpeedChangeValue(const float newBodySpeed)
{
    this->m_bodySpeedChangeValue = newBodySpeed;
}

void BodyMover::setJumpForce(const float newBodySpeed)
{
    this->m_bodyJumpForce = newBodySpeed;
}

void BodyMover::move(Direction moveDir)
{
    switch(moveDir)
    {
        case Direction::Right:
        {
            const b2Vec2 oldVelocity = m_body->GetLinearVelocity();
            const b2Vec2 newVelocity = b2Vec2(oldVelocity.x + m_bodySpeedChangeValue, oldVelocity.y);

            m_body->SetLinearVelocity(newVelocity);

            break;
        }
        case Direction::Left:
        {
            const b2Vec2 oldVelocity = m_body->GetLinearVelocity();
            const b2Vec2 newVelocity = b2Vec2(oldVelocity.x - m_bodySpeedChangeValue, oldVelocity.y);

            m_body->SetLinearVelocity(newVelocity);

            break;
        }
        case Direction::Jump:
        {
            const b2Vec2 oldVelocity = m_body->GetLinearVelocity();
            const b2Vec2 newVelocity = b2Vec2(oldVelocity.x, - m_bodyJumpForce);

            m_body->SetLinearVelocity(newVelocity);

            break;
        }
    }
}

Użycie jej jest bardzo proste i myślę że nie wymaga większego komentarza.Nasza funkcja główna od razu zyskuje na atrakcyjności:


int main(int argc, char *argv[])
{
    /* Tworzenia swiata. */
    std::unique_ptr<b2World> myWorld(createWorld());

    /* Tekstury. */
    sf::Texture textureMap;
    if(!textureMap.loadFromFile("textureMap.png")){
        std::cout << "textureMap problem \n";
    }
    sf::Texture texturePlayer;
    if(!texturePlayer.loadFromFile("textureHero.png")){
        std::cout << "texturePlayer problem \n";
    }

    /* Obiekty mapy. */
    DrawablePolygonBody bodyPlatform(createStaticBody(myWorld.get(), 300, 40));
    bodyPlatform.setPosition(300.f, 450.f);
    bodyPlatform.setTexture(textureMap);

    DrawablePolygonBody bodySecondPlatform(createStaticBody(myWorld.get(), 100, 40));
    bodySecondPlatform.setPosition(600.f, 450.f);
    bodySecondPlatform.setTexture(textureMap);

    /* Obiekt gracza i jego *poruszacza*. */
    DrawablePolygonBody myPlayerBody(createPlayerBody(myWorld.get()));
    myPlayerBody.setTexture(texturePlayer);
    myPlayerBody.setPosition(300.f, 50.f);

    BodyMover playerMove(myPlayerBody.getBody());

    /* Ewidencja cial. */
    std::vector<DrawablePolygonBody> listWorldBodies;
    listWorldBodies.push_back(bodyPlatform);
    listWorldBodies.push_back(bodySecondPlatform);
    listWorldBodies.push_back(myPlayerBody);

    /* Okno SFML. */
    sf::RenderWindow window(
                sf::VideoMode(800, 600, 32),
                std::string("Box2d - SFML"),
                sf::Style::Default);
    window.setFramerateLimit(60);

    /* Petla glowna. */
    while(window.isOpen())
    {
        /* Zdarzenia. */
        sf::Event myEvent;
        while(window.pollEvent(myEvent))
        {
            if(myEvent.type == sf::Event::Closed){
                window.close();
            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::Space))
            {
                playerMove.move(BodyMover::Direction::Jump);
            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::D))
            {
                playerMove.move(BodyMover::Direction::Right);
            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::A))
            {
                playerMove.move(BodyMover::Direction::Left);
            }
        }

        /* Logika. */
        beforeGameLoop(*myWorld.get());
        myPlayerBody.update();

        /* Render. */
        window.clear(sf::Color::Black);

        for(DrawablePolygonBody& item : listWorldBodies){
            item.render(window);
        }

        window.display();
    }

    return 0;
}

Na zakończenie

Po tym wpisie można z pełną stanowczością stwierdzić że w naszej aplikacji zaczęło się coś dziać. Co prawda skakanie naszego ciała nie jest jeszcze doskonałe i nasz bohater może odbić się w górę z powietrza ale mamy już solidną bazę do dalszego rzeźbienia rozgrywki naszej aplikacji-gry. Oczywiście zapraszam do samodzielnego sprawdzenia kodu.

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.