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

Notatki programisty: Wrapper na ciało Box2D i render SFML

@biomenNotatki programisty: Wrapper na ciało Box2D i render SFML08.01.2017 13:46

W poniższym wpisie chciałbym rozwinąć myśl techniczną ze swojego poprzedniego wpisu dotyczącego tworzenia reprezentacji graficznej dla ciała Box2D. Nie przedłużając, zapraszam do lektury.

Zanim zaczniemy

Jak można zauważyć w listingach poprzedniego wpisu, kod miał charakter strukturalny. Celem tego wpisu będzie opakowanie ciała Box2D i funkcjonalności SFML w jedną elegancką klasę. Dodatkowo dla poprawienia czytelności, ujednolicimy jednostki którymi się posługujemy. Myślę że w grafice komputerowej o wiele łatwiej operuje się na pikselach i stopniach stąd właśnie w stronę tych jednostek skierujemy nasze oczy. Dla poprawy efektu wizualnego dorzucimy jakąś teksturę. Nie przeciągając, oto jak będzie wyglądać interfejs naszej klasy:


/* drawable_polygon_body.h */
class DrawablePolygonBody
{
private:
    /* Stale przelicznikowe.*/
    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:
    DrawablePolygonBody(b2Body* baseBody);

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

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

    b2Body* getBody();

    double getRotate() const;

private:
    b2Body* m_bodyPtr;
    std::unique_ptr<sf::Shape> m_renderObj;

    sf::ConvexShape* generateView(b2Body* body) const;
    void synchronize(sf::Shape* view, b2Body* model) const;

};

Słowem komentarza: dobrą praktyką programowania jest to aby klasy z których jest zbudowany nasz program, były jak najmniej od siebie zależne a całość była, najlepiej, złożona z niezależnych modułów. Stosowanie tej wskazówki w praktyce sprawia że nasze programy są łatwiejsze w modyfikacji. Kierując się tą myślą warto było przerzucić stałe przelicznikowe do klasy aby uniezależnić ją od innych części programu. Dodatkowo pojawiły się dwie stałe służące do przeliczania pikseli na radiany i w drugą stronę. Co więcej chcemy sobie zostawić opcje operowania na czystym ciele Box2D do np. sprawdzania jego kolizji, stąd funkcja zwracająca wskaźnik na ciało fizyczne. Jak wspomniałem chcemy ujednolicić jednostki stąd tworzymy dodatkową warstwę na istniejące funkcje zmieniania pozycji i rotacji. Jako że lenistwo to wrodzona cecha każdego programisty ;) chciałem w jak najmniejszym stopniu modyfikować kod z poprzedniego przykładu. Przez co funkcje generateView() i synchronize() mają taki a nie inny wygląd. Oczywiście można je z powodzeniem przerobić na to aby korzystały ze składowych klasy, ale po co jak to wszystko odbywa się pod maską i końcowym użyciu nie będzie tego widać? ;) Funkcji render() i setTexture() nie trzeba tłumaczyć, ich nazwy są dość wymowne. Całość klasy opakowującej ma charakter takiego „dekoratora-fasady”. Mając zarys tego jak wygląda nasz interfejs, czas przejść do implementacji.


/* drawable_polygon_body.cpp */
DrawablePolygonBody::DrawablePolygonBody(b2Body* baseBody) :
    m_bodyPtr(baseBody),
    m_renderObj(generateView(m_bodyPtr))
{

}

double DrawablePolygonBody::getRotate() const
{
    return m_bodyPtr->GetAngle() * RADIANS_TO_PIXELS;
}

void DrawablePolygonBody::setTexture(sf::Texture& texture)
{
    this->m_renderObj->setTexture(&texture);
}

void DrawablePolygonBody::setPosition(const float32 x, const float32 y)
{
    m_bodyPtr->SetTransform(
                b2Vec2(
                    x*PIXELS_TO_METERES,
                    y*PIXELS_TO_METERES), 0.f);
    this->synchronize(m_renderObj.get(), m_bodyPtr);
}

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

void DrawablePolygonBody::setRotate(const float32 angle)
{
    
    m_bodyPtr->SetTransform(
                m_bodyPtr->GetPosition(),
                ( angle * PIXELS_TO_RADIANS));
    this->synchronize(m_renderObj.get(), m_bodyPtr);
}

void DrawablePolygonBody::synchronize(sf::Shape* view, b2Body* model) const
{
    float bodyPositionX = model->GetPosition().x * METERES_TO_PIXELS;
    float bodyPositionY = model->GetPosition().y * METERES_TO_PIXELS;
    float bodyRotate = model->GetAngle() * RADIANS_TO_PIXELS;

    view->setPosition(bodyPositionX, bodyPositionY);
    view->setRotation(bodyRotate);
}

sf::ConvexShape* DrawablePolygonBody::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;
}

b2Body* DrawablePolygonBody::getBody()
{
    return m_bodyPtr;
}

W zasadzie spora cześć kodu została wyjaśniona w poprzednim wpisie. Jednak w implementacji kryje się pewne „sprytne zabezpieczenie”. Mianowicie funkcja synchronizacji ciała-modelu z widokiem, wywoływana jest jedynie w chwili zawołania funkcji setPosition() i setRotate() klasy opakowującej. Ma to moim zdaniem dwie zalety: pierwsza - wymusza ona na użytkowniku klasy wołanie dedykowanych funkcji a nie zmianę pozycji ciała przez:


getBody()->setTransform(blablabla);

Drugą ważną korzyścią jest to że nie musimy wywoływać synchronizacji w funkcji rysowania dzięki czemu oszczędzamy cenną moc obliczeniową naszego komputera. W tym miejscu pozwolę sobie na małą dygresję: Ktoś może zapytać: ale po co takie skąpskie optymalizacje skoro mamy takie szybkie maszyny a to tylko kilka instrukcji? Zapewne takie myślenie się kryje za postępowaniem części współczesnych programistów gier gdzie później w ostatecznym rozrachunku mamy wymagania z kosmosu. Bo to tam kilka instrukcji, tutaj małe zaniedbanie, tam jakiś mały grzeszek i później na wymaganiach korytarzowej strzelanki: 4 rdzenie CPU po 3.2Ghz. No nie, dziękuje. ;)

Wracając do tematu. W tym miejscu mamy zatem naszą klasę opakowującą, razem z implementacją, czas zobaczyć jak wygląda użycie naszej klasy. Kod funkcji głównej prezentuje się następująco:


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

    /* Tekstura. */
    sf::Texture textureWood;
    if(!textureWood.loadFromFile("texture.png")){
        std::cout << "Texture problem \n";
    }

    /* Obiekt naszego wrappera. */
    DrawablePolygonBody myRenderBody(createBodyPro(myWorld.get()));
    myRenderBody.setPosition(150.f, 150.f);
    myRenderBody.setTexture(textureWood);

    /* Zgodnie z tradycją, bez
     * zmiany rotacji ani rusz. */
    const double changeRotateValue = 0.05;

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

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

        /* Logika modeli, ewentualna petla
         * symulacji, na potrzeby przykladu
         * nie byla ona nam potrzebna */
        beforeGameLoop(*myWorld);
        myRenderBody.setRotate(
                    myRenderBody.getRotate()
                        +changeRotateValue);

        /* Render. */
        window.clear(sf::Color::Black);
        myRenderBody.render(window);
        window.display();
    }

    return 0;
}

Jak widać użycie naszego wrappera jest bardzo proste. Po uruchomieniu naszym oczom ukaże się następujący obrazek, tekstura nie jest przypadkowa. ;)

Na zakończenie

Jak zawsze zachęcam do przetestowania kodu samemu. 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.