Blog (35)
Komentarze (574)
Recenzje (0)
@biomenNotatki programisty: im nas więcej tym weselej, czyli porządkujemy kod i tworzymy grupę niezależnych przeciwników

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

17.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:

613042

Jak zawsze dzięki za uwagę!

Wybrane dla Ciebie
Komentarze (5)