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

Notatki programisty: im nas więcej tym weselej, czyli porządkujemy kod i tworzymy grupę niezależnych przeciwników

@biomenNotatki programisty: im nas więcej tym weselej, czyli porządkujemy kod i tworzymy grupę niezależnych przeciwników17.01.2017 15:05

W poprzednich przykładach, nasz gracz miał do czynienia tylko z jednym przeciwnikiem. W niniejszym wpisie chciałbym zaprezentować dość schludny, moim zdaniem, sposób zarządzania większą ilością obiektów na ekranie a także rozwiniemy istniejące rozwiązania do wyższych abstrakcji. Zapraszam do lektury.

Zaczyna się od porządku, jak w wojsku

Już w poprzednim wpisie pominąłem znaczną część kodu funkcji głównej ze względu na jego niezmienność i „klocowatość”. Jak widać na przestrzeni wpisów, z każdym kolejnym przykładem, ta część kodu rozrosła się nam niemiłosiernie. To znak że nadszedł czas na cięcia i modernizacje. Zamkniemy graficzne ciało i mechanizm ruchu w jednej klasie fasadowej dzięki czemu uzyskamy abstrakcję w postaci MovableBody. Dzięki temu łatwiej będzie nam zarządzać ciałami, które mają zostać wprawione w ruch, poprzez użycie listy ewidencyjnej, ale o tym w późniejszej części wpisu. Deklaracja i implementacja klasy wygląda następująco:


class MovableBody
{

public:
    MovableBody(
            DrawablePolygonBody* renderShape,
            BodyUserData::Type type);

    b2Body* getBody() const;
    BodyMover* getMover() const;
    DrawablePolygonBody* getRenderShape() const;

private:
    std::shared_ptr<DrawablePolygonBody> m_shape;
    std::shared_ptr<BodyUserData> m_data;
    std::shared_ptr<BodyMover> m_mover;
};

MovableBody::MovableBody(
        DrawablePolygonBody* renderShape,
        BodyUserData::Type type) :
    m_shape(renderShape),
    m_data(new BodyUserData(type)),
    m_mover(new BodyMover(renderShape->getBody()))
{
    renderShape->getBody()->SetUserData((void*)m_data.get());
}

DrawablePolygonBody* MovableBody::getRenderShape() const
{
    return m_shape.get();
}

BodyMover* MovableBody::getMover() const
{
    return m_mover.get();
}

b2Body* MovableBody::getBody() const
{
    return m_shape->getBody();
}

Ot nic specjalnego, zwykła klasa opakowująca. Dla większej elegancji użycia zostawiliśmy sobie furtkę w postaci getBody() aby mieć łatwy dostęp do ciała Box2D. Skrót ten jest dużo „naturalniejszy” niż wywołanie getDrawableShape()->getBody() i okaże się nieoceniony w walce o czytelność kodu. Następnym krokiem będzie optymalizacja testu skoku. Jako że nasza aplikacja z każdym kolejnym artykułem się rozrasta, zachodzi silna potrzeba szukania optymalizacji i wydajności. Nowa funkcja wygląda jak poniżej:


bool BodyJumpValidator::test(b2Body* body)
{
    if(body->GetContactList() != nullptr){

        for(b2ContactEdge* it = body->GetContactList(); it != nullptr; it = it->next)
        {
            b2Contact* contact = it->contact;

            b2Body* bodyA = contact->GetFixtureA()->GetBody();
            b2Body* bodyB = contact->GetFixtureB()->GetBody();

            if(bodyA != nullptr && bodyB != nullptr)
            {
                b2Body* bodyToCheck = nullptr;
                if(bodyA == body){
                    bodyToCheck = bodyB;
                } else {
                    bodyToCheck = bodyA;
                }

                BodyUserData* bodyUserData = (BodyUserData*)(bodyToCheck->GetUserData());
                BodyUserData::Type bodyType = bodyUserData->getType();

                if(bodyType == BodyUserData::Type::Map){
                    return true;
                }
            }
        }
    }
    return false;
}

Wprowadziliśmy prosty test sprawdzający które z ciał kolizji jest testowanym, podanym w parametrze funkcji. Wiedząc które ciało jest testowanym, ograniczamy ilość pobrań informacji. Zyskujemy dzięki temu prostemu rozwiązaniu większą wiedzę nad tym co się dzieje w naszej pętli. Wcześniej sprawdzaliśmy czy którekolwiek ciało z dwóch jest np. typu Wall. Teraz już wiemy które konkretnie ciało musimy zbadać. Da nam to większe pole manewru do określania warunków skoku.

Mając te rzeczy możemy przejść do omówienia ewidencji przeciwników. Tak jak w poprzednich wpisach tak i w tym, wszystkie obiekty MovableBodies należące do przeciwników przypiszemy do wektora ewidencji. W poprzednim wpisie, zaprezentowałem nowy szczegółowy mechanizm kolizji który pozwalał na uzyskanie informacji o typie zdarzenia i wskaźników do ciał. Chcąc dowiedzieć się które ciało z ewidencji brało udział w kolizji musimy napisać prostą funkcję która będzie nam identyfikować który obiekt z listy jest posiadaczem ciała z kolizji. Funkcja wygląda następująco:


struct Searchers
{
    template< typename T>
    static T* test( vector<T>& list, b2Body* body )
    {
        T* toReturn = nullptr;
        if(body != nullptr)
        {
            for(auto& item : list)
            {
                if(item.getBody() == body){
                    toReturn = &item;
                    break;
                }
            }
        }
        return toReturn;
    }
};

Zdecydowałem się na użycie szablonu gdyż dzięki temu mamy możliwość stosowania funkcji do każdego wektora który zawiera elementy posiadające metodę getBody(). Niestety ale naszej funkcji z typem ogólnym i nijak nie możemy zabezpieczyć przed nieprawidłowym użyciem, jak chociażby w Javie. Aczkolwiek, można dojść do wniosku że funkcja sama się zabezpieczy i jej reakcją obronną będzie wyłożenie całej aplikacji.

Grunt to stabilizacja

Niestety ale pisząc aplikację w SFML z użyciem Box2D bardzo często zdarzało mi się że domyślnie dostępna funkcja ograniczenia klatek na sekundę z API potrafiła zachowywać się w nieprzewidywalny sposób. Rozwiązaniem okazało się napisanie własnego mechanizmu stabilizującego wartość uśpienia wątku. Deklaracja i implementacja wygląda następująco:


class FpsStabilizer
{
public:
    FpsStabilizer(const int fpsValue);

    void work();
    void setLog(const bool show);

private:
    const int ERROR_MARGIN = 5;

	sf::Clock m_clockBuffer;
	sf::Time m_timeBuffer;

    double getFPS();
    double m_sleepValue;

	int m_miniFPS;
	int m_maxFPS;

	bool m_showLog; 
};

FpsStabilizer::FpsStabilizer(const int fpsValue) :
    m_sleepValue(0),
    m_showLog(false)
{
    m_maxFPS  = (fpsValue + ERROR_MARGIN);
    m_miniFPS = (fpsValue - ERROR_MARGIN);
}

void FpsStabilizer::setLog(const bool show){
    m_showLog = show;
}

void FpsStabilizer::work()
{
	m_timeBuffer = m_clockBuffer.restart();

    float currentFPS = getFPS();

    if (currentFPS < m_miniFPS)
    {
        m_sleepValue -= 0.001
                ;
    } else if (currentFPS > m_maxFPS){

        m_sleepValue += 0.001;
    }

    if(m_showLog){
        std::cout
            << "minFPS: " << m_miniFPS
            << " maxFPS: " << m_maxFPS
            << " currentFPS: " << currentFPS
            << "\n";
    }

	sf::sleep(sf::seconds((float)m_sleepValue));
}

double FpsStabilizer::getFPS(){

    return (1000000.0 / m_timeBuffer.asMicroseconds());
}

Zasada działania jest bardzo prosta, z każdym obiegiem pętli głównej wyliczamy ilość FPS'ów w naszym oknie po czym w zależności od wartości zwiększamy lub zmniejszamy wartość usypiania głównego wątku. Funkcje work() wywołujemy w pętli głównej.

Mając nowy test skoku, stabilizator klatek, klasę opakowującą i metodę wyszukiwania możemy przejść do funkcji głównej:


int main(int argc, char *argv[])
{
    ContactDetector 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 textureEnemy;
    if(!textureEnemy.loadFromFile("textureEnemy.png")){
        std::cout << "textureEnemy problem \n";
    }
    sf::Texture textureWall;
    if(!textureWall.loadFromFile("textureWall.png")){
        std::cout << "textureMap problem \n";
    }

    sf::Texture texturePlayer;
    if(!texturePlayer.loadFromFile("textureHero.png")){
        std::cout << "texturePlayer problem \n";
    }

    /* Glowna podloga. */
    std::shared_ptr<BodyUserData>bodyPlatformData (
                new BodyUserData(BodyUserData::Type::Map));

    DrawablePolygonBody bodyPlatform(
                createStaticBody(myWorld.get(), 750, 40));

    bodyPlatform.setPosition(400.f, 450.f);
    bodyPlatform.setTexture(textureMap);
    bodyPlatform.getBody()->SetUserData((void*)bodyPlatformData.get());

    /* Sciana pionowa A . */
    std::shared_ptr<BodyUserData>bodyDataWallA (
                new BodyUserData(BodyUserData::Type::Wall));

    DrawablePolygonBody bodyWallA(
                createStaticBody(myWorld.get(), 40, 300));

    bodyWallA.setPosition(25.f, 325.f);
    bodyWallA.setTexture(textureWall);
    bodyWallA.getBody()->SetUserData((void*)bodyDataWallA.get());

    /* Sciana pionowa B . */
    std::shared_ptr<BodyUserData>bodyDataWallB (
                new BodyUserData(BodyUserData::Type::Wall));

    DrawablePolygonBody bodyWallB(
                createStaticBody(myWorld.get(), 40, 300));

    bodyWallB.setPosition(775.f, 325.f);
    bodyWallB.setTexture(textureWall);
    bodyWallB.getBody()->SetUserData((void*)bodyDataWallB.get());

    std::vector<DrawablePolygonBody> listWorldBodies;
    listWorldBodies.push_back(bodyPlatform);
    listWorldBodies.push_back(bodyWallA);
    listWorldBodies.push_back(bodyWallB);

    /* Nowe abstrakcje */
    MovableBody playerItem(
                new DrawablePolygonBody(
                        createDynamicBody(myWorld.get(), 40, 40)),
                BodyUserData::Type::Player);

    playerItem.getRenderShape()->setTexture(texturePlayer);
    playerItem.getRenderShape()->setPosition(400.f, 10.f);
    playerItem.getMover()->setJumpForce(4.5f);

    MovableBody enemyItem(
                new DrawablePolygonBody(
                        createDynamicBody(myWorld.get(), 90, 60)),
                BodyUserData::Type::Enemy);
    enemyItem.getRenderShape()->setTexture(textureEnemy);
    enemyItem.getRenderShape()->setPosition(600.f, 50.f);
    enemyItem.getMover()->setJumpForce(3.f);
    enemyItem.getMover()->setMaxSpeed(2.5f);

    MovableBody enemyItemB(
                new DrawablePolygonBody(
                        createDynamicBody(myWorld.get(), 90, 60)),
                BodyUserData::Type::Enemy);
    enemyItemB.getRenderShape()->setTexture(textureEnemy);
    enemyItemB.getRenderShape()->setPosition(150.f, 50.f);
    enemyItemB.getMover()->setJumpForce(3.f);
    enemyItemB.getMover()->setMaxSpeed(2.5f);

    /* Ewidencja wrogow. */
    std::vector<MovableBody> listEnemies;
    listEnemies.push_back(enemyItem);
    listEnemies.push_back(enemyItemB);

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

    sf::Color backgroundColor;

    FpsStabilizer stabilizer(60);

    while(window.isOpen())
    {
        stabilizer.work();

        /* Render-cleaner. */
        backgroundColor = sf::Color::Black;
        for(MovableBody& item : listEnemies){
            item.getRenderShape()->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))
        {
            playerItem.getMover()->move(BodyMover::Direction::Jump);
        }

        if(sf::Keyboard::isKeyPressed(sf::Keyboard::D))
        {
            playerItem.getMover()->move(BodyMover::Direction::Right);
        }

        if(sf::Keyboard::isKeyPressed(sf::Keyboard::A))
        {
            playerItem.getMover()->move(BodyMover::Direction::Left);
        }

        beforeGameLoop(*myWorld.get());

        /* Dla kazdego przeciwnika z ewidencji
         * sprawdz czy ten, widzi gracza. */
        for(auto& enemyItem : listEnemies)
        {
            const Observer::Info isEnemySeePlayerData =
                                Observer::isBodySeeBody(
                                    enemyItem.getBody(),
                                    playerItem.getBody(), 150);

            if(isEnemySeePlayerData.isSee)
            {
                BodyMover* movePtr = enemyItem.getMover();

                switch(isEnemySeePlayerData.side)
                {
                    case Observer::Info::Side::Left: {
                        movePtr->move(BodyMover::Direction::Left);
                        break;
                    }

                    case Observer::Info::Side::Right: {
                        movePtr->move(BodyMover::Direction::Right);
                        break;
                    }
                }

                backgroundColor = sf::Color::Yellow;
            }
        }

        /* Kolizje. */
        const bool contactCondition =
                (!myContactLister.isContactListIsEmpty()) &&
                    myContactLister.isContactListContains(
                        ContactDetector::Contact::Type::PlayerTouchEnemy);

        if(contactCondition)
        {
            std::vector<ContactDetector::Contact::Info> enemyContacts =
                    myContactLister.getContactList(
                        ContactDetector::Contact::Type::PlayerTouchEnemy);

            if(!enemyContacts.empty()){

                /* Wiemy ze miedzy przeciwnikami a
                 * graczem moze zajsc wiecej niz jedna
                 * kolizja. Stad musimy odnalezc przeciwnika
                 * do ktorego nalezy cialo z ktorym zaszla
                 * kolizja gracza. */
                for(auto& contact : enemyContacts)
                {
                    MovableBody* ptr =
                            Searchers::test<MovableBody>(
                                listEnemies,
                                contact.bodyPtrSecond);

                    if(ptr != nullptr)
                    {
                        ptr->getRenderShape()->setColor(sf::Color::Green);

                        const float enemyPositionY =
                                ptr->getRenderShape()->getPosition().y;

                        const float playerPositionY =
                                playerItem.getRenderShape()->getPosition().y;

                        if(playerPositionY < enemyPositionY-10 ){

                            ptr->getMover()->move(BodyMover::Direction::Jump);

                        }
                        backgroundColor = sf::Color::Red;
                    }
                }
            }
        }

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

        playerItem.getRenderShape()->update();
        playerItem.getRenderShape()->render(window);

        for(MovableBody& item : listEnemies){
            item.getRenderShape()->update();
            item.getRenderShape()->render(window);
        }

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

        window.display();
    }
    return 0;
}

Co może nam się rzucić w oko po przejrzeniu pętli głównej? Na pewno fakt że rozdzieliliśmy kolizje ze skokiem przeciwnika od sekcji w której sprawdzamy czy wróg widzi naszego bohatera. Postanowiłem nie zlewać tych dwóch sekcji pomimo faktu że logiczne jest że jak przeciwnik nie widzi gracza to nie ma sensu sprawdzać kolizji. Podział ten przyda nam się jednak w późniejszych etapach rzeźbienia kiedy będziemy pisać klasę dekoratora z funkcjami zachowań dla przeciwników. Przed pętlą główną możemy zaobserwować przykład stosowania nowej klasy fasadowej. Wszystkie niezbędne elementy trzymamy w jednym obiekcie a użycie wydaje się zdecydowanie czytelniejsze. Dzięki zmianom mamy możliwość efektywnego sprawdzania kolizji między bohaterami z możliwością prowadzenia prostej ewidencji obiektów. Dodatkowo, przechwytywanie zdarzeń z klawiatury przenieśliśmy z pętli zdarzeń do głównej dzięki czemu nasz bohater porusza się szybciej, wzrosła jego dynamika ruchu. Jeżeli nie wierzysz - sprawdź to. ;) Efekt prezentuje się następująco:

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.