Blog (35)
Komentarze (574)
Recenzje (0)
@biomenNotatki programisty: Wrapper na ciało Box2D i render SFML

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

08.01.2017 13:46, aktualizacja: 08.01.2017 17:48

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. ;)

612387

Na zakończenie

Jak zawsze zachęcam do przetestowania kodu samemu. Dzięki za uwagę!

Wybrane dla Ciebie
Komentarze (0)