Notatki programisty: czyje to zwłoki? Czyli identyfikujemy ciała biorące udział w kolizji
16.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ę!