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

Notatki programisty: Czas na wyzwanie, czyli piszemy obiekt przeciwnika w grze Box2D/SFML

@biomenNotatki programisty: Czas na wyzwanie, czyli piszemy obiekt przeciwnika w grze Box2D/SFML11.01.2017 20:01

Oprogramowanie nie jest doskonałe. Nie ma doskonałego oprogramowania, jestem zwolennikiem podejścia: program jest skończony gdy jest dość dobry i mamy sytuacje w której nie możemy z projektu nic wyciąć. Tym optymistycznym akcentem zapraszam do lektury kolejnego wpisu w którym poznajemy podstawy i tajniki rzemiosła pisania gier.

Dzień zaczynamy od napraw

W nawiązaniu do wstępu wpis ten, jak i poprzedni, zaczniemy od „ulubionej” czynności programistów, czyli naprawy błędów. Ba, poprawka będzie dotyczyć tego samego co i poprzednim tekście czyli skoku bohatera. Zasada działania testu czy gracz może wykonać skok polegała na sprawdzeniu czy bohater dotyka jakiejś powierzchni, aczkolwiek nie przewidzieliśmy przypadku gry powierzchnią tą będzie, pionowa ściana. Jako że w „specyfikacji” serii nie mam żadnych wzmianek o tym że demo technologiczne będzie dotyczyć gry ze Spider-man'em w roli głównej, zajmiemy się naprawą tego „ficzeru”.

W tym miejscu pozwolę sobie na małą dygresję apropo projektowania kodu. Pytanie filozoficzne na dziś, brzmi: Czy nie łamie zasad hermetyzacji danych, sytuacja w której to klasa służąca do wprawiania w ruch ciał wie o tym że ciała mają jakieś dodatkowe dane, typy itp.? Po chwili zastanowienia myślę że poprawna odpowiedź brzmi: łamie. Aby zachować jak największą modułowość i niezależność kodu, nie powinniśmy tworzyć klas mutantów. Ktoś może zapytać ale czemu nie przeniesiemy tego testu do klasy nasłuchującej zdarzenia całego świata? Testowałem rozwiązanie i doszedłem do wniosku że jest problematyczne, niedokładne oraz zwiększa, w moim odczuciu, bałagan i nieczytelność kodu. Tak więc podejmujemy decyzję by funkcję naszego testu przeniesiemy do nowej klasy/struktury której deklaracja i implementacja prezentuje się następująco:



struct BodyJumpValidator
{
    static bool test(b2Body* body);
};

bool BodyJumpValidator::test(b2Body* body)
{
    if(body->GetContactList() == nullptr){
        return false;
    }

    for (b2Contact* contact = body->GetContactList()->contact;
            contact != nullptr; contact = contact->GetNext())
    {
        BodyUserData* bodyUserDataA = (BodyUserData*)(
                    contact->GetFixtureA()->GetBody()->GetUserData());

        BodyUserData* bodyUserDataB = (BodyUserData*)(
                    contact->GetFixtureB()->GetBody()->GetUserData());

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

            bool bodyIsWall =
                    bodyUserDataA->getType() == BodyUserData::Type::Wall ||
                    bodyUserDataB->getType() == BodyUserData::Type::Wall;

            if(bodyIsWall){
                return false;
            }
        }
    }

    return true;
}

Posiadając wiedzę z poprzednich wpisów, kod nie powinien sprawiać problemów z rozumieniem tego co dzieje się w implementacji. W tym miejscu poznajemy kolejną niedogodność naszej technologii, mianowicie każde pionowe ściany na mapie będziemy musieli oznaczać jako BodyUserData::Type::Wall. Odbiegając od głównego wątku i wracając do projektowania i hermetyzacji. Czy aby dobrym rozwiązaniem jest zamieszczanie testu skoku w funkcji move()? Nie lepiej go wykonać w pętli głównej? Z jednej strony nasza pętla rozrośnie się o dwie linijki co jest cegiełką do nieczytelności ale z drugiej strony klasę odpowiedzialną za ruch powinno, czysto teoretycznie, guzik obchodzić jakieś warunki ruchu skoro zajmuje się samym ruchem. Jako że jestem programistą bez formalnego akademickiego wykształcenia, zostawię ten dylemat komuś „starszemu i mądrzejszemu”. Chętnie poznam zdanie kogoś innego. Wracając do tematu. Użycie nowego testu prezentuje się następująco w funkcji move():


        case Direction::Jump:
        {
            if(BodyJumpValidator::test(m_body)){
                const b2Vec2 oldVelocity = m_body->GetLinearVelocity();
                const b2Vec2 newVelocity = b2Vec2(oldVelocity.x, - m_bodyJumpForce);

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

Po tych wszystkich rozmyślaniach i poprawkach powinniśmy uzyskać efekt o jaki nam chodziło czyli: bohater nie może skakać z powietrza i przy zetknięciu z obiektem oznaczonym jako Type::Wall. Rozwiązanie przetestujemy w dalszej części wpisu.

Czas podjąć wyzwanie!

Każdemu kto czytał moje poprzednie wpisy, znajoma jest metodyka programowania „magic of copy-pase”. W prezentowanym przykładzie będziemy potrzebowali elastycznej funkcji do tworzenia ciał dynamicznych dla obiektu gracza jak i wspomnianego w tytule, przeciwnika. Funkcja wygląda następująco:


b2Body* createDynamicBody(
            b2World* world,
            const float width,
            const float height)
{
    /* Wielkosc ciala przesylamy
     * w parametrach. */
    b2PolygonShape bodyShape;
    bodyShape.SetAsBox(
                (width/2)*G_PIXELS_TO_METERES,
                (height/2)*G_PIXELS_TO_METERES);

    b2FixtureDef bodyFixture;
    bodyFixture.density = 0.1f;
    bodyFixture.friction = 0.2f;
    bodyFixture.shape = &bodyShape;

    b2BodyDef bodyDef;
    bodyDef.type = b2_dynamicBody;

    b2Body* myBody = world->CreateBody(&bodyDef);
    myBody->CreateFixture(&bodyFixture);

    /* Wylaczamy "naprawiona" rotacje,
     * stwierdzilem ze troche slabo wyglada
     * grzybek ktory po przewroceniu ma
     * narzady ruchu na policzku. */
    myBody->SetFixedRotation(true);
    return myBody;
}

Wygląda zrozumiale. Kolejną rzeczą jaka będzie nam niezbędna do uatrakcyjnienia naszego demka, będzie funkcja która pozwoli nam na określenie czy ciało A znajduje się w obszarze widoku ciała B. Wyjdę w tym miejscu nieco na hipokrytę gdyż z jednej strony zastanawiam się nad poprawnością projektu a z drugiej strony, jako rozwiązanie problemu proponuje następujące rozwiązanie:


struct ObserverData
{
    enum Side : int
    {
        Left,
        Right
    };
    Side side;
    bool isSee;
};

ObserverData isBodySeeBody(
        const b2Body* observer,
        const b2Body* target,
        const float seeRangeInPx)
{
    ObserverData toReturn;

    const b2Vec2 targetPosition = target->GetPosition();
    const float targetPositionX = targetPosition.x;
    const float targetPositionY = targetPosition.y;

    b2Vec2 observerPosition = observer->GetPosition();
    const float observerPositionX = observerPosition.x;
    const float observerPositionY = observerPosition.y;

    if (targetPositionX < observerPositionX + seeRangeInPx*G_PIXELS_TO_METERES &&
        targetPositionX > observerPositionX - seeRangeInPx*G_PIXELS_TO_METERES &&
        targetPositionY < observerPositionY + seeRangeInPx*G_PIXELS_TO_METERES &&
        targetPositionY > observerPositionY - seeRangeInPx)
    {
        toReturn.isSee = true;

        if(targetPositionX < observerPositionX){
            toReturn.side = ObserverData::Side::Left;
        } else {
            toReturn.side = ObserverData::Side::Right;
        }
    } else {
        toReturn.isSee = false;
    }

    return toReturn;
}

Metoda robi dwie rzeczy, sprawdza czy ciało A widzi ciało B a dodatkowo określa z której strony je widzi przy czym zwraca jakąś mało spójną abstrakcyjną strukturę. Zbrodnia jakich mało. Pomijając rozterki architektury, śmiało możemy przejść do krótkiego omówienia. Warto zauważyć że pobierając pozycje ciała Box2D pobieramy pozycje środka ciała a nie któreś z krawędzi. Przez co mając zasięg widzenia ustawiony na 100 pikseli warto pamiętać że odległość ta tyczy się odległości między środkami ciał a nie krawędziami ich kształtów. Warunek dotyczący tego czy ciało widzi ciało jest dość łopatologiczny. Czas na danie główne każdego tekstu serii czyli, przed państwem, funkcja główna:


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 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(), 500, 40));

    bodyPlatform.setPosition(420.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(150.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(650.f, 325.f);
    bodyWallB.setTexture(textureWall);
    bodyWallB.getBody()->SetUserData((void*)bodyDataWallB.get());

    /* Obiekt gracza. */
    std::shared_ptr<BodyUserData>playerData (
                new BodyUserData(BodyUserData::Type::Player));

    DrawablePolygonBody bodyPlayer(
                createDynamicBody(myWorld.get(), 40, 40));

    bodyPlayer.setTexture(texturePlayer);
    bodyPlayer.setPosition(300.f, 50.f);
    bodyPlayer.getBody()->SetUserData((void*)playerData.get());

    BodyMover playerMover(bodyPlayer.getBody());

    /* Obiekt przeciwnika, danych i jego poruszacz. */
    std::shared_ptr<BodyUserData>enemyData (
                new BodyUserData(BodyUserData::Type::Enemy));

    DrawablePolygonBody bodyEnemy(createDynamicBody(myWorld.get(), 90, 60));
    bodyEnemy.setTexture(textureEnemy);
    bodyEnemy.setPosition(500.f, 50.f);
    bodyEnemy.getBody()->SetUserData((void*)enemyData.get());

    BodyMover enemyMover (bodyEnemy.getBody());

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

    /* Okno SFML. */
    sf::RenderWindow window(
                sf::VideoMode(800, 600, 32),
                std::string("SFML/Box2D - tech demo"),
                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))
            {
                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);
            }
        }

        /* Troche niewydajne sie wydaje sprawdzanie
         * z kazdym obiegiem petli czy przeciwnik widzi
         * gracza aczkolwiek na razie przy nim zostaniemy.
         * Dzialajaca atrapa to dobra atrapa. ;) */
        ObserverData isEnemySeePlayerData =
                        isBodySeeBody(
                            bodyEnemy.getBody(),
                            bodyPlayer.getBody(), 150);

        /* Jezeli przeciwnik widzi gracza,
         * odczytujemy strone a nastepnie
         * wprawiamy w ruch cialo przeciwnika. */
        if(isEnemySeePlayerData.isSee){
            switch(isEnemySeePlayerData.side)
            {
                case ObserverData::Side::Left: {
                    enemyMover.move(BodyMover::Direction::Left);
                    break;
                }

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

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

        beforeGameLoop(*myWorld.get());
        bodyEnemy.update();
        bodyPlayer.update();

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

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

        window.display();
    }

    return 0;
}

W kodzie zamieściliśmy kilka warunków które poniekąd są skryptem zachowania naszego przeciwnika. Wszystkie elementy z jakich skorzystaliśmy zostały w każdym ze wpisów umówione. Po uruchomieniu przybliżony efekt naszych prac powinien przedstawiać się następująco:

Ot prosta mini gierka, nie dajemy się przygnieść przeciwnikowi do muru, co prawda nie mamy jeszcze warunków które decydują o tym czy wygraliśmy lub przegraliśmy ale to już temat na następne wpisy. Jak na razie: 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.