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

Notatki programisty: Kolizje, poprawki czyli rzeźbienia gry w Box2D/SFML ciąg dalszy

@biomenNotatki programisty: Kolizje, poprawki czyli rzeźbienia gry w Box2D/SFML ciąg dalszy10.01.2017 19:25

Czytając poprzednie wpisy dowiedzieliśmy jak napisać reprezentację graficzną ciała Box2D. Poznaliśmy ciała statyczne i dynamiczne. A nawet wprawiliśmy co niektóre obiekty w ruch. W poniższym tekście rozwiniemy nasze dotychczasowe rozwiązania by jeszcze bardziej zbliżyć się do umownego celu jakim jest demo technologiczne gry 2D. Zapraszam do lektury.

Naprawa skoku bohatera

W poprzednim tekście wspomniałem że funkcjonalność skakania naszego bohatera daleka jest od ideału. Bohater może odbić się z powietrza i szybować w przestworza w nieskończoność. Aby naprawić ten nieszczęsny „ficzer” napiszemy prostą funkcję sprawdzającą czy ciało naszego bohatera dotyka jakiegoś ciała a następnie dodamy ją do klasy BodyMover. Implementacja prezentuje się następująco a komentarz do niej jest zbyteczny:


bool BodyMover::jumpTest()
{
    bool toReturn = false;

    if(this->m_body->GetContactList() != nullptr){

        toReturn = true;
    }
    return toReturn;
}

Zdecydowałem się na to by funkcja testu skoku była prywatną metodą klasy BodyMover i była dostępna tylko dla wąskiego elitarnego grona funkcji. Z użyciem testu skoku, nasza funkcja move() będzie wyglądać następująco:


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:
        {
            if(this->jumpTest()){
                const b2Vec2 oldVelocity = m_body->GetLinearVelocity();
                const b2Vec2 newVelocity = b2Vec2(oldVelocity.x, - m_bodyJumpForce);

                m_body->SetLinearVelocity(newVelocity);
            }
            break;
        }
    }
}

Po uruchomieniu naszego programu powinniśmy utracić możliwość skakania aż do księżyca. Born to fly nie tym razem. ;)

Kolizje czyli zmora początkującego programisty gier

Koncept w jaki sposób, w Box2D, obsługujemy kolizje wymaga nieco wyjaśnienia. Zaczniemy od tego że mając świat i ciała w grze, w chwili wystąpienia kolizji musimy wiedzieć do kogo należy to ciało, jakiego jest typu itp. W Box2D mamy możliwość przypisywania do ciał wskaźników na dodatkowe struktury danych. Poniżej znajduje się prosta klasa z informacjami jakie będziemy chcieli przypisać do naszego ciała.


class BodyUserData
{
public:
    enum Type : int
    {
        None,
        Player,
        Map,
        Wall,
        Enemy
    };

    BodyUserData(BodyUserData::Type type);

    BodyUserData::Type getType() const;

private:
    BodyUserData::Type m_type;

};

BodyUserData::BodyUserData(BodyUserData::Type type) : m_type(type) {}

BodyUserData::Type BodyUserData::getType() const {
    return m_type;
}

Nie wygląda strasznie. Jak widać klasa zawiera tylko informację o typie do jakiego będzie przypisane nasze ciało albo na odwrót jak kto woli. Typie w rozumieniu: czy ciało jest elementem mapy, bohaterem czy może należy do przeciwnika. Oczywiście nic nie stoi na przeszkodzie aby dodać do niej więcej informacji aczkolwiek, na chwilę obecną, myślę że tyle nam wystarczy. Mając gotową klasę danych dodatkowych dla obiektu fizycznego możemy je przypisać. Poniżej przykładowe przypisanie. Dla pewności możemy uruchomić projekt czy po przypisaniu do któregoś z ciał dodatkowych informacji, aplikacja nam się nie wyłoży. ;)


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

    std::shared_ptr<BodyUserData>bodyPlatformData (new BodyUserData(BodyUserData::Type::Map));
    bodyPlatform.getBody()->SetUserData((void*)bodyPlatformData.get());

W Box2D do wykrywania zdarzeń kolizji służy klasa b2ContactListener. Obiekt tej klasy będzie naszym „nasłuchiwaczem” wypadków. Jako że chcemy posiadać bardziej spersonalizowaną klasę by mieć większą kontrolę nad jakie kolizje będą przechwytywane, napiszemy własną klasę pochodną. Jako że jestem lepszym koderem niż publicystą, zapraszam do zapoznania się z kodem i komentarzami.


class WorldContactListener : public b2ContactListener
{
public:

    /* Gdy wystapi jakas kolizja czyli
     * stykna sie dwa ciala, chcemy wiedziec
     * jaki charakter ma ta kolizja stad
     * pomocniczy tryb wyliczeniowy,
     * implementacja i uzycie powinno 
     * rozjasnic ogolny zamysl. */
    enum ContactType : int
    {
        Empty,
        PlayerTouchEnemy
    };

    /* Funkcja BeginContact jest wywolana w 
     * chwili gdy silnik fizyczny wychwyci jakias 
     * nowa kolizje: sytuacje w ktorej to 
     * dwa ciala sie stykaja. */
    void BeginContact(b2Contact* contact);
    
    /* Mozna sie domyslic na podstawie komentarza
     * do funkcji BeginContact.  ;) */
    void EndContact(b2Contact* contact);

    ContactType getContactType() const;

private:
    ContactType m_contactType;
};

void WorldContactListener::BeginContact(b2Contact* contact)
{
    std::cout << __func__ << "\n";

    /* Gdy wystepuje kolizja do funkcji przesylany
     * jest obiekt contact ktory zawiera informacje
     * jakie ciala sie spotkaly, do identyfikacji cial
     * uzywamy wczesniej przypisanych danychUzytkownika. */
    BodyUserData* bodyUserDataA = (BodyUserData*)(contact->GetFixtureA()->GetBody()->GetUserData());
    BodyUserData* bodyUserDataB = (BodyUserData*)(contact->GetFixtureB()->GetBody()->GetUserData());

    std::cout << bodyUserDataA->getType() << "\n";
    std::cout << bodyUserDataB->getType() << "\n";

    if(bodyUserDataA != nullptr && bodyUserDataB != nullptr){

        /* Sprawdz czy ktores z cial jest typu: Player. */
        bool bodyIsPlayer =
                bodyUserDataA->getType() == BodyUserData::Type::Player ||
                bodyUserDataB->getType() == BodyUserData::Type::Player;

        /* Sprawdz czy ktores cial jest typu: Enemy. */
        bool bodyIsEnemy =
                bodyUserDataA->getType() == BodyUserData::Type::Enemy ||
                bodyUserDataB->getType() == BodyUserData::Type::Enemy;

        /* Jezeli dwa ciala, jedno typu Player, drugie typu Enemy
         * dotykaja sie, ustaw informacje o rodzaju contactu. */
        if(bodyIsPlayer && bodyIsEnemy)
        {
            m_contactType = ContactType::PlayerTouchEnemy;
        }
    }
}

void WorldContactListener::EndContact(b2Contact* contact)
{
    std::cout << __func__ << "\n";

    /* Sytuacja bardzo podobna tylko dotyczy 
     * zakonczenia kontaktu miedzy dwoma cialami. */
    BodyUserData* bodyUserDataA = (BodyUserData*)(contact->GetFixtureA()->GetBody()->GetUserData());
    BodyUserData* bodyUserDataB = (BodyUserData*)(contact->GetFixtureB()->GetBody()->GetUserData());

    std::cout << bodyUserDataA->getType() << "\n";
    std::cout << bodyUserDataB->getType() << "\n";

    if(bodyUserDataA != nullptr && bodyUserDataB != nullptr){

        bool bodyIsPlayer =
                bodyUserDataA->getType() == BodyUserData::Type::Player ||
                bodyUserDataB->getType() == BodyUserData::Type::Player;

        bool bodyIsEnemy =
                bodyUserDataA->getType() == BodyUserData::Type::Enemy ||
                bodyUserDataB->getType() == BodyUserData::Type::Enemy;

        /* Gdy ciala typu Player i Enemy sie nie dotykaja 
         * to ustaw dane ContactType na pusty. */
        if(bodyIsPlayer && bodyIsEnemy)
        {
            m_contactType = ContactType::Empty;
        }
    }
}

WorldContactListener::ContactType WorldContactListener::getContactType() const 
{
    return m_contactType;
}

Na pierwszy rzut oka wydaje się że jest tego sporo aczkolwiek po chwili myślę że wszystko się wyjaśni. Mając te wszystkie rzeczy zebrane w całość, warto przyjrzeć do funkcji głównej:


int main(int argc, char *argv[])
{
    /* Tworzenia swiata i przypisanie nasluchiwacza kolizji. */
    WorldContactListener myContactLister;
    std::unique_ptr<b2World> myWorld(createWorld());
    myWorld.get()->SetContactListener(&myContactLister);

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

    std::shared_ptr<BodyUserData>bodyPlatformData (new BodyUserData(BodyUserData::Type::Map));
    bodyPlatform.getBody()->SetUserData((void*)bodyPlatformData.get());

    /* Platforma-przeciwnik. */
    DrawablePolygonBody bodySecondPlatform(createStaticBody(myWorld.get(), 100, 40));
    bodySecondPlatform.setPosition(600.f, 450.f);
    bodySecondPlatform.setTexture(textureMap);

    std::shared_ptr<BodyUserData>secondPlatformData (new BodyUserData(BodyUserData::Type::Enemy));
    bodySecondPlatform.getBody()->SetUserData((void*)secondPlatformData.get());

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

    std::shared_ptr<BodyUserData>playerData (new BodyUserData(BodyUserData::Type::Player));
    myPlayerBody.getBody()->SetUserData((void*)playerData.get());

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

    /* W zaleznosci od kolizji gracza z 
     * platforma-przeciwnikiem
     * bedziemy zmieniac tlo ekranu. */
    sf::Color colorWindowBackground;

    /* 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);
            }
        }

        /* Kolizje. */
        if(myContactLister.getContactType() ==
                WorldContactListener::ContactType::PlayerTouchEnemy)
        {
            colorWindowBackground = sf::Color::White;
        } else {
            colorWindowBackground = sf::Color::Black;
        }

        /* Inna logika. */
        beforeGameLoop(*myWorld.get());
        myPlayerBody.update();

        /* Render. */
        window.clear(colorWindowBackground);

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

        window.display();
    }

    return 0;
}

Zmiany w funkcji main w stosunku do poprzedniego wpisu są dość subtelne. Warty podkreślenia jest fakt aby od tej pory każde ciało naszej symulacji miało ustawione jakiekolwiek BodyUserData. Funkcje beginContact() i endContact() mogą w którymś momencie pobrać ciało które tych danych nie będzie posiadało i zrzutować nam jakieś pamięciowe śmieci przez co aplikacja nam się, pisząc kolokwialnie, wyłoży. Jest to na razie chyba jedyna niedogodność obecnego rozwiązania. Dodatkowo wprowadziliśmy prosty mechanizm zmiany koloru tła aby faktycznie przekonać się że nasz detektor kolizji działa.

Na zakończenie oczywiście zachęcam do samodzielnego przetestowania kodu. Tym razem bez zrzutu ekranowego. Jest zbyt efekciarski by jego „majestat” zamykać w zwykłym obrazku. ;)

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.