Blog (35)
Komentarze (574)
Recenzje (0)
@biomenNotatki programisty: rysowanie ciał Box2D z użyciem SFML

Notatki programisty: rysowanie ciał Box2D z użyciem SFML

07.01.2017 19:29, aktualizacja: 08.01.2017 11:23

W poniższym wpisie chciałbym przedstawić prosty sposób na wykonanie prezentacji graficznej ciała Box2D, w aplikacji SFML. Jako że wyżej wspomniana biblioteka fizyczna nie zapewnia możliwości rysowania obiektów, render musimy opracować sami. Wszystkich zainteresowanych zapraszam do lektury.

Słowem wstępu

Pisząc grę bardzo często zachodzi potrzeba posiadania modułu programu odpowiedzialnego za symulacje świata fizycznego. Tak się dobrze składa że dzięki uprzejmości jednego z pracowników firmy Blizzard, Erina Catto, mamy do dyspozycji łatwą w obsłudze otwarto-źródłową bibliotekę Box2D z której z powodzeniem możemy używać w swoich produkcjach. Co prawda ostatnia aktualizacja pochodzi z roku 2014, aczkolwiek każdy kto miał z nią styczność na pewno się zgodzi z tym że projekt zapewnia na tyle wysoką jakość że spokojnie możemy go użyć w środowisku produkcyjnym. Na potwierdzenie powyższej tezy, powołując się (a jakże) na Wikipedię, warto wspomnieć o tym że Box2D został wykorzystany w grze Limbo oraz Angry Birds. Aby przysporzyć Ci drogi czytelniku cukrzycy dorzucę tylko drobny szczegół, mianowicie: biblioteka doczekała się portów na inne języki. Jest ona między innymi jest jedną ze składowych Javowego libGDX dzięki któremu pisanie aplikacji na systemy mobilne staje się dużo prostsze.

Grunt to warsztat

Tak jak w swoim poprzednim wpisie, będę korzystać z QtCreatora z MinGW. O takich oczywistościach jak posiadanie zbudowanych bibliotek Box2D i SFML nie będę wspominać. ;) Aby lepiej zrozumieć idee jaka mi przyświecała pisząc ten przykład, warto zapoznać się ze wzorcem model-widok-kontroler. Chciałem zachować wyrazistą granicę między modelem (ciałem fizycznym) a widokiem (obiektem graficznym SFML który to ciało reprezentuje). Gdy nasz czysty projekt został utworzony a zależności dołączone, zapraszam do kodu.

Kodzimy

Przykład zaczniemy od stworzenia świata, funkcja tworzenia świata prezentuje się następująco:


b2World* createWorld()
{
    b2Vec2 myGravity(0.f, 9.8f);
    return new b2World(myGravity);
}

Pozwolę sobie nie omawiać poszczególnych etapów tworzenia ciał czy też innych elementów biblioteki fizycznej, w celu pogłębiania wiedzy odsyłam do tej strony. Serdecznie polecam. Następnym krokiem jest stworzenie przykładowego ciała, kod funkcji wygląda następująco:


/* Tworzymy cialo statyczne o rozmiarach 
 * 100x100px i dodajemy je do przekazanego 
 * w parametrze swiata Box2d. */

const float G_PIXELS_TO_METERES = 0.02f;
const int G_METERES_TO_PIXELS = 50;

b2Body* createBody(b2World* world)
{
    b2PolygonShape bodyShape;
    bodyShape.SetAsBox(50*G_PIXELS_TO_METERES, 50*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;
}

Słowem wyjaśnienia, Box2D używa jednostek miar wyrażonych w metrach, natomiast SFML – pikseli. Stąd dwie pomocnicze zmienne globalne których będziemy używać do przeliczania metrów na piksele i odwrotnie.

W dalszej części wpisu chciałbym przedstawić dwa sposoby na generowanie widoku dla ciała-modelu. Pierwszy jest dość naiwny, zakłada bowiem że z góry wiemy jakiej wielkości i jakiego kształtu będzie nasze ciało fizyczne. Kod na wygenerowanie takiego widoku prezentuje się następująco:


sf::RectangleShape generateView()
{
    sf::RectangleShape bodyView;
    bodyView.setSize(sf::Vector2f(100.f, 100.f));
    bodyView.setOrigin(sf::Vector2f(50.f, 50.f));
    bodyView.setFillColor(sf::Color::Green);

    return bodyView;
}

To nic innego jak stworzenie kwadratu o odpowiednich właściwościach. Warto mieć na uwadze że nie żyjemy w idealnym świecie przez co przydałoby się nam bardziej uniwersalne podejście do tematu. Poniższy kod jest mniej naiwny, zakłada bowiem tylko że ciało jest wielokątem. Kod prezentuje się następująco:


sf::ConvexShape generateViewPro(b2Body* body)
{

    sf::ConvexShape bodyView;
    bodyView.setFillColor(sf::Color::White);

    /* Informacja o ksztalcie ciala znajduje sie
     * w jego fixturze, stad pobieramy fixtury ciala.
     * Jako ze cialo moze posiadac kilka fixtur,
     * uzyjemy przez to petli. */
    for (b2Fixture* fixturePtr = body->GetFixtureList();
            fixturePtr != nullptr; fixturePtr = fixturePtr->GetNext())
    {
        /* Pobieramy ksztalt z fixtury i odczytujemy jego typ. */
        b2Shape* shapeBuffer = fixturePtr->GetShape();
        if(shapeBuffer->m_type == b2Shape::Type::e_polygon)
        {
            b2PolygonShape* realBodyShape = static_cast<b2PolygonShape*>(shapeBuffer);

            /* e_polygon jest ksztaltem wieszcholkowym
             * stad pobieramy jego wierzcholki i konstruujemy
             * z nich obiekt sf::ConvexShape ktory ma za zadanie
             * reprezentowac ksztalt wielokata. */
            const int vertexCount = realBodyShape->GetVertexCount();
            bodyView.setPointCount(vertexCount);

            for (int i = 0; i < vertexCount; ++i)
            {
                b2Vec2 currVerts = realBodyShape->GetVertex(i);

                float posX = currVerts.x * G_METERES_TO_PIXELS;
                float posY = currVerts.y * G_METERES_TO_PIXELS;

                bodyView.setPoint(i, sf::Vector2f(posX, posY));
            }

            bodyView.setOrigin(0, 0);
        }
    }

    return bodyView;
}

Powyższe rozwiązanie z powodzeniem możemy zastosować dla ciał z kształtami inne niż kwadraty, stosowny przykład zaprezentuje pod koniec tekstu. Mając powyższe funkcje dochodzimy do momentu w którym mamy kod odpowiedzialny za tworzenie świata, przykładowego ciała oraz reprezentacji graficznych. Prawie ostatnim elementem jaki musimy wykonać jest synchronizacja modelu z widokiem. Kod na synchronizacje fizyki z grafiką wydaje się dość zrozumiały, czyż nie? ;)


void updateView(sf::Shape* view, b2Body* model)
{
    /* Aktualizacja pozycji i rotacji widoku. */
    float bodyPositionX = model->GetPosition().x * G_METERES_TO_PIXELS;
    float bodyPositionY = model->GetPosition().y * G_METERES_TO_PIXELS;
    float bodyRotate = model->GetAngle() * 180 / b2_pi;

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

Dzięki temu że sf::RectangleShape i sf::ConvexShape dziedziczą po sf::Shape mamy możliwość rzutowania w górę przez co jedną funkcje możemy zastosować do obu tych typów. Ostatnią rzeczą o jakiej muszę wspomnieć przed przejściem do pętli głównej jest funkcja przygotowująca świat do symulacji:


void beforeGameLoop(b2World& world)
{
    const float32 timeStep = 1.0f / 60.0f;
    const int32 velocityIterations = 6;
    const int32 positionIterations = 2;
    world.Step(
            timeStep,
            velocityIterations,
            positionIterations);
}

Przed wykonaniem pętli głównej symulacji wykonujemy powyższą funkcję w celu określenia „kroków czasowych” dla świata Box2D które determinują dokładność z jaką odbywa się nasza symulacja. Mają wszystkie te rzeczy nadszedł czas na danie główne wpisu czyli funkcję główną, oto i ona:


int main(int argc, char *argv[])
{
    /* Ciala nalezace do swiata b2world automatycznie
     * zostaja zdealokowane z pamieci przez co musimy
     * martwic sie jedynie o poprawne zwolnienie
     * pamieci swiata box2d. */
    std::unique_ptr<b2World> myWorld(createWorld());

    /* Tworzymy cialo statyczne o wymiarach 100x100
     * pikseli a nastepnie umieszczamy je na pozycji
     * x-150, y-150 pikseli. */
    b2Body* myBody = createBody(myWorld.get());
    myBody->SetTransform(
                b2Vec2(
                    150*G_PIXELS_TO_METERES,
                    150*G_PIXELS_TO_METERES), 0.f);

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

    /* Prosty przelacznik pomiedzy widokami. */
    bool viewSwitch = false;

    /* Graficzne reprezentacje ciala box2d. */
    sf::RectangleShape bodyView = generateView();
    sf::ConvexShape bodyViewPro = generateViewPro(myBody);;

    /* Nasze cialo bedziemy obracac w petli
     * glownej symulacji Box2D, wartosc o jaka
     * bedziemy zmieniac rotacje przypisujemy
     * do stalej. */
    const double rotateValue = 0.001;

    /* 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)){

                /* Aby latwiej bylo przelaczac klawiaszem
                 * renderowany widok, wstrzymujemy watek
                 * aby informacja o kliknieciu nie poszla
                 * za szybko. */
                viewSwitch = !viewSwitch;
                sf::sleep(sf::milliseconds(250));
            }
        }

        /* Fizyka - petla glowna symulacji. */
        /* Przygotowanie.*/
        beforeGameLoop(*myWorld);

        /* W petli pobierane sa wszystkie ciala
         * zawarte w symulowanym swiecie, w naszym
         * przypadku wiemy ze jest tylko jedno co
         * znaczaco ulatwia nam zadanie. */
        for(b2Body* bodyPtr = myWorld->GetBodyList();
                bodyPtr != nullptr;
                    bodyPtr = bodyPtr->GetNext() )
        {
            /* Nasz obiekt-model obracamy aby
             * bylo bardziej bajerancko. */
            bodyPtr->SetTransform(
                        bodyPtr->GetPosition(),
                        bodyPtr->GetAngle()+rotateValue);

            /* Synchronizacja widoku i modelu. */
            updateView(&bodyView, bodyPtr);
            updateView(&bodyViewPro, bodyPtr);
        }

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

        if(viewSwitch){
            window.draw(bodyView);
        } else {
            window.draw(bodyViewPro);
        }

        window.display();
    }

    return 0;
}

W zdarzeniach możemy użyć spacji aby przełączać się między widokiem naiwnym a widokiem generowanym. Różnicę między nimi poznamy, jak idzie wywnioskować po kodzie, po kolorze. Myślą że komentarze rozwiewają ewentualne niejasności. Jak widać, zachowaliśmy w kodzie porządek z jasno określoną granicą między, fizyka, grafiką a obsługą zdarzeń. Efekt prezentuje się następująco:

612353

W jednym ze wcześniejszym akapitów wspomniałem że funkcje generowania widoku możemy zastosować do innych kształtów niż kwadraty. Użyjmy do tworzenia ciała następującej funkcji:


b2Body* createBodyPro(b2World* world)
{
    b2Vec2 vertices[5];
    vertices[0].Set( -30*G_PIXELS_TO_METERES,  40*G_PIXELS_TO_METERES);
    vertices[1].Set( -30*G_PIXELS_TO_METERES,   0*G_PIXELS_TO_METERES);
    vertices[2].Set(   0*G_PIXELS_TO_METERES, -50*G_PIXELS_TO_METERES);
    vertices[3].Set(  30*G_PIXELS_TO_METERES,   0*G_PIXELS_TO_METERES);
    vertices[4].Set(  30*G_PIXELS_TO_METERES,  30*G_PIXELS_TO_METERES);

    b2PolygonShape polygonShape;
    polygonShape.Set(vertices, 5);

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

    b2BodyDef bodyDef;
    bodyDef.type = b2_staticBody;

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

Przełączając się na widok generowany, naszym oczom powinien ukazać się następujący obrazek:

612357

Na zakończenie

Temat fizyki i wariantów użyć Box2D jest zbyt rozległy aby całość zawrzeć w jednym wpisie. Znając podstawy dotyczące generowania obiektów graficznych dla ciał fizycznych opisywanej biblioteki mamy solidne fundamenty do tego aby wizualnie rozpocząć naszą przygodę z tą technologią. Jeżeli interesuje nas temat samego silnika warto zerknąć na stronę podlinkowaną wyżej. Znajomość Box2D i renderingu jego obiektów otwiera nam furtkę do tworzenia komercyjnych gier. A wszystko na licencji zlib. Grzech nie skorzystać. ;) Jak zawsze zachęcam do przetestowaniu kodu.

Dzięki wielkie za uwagę.

Wybrane dla Ciebie
Komentarze (1)