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

Notatki programisty: czyje to zwłoki? Czyli identyfikujemy ciała biorące udział w kolizji

@biomenNotatki programisty: czyje to zwłoki? Czyli identyfikujemy ciała biorące udział w kolizji16.01.2017 12:11

W poprzednich przykładach korzystaliśmy z systemu wykrywania kolizji który pozwalał jedynie na odczytanie informacji jakiegoś typu zdarzenie miało miejsce. Rozwiązanie jednak jest nie wystarczające w sytuacji w której to chcemy pobrać informację o ciałach które biorą udział w zdarzeniu lub policzeniu ile razy jednocześnie dane wydarzenie miało miejsce. W poniższym wpisie zaprezentuje nowy mechanizm który da nam większą kontrolę na tym co się dzieje na naszym podwórku. Zapraszam do lektury.

Od czego zacząć

Dla czystości w projekcie zaczniemy od napisania nowej klasy. Oczywiście możemy przerobić istniejącą aczkolwiek łatwiej pracuje mi się nad czystą kartką papieru. ;) Koncept działania klasy będzie bazować na naszym dotychczasowym rozwiązaniu. Kluczową zmianą jaką wprowadzimy to utworzenie struktury która będzie przechowywać informację o typie zdarzenia oraz ciałach jakie biorą w nim udział. Dodatkowo uporządkujemy informację o typach w strukturze klasy, nie przedłużając, definicja klasy wygląda następująco.


class ContactDetector : public b2ContactListener
{
public:
    struct Contact
    {
        enum Type : int
        {
            Empty,
            PlayerTouchEnemy,
            PlayerTouchWall
        };

        struct Info
        {
            Type type;
            b2Body* bodyPtrFirst;
            b2Body* bodyPtrSecond;
        };
    };

    void BeginContact(b2Contact* contact);
    void EndContact(b2Contact* contact);

    bool isContactListIsEmpty() const;
    bool isContactListContains(Contact::Type type) const;
    
    std::vector<Contact::Info> getContactList(Contact::Type type);

private:
    std::vector<Contact::Info> m_contactDiary;

    int getContactInfoItemIndex(const Contact::Info& data) const;

    bool isContactShouldBeServerd(Contact::Type type) const;
    bool isContactInfoExistsInDiary(const Contact::Info& data) const;

    Contact::Info constructContactInfo(
            Contact::Type type,
            b2Body* contactBodyA,
            b2Body* contactBodyB) const;

    Contact::Type getContactType(b2Body* bodyA, b2Body* bodyB) const;
};

Ot stara klasa ze zmienionym typem zapisywanych obiektów w wektorze. Doszła nam nowa funkcja publiczna którą warto omówić, mianowicie: mając symulację w grze może zdarzyć się sytuacja w której to bohater dotknie dwóch obiektów przeciwników. Funkcja zwraca listę/wektor wszystkich obiektów zdarzeń, o określonym typie. Co więcej, ważne było dla mnie aby zachować zgodność starego kodu z nową klasą przez co zmiany w obszarze metod publicznych są mocno ograniczone. Reszta funkcji została omówiona w komentarzach. Oto jak wygląda nasza implementacja:


void
ContactDetector::BeginContact(b2Contact* contact)
{
    b2Body* bodyA = contact->GetFixtureA()->GetBody();
    b2Body* bodyB = contact->GetFixtureB()->GetBody();

    if(bodyA != nullptr && bodyB != nullptr){

        Contact::Type contactType = getContactType(bodyA, bodyB);

        if(this->isContactShouldBeServerd(contactType))
        {
            Contact::Info newDiaryElement =
                    this->constructContactInfo(
                        contactType, bodyA, bodyB);

            if(!isContactInfoExistsInDiary(newDiaryElement)){
                m_contactDiary.push_back(newDiaryElement);
            }
        }
    }
}

void
ContactDetector::EndContact(b2Contact* contact)
{
    b2Body* bodyA = contact->GetFixtureA()->GetBody();
    b2Body* bodyB = contact->GetFixtureB()->GetBody();

    if(bodyA != nullptr && bodyB != nullptr){

        Contact::Type contactType = getContactType(bodyA, bodyB);

        if(this->isContactShouldBeServerd(contactType))
        {
            Contact::Info diaryItem =
                    this->constructContactInfo(
                        contactType, bodyA, bodyB);

            if(this->isContactInfoExistsInDiary(diaryItem)){
                const int index = this->getContactInfoItemIndex(diaryItem);
                m_contactDiary.erase(m_contactDiary.begin() + index);
            }
        }
    }
}

/* Nie wszystkie zdarzenia cial bedziemy
 * rejestrowac stad funkcja sprawdzajaca
 * czy zdarzenie nalezy do listy
 * interesujacych nas zdarzen. */
bool
ContactDetector::isContactShouldBeServerd(Contact::Type type) const
{
    return
        type == Contact::Type::PlayerTouchEnemy ||
        type == Contact::Type::PlayerTouchWall;
}

/* Zwraca typ zdarzenia jaki
 * okreslilismy miedzy dwoma cialami. */
ContactDetector::Contact::Type
ContactDetector::getContactType(b2Body *bodyA, b2Body *bodyB) const
{
    BodyUserData* bodyUserDataA = (BodyUserData*)(bodyA->GetUserData());
    BodyUserData* bodyUserDataB = (BodyUserData*)(bodyB->GetUserData());

    BodyUserData::Type bodyTypeA = bodyUserDataA->getType();
    BodyUserData::Type bodyTypeB = bodyUserDataB->getType();

    const bool bodyIsPlayer =
            bodyTypeA == BodyUserData::Type::Player ||
            bodyTypeB == BodyUserData::Type::Player;

    const bool bodyIsEnemy =
            bodyTypeA == BodyUserData::Type::Enemy ||
            bodyTypeB == BodyUserData::Type::Enemy;

    const bool bodyIsWall =
            bodyTypeA == BodyUserData::Type::Wall ||
            bodyTypeB == BodyUserData::Type::Wall;

    Contact::Type contactType = Contact::Type::Empty;
    if(bodyIsPlayer && bodyIsEnemy)
    {
        contactType = Contact::Type::PlayerTouchEnemy;
    }

    if(bodyIsPlayer && bodyIsWall)
    {
        contactType = Contact::Type::PlayerTouchWall;
    }
    return contactType;
}

/* Funkcja konstruuje obiekt
 * dziennika zdarzen. Okresla
 * rowniez ktore z cial jest
 * graczem i autematycznie przypisuje
 * cialo gracza do bodyPtrFirst. */
ContactDetector::Contact::Info
ContactDetector::constructContactInfo(
        Contact::Type type,
        b2Body *contactBodyA,
        b2Body *contactBodyB) const
{
    BodyUserData* bodyUserDataA =
            (BodyUserData*)(contactBodyA->GetUserData());

    BodyUserData::Type bodyTypeA = bodyUserDataA->getType();

    Contact::Info newDiaryElement;
    if(bodyTypeA == BodyUserData::Type::Player)
    {
        newDiaryElement.bodyPtrFirst = contactBodyA;
        newDiaryElement.bodyPtrSecond = contactBodyB;
    } else {
        newDiaryElement.bodyPtrFirst = contactBodyB;
        newDiaryElement.bodyPtrSecond = contactBodyA;
    }
    newDiaryElement.type = type;

    return newDiaryElement;
}

/* Funkcja zwraca liste obiektow
 * dziennika zdarzen o interesujacym
 * nas typie. */
std::vector<ContactDetector::Contact::Info>
ContactDetector::getContactList(Contact::Type type)
{
    std::vector<ContactDetector::Contact::Info> toReturn;
    for(const Contact::Info& item : m_contactDiary){

        if(item.type == type){
            toReturn.push_back(item);
        }
    }
    return toReturn;
}

bool
ContactDetector::isContactListContains(Contact::Type type) const
{
    bool toReturn = false;
    for(const Contact::Info& item : m_contactDiary){

        if(item.type == type){
            toReturn = true;
            break;
        }
    }
    return toReturn;
}

bool
ContactDetector::isContactListIsEmpty() const
{
    return m_contactDiary.empty();
}

bool
ContactDetector::isContactInfoExistsInDiary(const Contact::Info& data) const
{
    bool toReturn = false;
    for(const Contact::Info& item : m_contactDiary){

        const bool condition =
                item.type == data.type &&
                item.bodyPtrFirst == data.bodyPtrFirst &&
                item.bodyPtrSecond == data.bodyPtrSecond;

        if(condition){
            toReturn = true;
            break;
        }
    }
    return toReturn;
}

int
ContactDetector::getContactInfoItemIndex(
        const ContactDetector::Contact::Info& data) const
{
    int toReturn = 0;

    const int listSize = m_contactDiary.size();
    for(int i = 0; i < listSize; ++i){

        const Contact::Info* item = &m_contactDiary.at(i);

        const bool condition =
                item->type == data.type &&
                item->bodyPtrFirst == data.bodyPtrFirst &&
                item->bodyPtrSecond == data.bodyPtrSecond;

        if(condition){
            toReturn = i;
            break;
        }
    }

    return toReturn;
}

Na pierwszy rzut oka rzucają się zmiany w funkcjach beginContact() i endContact(). Jak można było zauważyć w poprzednich wpisach. Pewne mechanizmy identyfikowania typu zdarzeń, powtarzały się w obu metodach. Przez co wydajniej było je wydzielić do nowych funkcji. Dzięki temu że w C++ mechanizm porównywania wskaźników działa na zasadzie: sprawdź czy wskaźnik A pokazuje na ten sam obszar pamięci co wskaźnik B, nasza funkcja do wykrywania duplikatów nie jest zbyt skomplikowana. Co więcej zamknęliśmy w jednej metodzie warunki dotyczące tego jakie typy zdarzeń nas interesują. Warto mieć na uwadze że konstruując obiekt dziennika, ciało należące do bohatera przypisujemy do pola bodyPtrFirst. Patrząc na kod możemy dojść do prostego opisu algorytmu: sprawdź jakiego typu jest zdarzenie, jeżeli jest typu które nas interesuje, skonstruuj obiekt dziennika, jeżeli obiekt dziennika jest nowy i nie istnieje identyczny, zapisz go w dzienniku w przeciwnym wypadku, olej sprawę. Prosta lista kroków, prawda? ;)

Okej, mamy mechanizm który pozwala nam odczytać informację o ciałach Box2D jakie brały udział w zdarzeniu, ale w tym miejscu powstaje pytanie: jak pobrać obiekt graficzny do którego nasze ciało należy. W tym miejscu przyda nam się nasz wektor ewidencji obiektów. Funkcja do określania rodzica, ciała b2Body wygląda jak poniżej. Nic skompilowanego.


DrawablePolygonBody* getBodyShape(
        std::vector<DrawablePolygonBody> list,
        const b2Body* body)
{
    DrawablePolygonBody* toReturn = nullptr;

    for(DrawablePolygonBody& item : list){
        if(item.getBody() == body){
            toReturn = &item;
            break;
        }
    }

    return toReturn;
}

Kolejną rzeczą która ułatwi nam debugowanie aplikacji będzie dodanie nowej funkcji publicznej do klasy DrawablePolygonBody, jest to metoda setColor() dzięki której będziemy mogli zmieniać zabarwienie naszego obiektu graficznego. Implementacja jest dziecinnie prosta:


void DrawablePolygonBody::setColor(sf::Color newColor)
{
    this->m_renderObj.get()->setFillColor(newColor);
}

Mając wszystkie te rzeczy możemy przystąpić do analizy kodu źródłowego funkcji głównej.


int main(int argc, char *argv[])
{
    ContactDetector myContactLister;
    std::unique_ptr<b2World> myWorld(createWorld());
    myWorld.get()->SetContactListener(&myContactLister);

    /* Kod ladowania tekstur, tworzenia obiektow
     * platform, bohatera, przeciwnika i poruszaczy
     * cial zostaje taki sam jak w poprzednich przykladach. */

    /* Ewidencja cial. */
    std::vector<DrawablePolygonBody> listWorldBodies;
    listWorldBodies.push_back(bodyPlatform);
    listWorldBodies.push_back(bodyWallA);
    listWorldBodies.push_back(bodyWallB);
    listWorldBodies.push_back(bodyPlayer);
    listWorldBodies.push_back(bodyEnemy);

    sf::RenderWindow window(
                sf::VideoMode(800, 600, 32),
                std::string("SFML/Box2D - tech demo"),
                sf::Style::Default);
    window.setFramerateLimit(60);

    sf::Color colorWindowBackground;

    while(window.isOpen())
    {
        /* Render-cleaner. */
        bodyEnemy.setColor(sf::Color::White);

        /* Sekcja zdarzen. */
        sf::Event myEvent;
        while(window.pollEvent(myEvent))
        {
            if(myEvent.type == sf::Event::Closed){
                window.close();
            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::Space))
            {
                playerMover.move(BodyMover::Direction::Jump);
            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::D))
            {
                playerMover.move(BodyMover::Direction::Right);
            }

            if(sf::Keyboard::isKeyPressed(sf::Keyboard::A))
            {
                playerMover.move(BodyMover::Direction::Left);
            }
        }

        /* Sekcja Box2D. */
        beforeGameLoop(*myWorld.get());

        /* Wydzielony warunek widzenia przeciwnika. */
        const Observer::Info isEnemySeePlayerData =
                            Observer::isBodySeeBody(
                                bodyEnemy.getBody(),
                                bodyPlayer.getBody(), 150);

        if(isEnemySeePlayerData.isSee){
            switch(isEnemySeePlayerData.side)
            {
                case Observer::Info::Side::Left: {
                    enemyMover.move(BodyMover::Direction::Left);
                    break;
                }

                case Observer::Info::Side::Right: {
                    enemyMover.move(BodyMover::Direction::Right);
                    break;
                }
            }
        }

        /* Standardowe stare podejscie do kolizji. */
        if(isEnemySeePlayerData.isSee)
        {
            if(!myContactLister.isContactListIsEmpty())
            {
                const bool isPlayerTouchEnemy =
                                myContactLister.isContactListContains(
                                    ContactDetector::Contact::Type::PlayerTouchEnemy);

                if(isPlayerTouchEnemy)
                {
                    const float enemyPositionY = bodyEnemy.getPosition().y;
                    const float playerPositionY = bodyPlayer.getPosition().y;

                    if(playerPositionY < enemyPositionY-10){
                        enemyMover.move(BodyMover::Direction::Jump);
                    }
                    colorWindowBackground = sf::Color::Yellow;
                }

                const bool isPlayerTouchWall =
                                myContactLister.isContactListContains(
                                    ContactDetector::Contact::Type::PlayerTouchWall);

                if(isPlayerTouchEnemy && isPlayerTouchWall)
                {
                    const float enemyPositionY = bodyEnemy.getPosition().y;
                    const float playerPositionY = bodyPlayer.getPosition().y;

                    if(playerPositionY > enemyPositionY-20){
                        colorWindowBackground = sf::Color::Red;
                    }
                }
            } else {
                colorWindowBackground = sf::Color::Black;
            }
        }

        /* Precyzyjne podejscie do kolizji. */
        if(!myContactLister.isContactListIsEmpty())
        {
            if(myContactLister.isContactListContains(
                        ContactDetector::Contact::Type::PlayerTouchEnemy))
            {
                std::vector<ContactDetector::Contact::Info> enemyContacts =
                        myContactLister.getContactList(
                            ContactDetector::Contact::Type::PlayerTouchEnemy);

                if(!enemyContacts.empty()){
                    DrawablePolygonBody* ptr =
                            getBodyShape(
                                listWorldBodies,
                                enemyContacts.at(0).bodyPtrSecond);

                    ptr->setColor(sf::Color::Green);
                }
            }
        }

        bodyPlayer.update();
        bodyEnemy.update();

        /* Render. */
        window.clear(colorWindowBackground);
        for(DrawablePolygonBody& item : listWorldBodies){
            item.render(window);
        }

        window.display();
    }
    return 0;
}

Jak widzimy nowa klasa może być używana tak samo jak obiekt poprzedniej wersji. Dodatkowo dla poprawy czytelności wydzieliliśmy sekcje: przeciwnik widzi bohatera od systemu kolizji. W przykładzie wiemy że mamy tylko jednego przeciwnika wiec z enemyContacts pobieramy tylko pierwszy element w przyszłości taką listę możemy sobie według własnego uznania filtrować i decydować dodatkowymi warunkami to jaki obiekt nas interesuje. Uruchomiając przykład powinniśmy zaobserwować że przy kontakcie bohatera z przeciwnikiem, nasz smok stanie się zielony. Aby przywrócić naturalny kolor przeciwnika, przed rozpoczęciem głównej części pętli główej: czyścimy kolor przeciwnika. Nasze demo powinno prezentować się następująco:

Jak zawsze zachęcam do własnoręcznego przetestowania kodu i 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.