Blog (35)
Komentarze (574)
Recenzje (0)
@biomenNotatki programisty: Czas na akcje, czyli piszemy postać gracza w aplikacji Box2D/SFML

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

09.01.2017 | aktual.: 09.01.2017 19:04

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ć.

612626

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ę.

Wybrane dla Ciebie
Komentarze (4)