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

Notatki programisty: zawód? Magazynier, czyli piszemy menadżer zasobów w aplikacji SFML/Box2D

@biomenNotatki programisty: zawód? Magazynier, czyli piszemy menadżer zasobów w aplikacji SFML/Box2D18.01.2017 13:25

W miarę jak rozrasta się nasz projekt, ilość zasobów z jakich jesteśmy zmuszeni skorzystać niebywale rośnie. W niniejszym wpisie przedstawię swój patent na zarządzanie zasobami. Co więcej, uporządkujemy sekcje kodu odpowiedzialną za zachowanie przeciwnika. Zapraszam do lektury.

Dekorujemy poczwarę

W poprzednim wpisie, argumentowałem oddzielenie kodu odpowiedzialnego za sprawdzanie czy przeciwnik widzi gracza od kodu odpowiedzialnego za kolizję tym że w przyszłości ten niewydajny podział ułatwi nam napisanie klasy dekorującej dla obiektów przeciwnika. Nie przedłużając, oto jak ta klasa i jej implementacja będzie wyglądać:


class EnemyBehavior
{
public:
    EnemyBehavior(MovableBody* baseBody);

    bool moved(const b2Body* playerBody);

    bool jumped(const b2Body* playerBody);

private:
    MovableBody* m_ptr;
};
EnemyBehavior::EnemyBehavior(MovableBody* baseBody)
    : m_ptr(baseBody) {}

/* Zwraca true gdy przeciwnik
 * zobaczyl gracza i sie poruszyl. */
bool EnemyBehavior::moved(const b2Body* playerBody)
{
    bool toReturn = false;

    if(playerBody != nullptr)
    {
        const Observer::Info
                isEnemySeePlayerData =
                    Observer::isBodySeeBody(
                        m_ptr->getBody(),
                        playerBody, 150);

        if(isEnemySeePlayerData.isSee)
        {
            BodyMover* movePtr = m_ptr->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;
                }
            }
        }
        toReturn = isEnemySeePlayerData.isSee;
    }

    return toReturn;
}

/* Zwraca true gdy przeciwnik gracz
 * wskoczyl na przeciwnika i ten skoczyl
 * by go zrzucic. */
bool EnemyBehavior::jumped(const b2Body* playerBody)
{
    bool toReturn = false;

    if(playerBody != nullptr){
        const float enemyPositionY =
                m_ptr->getRenderShape()->getPosition().y;

        const float playerPositionY =
                playerBody->GetPosition().y*50;

        if(playerPositionY < enemyPositionY-10 ){

            m_ptr->getMover()->move(BodyMover::Direction::Jump);
            toReturn = true;
        }
    }

    return toReturn;
}

Ot, nic szczególnego, przenieśliśmy linijki kodu z pętli głównej do metod. Ktoś patrząc na kod mógłby zapytać dlaczego by wskaźnika do ciała bohatera nie uczynić składową klasy? Cóż zastanawiałem się nad tym aczkolwiek stwierdziłem że z projektowego punktu widzenia klasa odpowiedzialna jest tylko za „skryptowane” zachowanie i nie powinna zawierać w sobie informacji na ten temat. Taka „optymalizacja” w przyszłości mogłaby negatywnie wpłynąć na zachowanie gdy w jednej chwili przeciwnik miałby sprawdzać czy obserwuje dwa obiekty, coś takiego:



EnemyBehavior enemyDecorator(&enemyItem);
enemyDecorator.moved(someBodyA);
enemyDecorator.moved(someBodyB);

Umieszczając wskaźnik do ciała obserwowanego w klasie musielibyśmy go jakoś na nowo przypisywać itp. Tak więc pisząc „sprytne” rozwiązania warto się zastanowić czy przypadkiem ten cwany kod nie sprawi nam w przyszłości, więcej szkody niż pożytku.

Górnicy, kopalnie i zasoby

Przejdźmy teraz do głównego punktu wpisu czyli menadżera zasobami. Przed rozpoczęciem prac konieczne jest abyśmy do naszego projektu dołączyli bibliotekę PhysicsFS. Opis jej budowy i dołączania do projektu opisałem w jednym z poprzednich tekstów. Przed przystąpieniem do pisania kodu, wypadałoby omówić jak mechanizm zarządzania plikami jest zbudowany. Otóż wszystkie nasze zasoby, tekstury itp. będziemy przechowywać jako wartość mapy, kluczem zaś do obiektów mapy będzie prosty tryb wyliczeniowy. Możemy sobie to zobrazować w łatwy sposób że nasz enum będzie nazwą szuflady w której trzymamy zasób, a zawartość szuflady samym zasobem. Jako że w przykładzie korzystam jedynie z tekstur, słów zasób i tekstura będę używać zamiennie.

Kolejną kwestią jaką należy rozpatrzeć jest źródło skąd bierzemy zasoby. Do tej pory ładowaliśmy je z pliku aczkolwiek w poważnym świecie nie można sobie pozwolić na sytuację w której to obiekty, tekstury, audio naszej produkcji leżą w stanie surowym na dysku u konsumenta. Stąd wniosek że potrzebujemy dwóch mechanizmów do ładowania tekstur: jeden do ładowania z pliku, drugi do ładowania z archiwum. W celu napisania tego mechanizmu, posłużymy się wzorcem projektowym strategii gdyż idealnie nadaje się do użycia w wyżej przedstawionej sytuacji.

Następną sprawą która ułatwi nam życie będą dane współdzielone między mechanizmy ładowania. O co chodzi? Mając ścieżki do plików graficznych warto jest je zsynchronizować między tym jak ładujemy pliki z folderu a tym jak ładujemy je z archiwum gdy aplikacja jest już u klienta. Wszystkie ścieżki używanych plików będziemy trzymać w prostym singletonie. Myślę że kod będzie wystarczająco zrozumiały. Na koniec, chcielibyśmy aby klasy odpowiedzialne za ładowanie zasobów były niewidoczne dla reszty modułów aplikacji. Dlatego całość zamkniemy w jednej klasie. W ten oto sposób wszelkie mechanizmy zarządzania zasobami mamy dostępne w jednym miejscu bez siatki rozplątanych zależności. Oto jak będzie wyglądać nasz moduł:


class Assets
{
public:
    enum Textures : int
    {
        Map,
        Wall,
        Enemy,
        Player
    };

private:
    class Paths
    {
        public:

            const std::string TEXTURE_MAP    = std::string("textures/map.png");
            const std::string TEXTURE_WALL   = std::string("textures/wall.png");
            const std::string TEXTURE_ENEMY  = std::string("textures/enemy.png");
            const std::string TEXTURE_PLAYER = std::string("textures/player.png");

            static Paths* get();

        private:
            static std::shared_ptr<Paths> m_ptr;
    };

    class BaseLoader
    {
    public:
        virtual void loadTextures(
                std::map<Textures,
                std::shared_ptr<sf::Texture>>& list) = 0;
    };

    class FileLoader : public BaseLoader
    {
    public:
        void loadTextures(
                std::map<Textures,
                std::shared_ptr<sf::Texture>>& list);
    };

    class ZipLoader : public BaseLoader
    {
    public:
        ZipLoader(const std::string& fileName);

        void loadTextures(
            std::map<Textures,
            std::shared_ptr<sf::Texture>>& list);

    private:
        const std::string m_fileName;

        sf::Texture* getTextureFromZip(std::string name) const;
    };

public:
    class Resources
    {
    public:
        Resources();

        Resources(std::string zipFile);

        sf::Texture* getTexture(Assets::Textures id) const;

    private:
        std::shared_ptr<BaseLoader> m_loader;
        std::map<Textures,
            std::shared_ptr<sf::Texture>> m_textures;
    };

};

Sporo tego aczkolwiek po głębszy zapoznaniu się z kodem, wszystko powinno być jasne. Tak jak pisałem, mamy tryb wyliczeniowy który jest naszym kluczem do map, mamy klasy odpowiedzialne za ładowanie plików, klasę danych współdzielonych ze ścieżkami do plików oraz klasę przez którą uzyskujemy dostęp do zasobów. Implementacja funkcji klas wygląda następująco:


/* Paths .cpp */
std::shared_ptr<Assets::Paths> Assets::Paths::m_ptr = nullptr;

Assets::Paths* Assets::Paths::get()
{
    if(m_ptr.get() == nullptr){
        m_ptr = std::make_shared<Paths>(Paths());
    }
    return m_ptr.get();
}

/* File loader .cpp */
void Assets::FileLoader::loadTextures(
            std::map<Textures,
            std::shared_ptr<sf::Texture>>& list)
{
    sf::Texture textureMap;
    if(!textureMap.loadFromFile(Paths::get()->TEXTURE_MAP)){
        std::cout << Paths::get()->TEXTURE_MAP + " -problem \n";
    }

    sf::Texture textureEnemy;
    if(!textureEnemy.loadFromFile(Paths::get()->TEXTURE_ENEMY)){
        std::cout << Paths::get()->TEXTURE_ENEMY + " -problem \n";
    }

    sf::Texture textureWall;
    if(!textureWall.loadFromFile(Paths::get()->TEXTURE_WALL)){
        std::cout << Paths::get()->TEXTURE_WALL + " -problem \n";
    }

    sf::Texture texturePlayer;
    if(!texturePlayer.loadFromFile(Paths::get()->TEXTURE_PLAYER)){
        std::cout << Paths::get()->TEXTURE_PLAYER + " -problem \n";
    }

    list[Assets::Textures::Map] = std::make_shared<sf::Texture>(textureMap);
    list[Assets::Textures::Wall] = std::make_shared<sf::Texture>(textureWall);
    list[Assets::Textures::Enemy] = std::make_shared<sf::Texture>(textureEnemy);
    list[Assets::Textures::Player] = std::make_shared<sf::Texture>(texturePlayer);
}

/* Zip loader .cpp */
Assets::ZipLoader::ZipLoader(const std::string& fileName)
    : m_fileName(fileName) { }

void Assets::ZipLoader::loadTextures(
        std::map<Textures,
        std::shared_ptr<sf::Texture>>& list)
{
    PHYSFS_init(nullptr);
    PHYSFS_addToSearchPath(m_fileName.c_str(), 1);

    std::shared_ptr<sf::Texture> textureMap(
                getTextureFromZip(Paths::get()->TEXTURE_MAP));

    std::shared_ptr<sf::Texture> textureWall(
                getTextureFromZip(Paths::get()->TEXTURE_WALL));

    std::shared_ptr<sf::Texture> textureEnemy(
                getTextureFromZip(Paths::get()->TEXTURE_ENEMY));

    std::shared_ptr<sf::Texture> texturePlayer(
                getTextureFromZip(Paths::get()->TEXTURE_PLAYER));

    list[Assets::Textures::Map]     = textureMap;
    list[Assets::Textures::Wall]    = textureWall;
    list[Assets::Textures::Enemy]   = textureEnemy;
    list[Assets::Textures::Player]  = texturePlayer;

    PHYSFS_deinit();
}

sf::Texture*
Assets::ZipLoader::getTextureFromZip(std::string name) const
{
    sf::Texture* texturePtr = nullptr;

    if(PHYSFS_exists(name.c_str()))
    {
        PHYSFS_File* fileFromZip = PHYSFS_openRead(name.c_str());

        const int fileLenght = PHYSFS_fileLength(fileFromZip);
        char* buffer = new char[fileLenght];
        int readLength = PHYSFS_read (fileFromZip, buffer, 1, fileLenght);

        texturePtr = new sf::Texture();
        texturePtr->loadFromMemory(buffer, readLength);

        PHYSFS_close(fileFromZip);

    } else {
        std::cout << name + " - doesn't exists. \n";
    }
    return texturePtr;
}

/* Resources .cpp */
Assets::Resources::Resources()
{
    m_loader = std::make_shared<FileLoader>(FileLoader());
    m_loader->loadTextures(m_textures);
}

Assets::Resources::Resources(std::string zipFile)
{
    m_loader = std::make_shared<ZipLoader>(ZipLoader(zipFile));
    m_loader->loadTextures(m_textures);
}

sf::Texture* Assets::Resources::getTexture(Assets::Textures id) const
{
    return m_textures.at(id).get();
}

To czy użyjemy trybu plikowego czy trybu archiwum zależy który konstruktor klasy Resources wywołamy. Jak można zauważyć, w przypadku ładowania plików z archiwum wykorzystałem funkcje z wpisu opisującego PhysicsFS.. Warto jest pisać modułowe oprogramowanie. Jeżeli nie czytałeś tego wpisu to nadrób zaległości jak najszybciej. ;)

Coraz czyściej

W ten oto sposób dobrnęliśmy do momentu w którym nadszedł czas na danie główne, czyli funkcję main:



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

    Assets::Resources resources("data.zip");

    /* 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(*resources.getTexture(Assets::Textures::Map));
    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(*resources.getTexture(Assets::Textures::Wall));
    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(*resources.getTexture(Assets::Textures::Wall));
    bodyWallB.getBody()->SetUserData((void*)bodyDataWallB.get());

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

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

    playerItem.getRenderShape()->setTexture(
                *resources.getTexture(Assets::Textures::Player));

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

    /* Enemy A. */
    MovableBody enemyItem(
                new DrawablePolygonBody(
                        createDynamicBody(myWorld.get(), 90, 60)),
                BodyUserData::Type::Enemy);

    enemyItem.getRenderShape()->setTexture(
                *resources.getTexture(Assets::Textures::Enemy));

    enemyItem.getRenderShape()->setPosition(600.f, 50.f);
    enemyItem.getMover()->setJumpForce(3.f);
    enemyItem.getMover()->setMaxSpeed(2.5f);

    /* Enemy B. */
    MovableBody enemyItemB(
                new DrawablePolygonBody(
                        createDynamicBody(myWorld.get(), 90, 60)),
                BodyUserData::Type::Enemy);

    enemyItemB.getRenderShape()->setTexture(
                *resources.getTexture(Assets::Textures::Enemy));

    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)
        {
            EnemyBehavior enemyDecorator(&enemyItem);
            if(enemyDecorator.moved(playerItem.getBody())){
                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()){

                for(auto& contact : enemyContacts)
                {
                    MovableBody* ptr =
                            Searchers::test<MovableBody>(
                                listEnemies,
                                contact.bodyPtrSecond);

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

                        EnemyBehavior dec(ptr);
                        if(dec.jumped(playerItem.getBody())){
                            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;
}

Jak widać, napisanie funkcji dekoratora dla obiektu przeciwnika i menadżera zasobów opłaciło się. Nasza funkcja główna zdecydowanie się skróciła. Dotychczasowe tekstury spakowałem do archiwum data.zip. Jedyne czego musiałem przypilnować to tego czy w archiwum faktycznie znajdują się pliki o ścieżkach podanych w klasie Paths. Mimo iż w tym wpisie nie ma żadnych wizualnych zmian w samej symulacji, nasza aplikacja zyskała dodatkowe mechanizmy dzięki którym jej przyszła rozbudowa będzie zdecydowanie łatwiejsza a zarządzanie prostsze niż kiedykolwiek.

Jak zwykle, 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.