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

Notatki programisty: Dobra zmiana, czyli restrukturyzacja aplikacji Box2D/SFML

@biomenNotatki programisty: Dobra zmiana, czyli restrukturyzacja aplikacji Box2D/SFML23.01.2017 10:52

Czasem aby zrobić dwa kroki do przodu, trzeba zrobić jeden w tył. Ta myśl zawitała w mojej głowie gdy zastanawiałem się nad tematem kolejnego wpisu z serii. W poniższym tekście poprawimy część założeń projektowych, rozwiniemy istniejące mechanizmy i wzbogacimy aplikację o nowe funkcje. Zapraszam do lektury.

Kroki w przód, kroki w tył

Przeglądając kod zaprezentowany w poprzednim wpisie doszedłem do wniosku że trochę się zapędziłem z „ewolucją” klas. Dotychczas prezentowała się ona następująco:


b2Body › DrawablePolygonBody › MovableBody

Jednak bystre oko sprytnego obserwatora może zauważyć, gdzieś zginęła nam klasa która tylko i wyłącznie, odpowiadałaby za identyfikacje typu ciała. Dlatego od dziś nasza droga ewolucji będzie wyglądać jak poniżej:


b2body › DrawablePolygonBody › IdentifiedBody › MovableBody

Jako że w „specyfikacji” naszego dema technologicznego widnieje wymaganie dotyczące tego że każde ciało symulacji ma być możliwe do zidentyfikowania, zapis powyższy jak na razie powinien być prawidłowy. Piszę że na ten moment rozwiązanie jest wystarczająco dobre gdyż nie zwieliśmy jeszcze na warsztat animowany spriteów. Ale o nich kiedy indziej. Klasa IdentifiedBody z implementacją prezentuje się następująco:


class IdentifiedBody
{
public:
    IdentifiedBody(
            DrawablePolygonBody* renderShape,
            BodyUserData::Type type);

    b2Body* getBody() const;
    BodyUserData::Type getBodyType() const;
    DrawablePolygonBody* getRenderShape() const;

protected:
    std::shared_ptr<DrawablePolygonBody> m_shape;
    std::shared_ptr<BodyUserData> m_data;

};

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

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

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

BodyUserData::Type IdentifiedBody::getBodyType() const
{
    BodyUserData* bodyUserData =
            (BodyUserData*)(m_shape->getBody()->GetUserData());
    return bodyUserData->getType();
}

Teraz mamy kilka wariantów do wyboru w jaki sposób pociągnąć zależność między IdentifiedBody a MovableBody. Zdecydowałem się na proste dziedziczenie dzięki któremu klasa do poruszania ciała będzie jak najbardziej minimalna. Dodatkowo moglibyśmy zmodyfikować parametry konstruktora jednak dla zachowania zgodności ze starym kodem zdecydowałem się tylko na subtelne modyfikacje. Oto jak będzie wyglądać klasa po naszych modyfikacjach:


class MovableBody : public IdentifiedBody
{

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

    BodyMover* getMover() const;

private:
    std::shared_ptr<BodyMover> m_mover;
};

MovableBody::MovableBody(
        DrawablePolygonBody* renderShape,
        BodyUserData::Type type) :
    IdentifiedBody(renderShape, type),
    m_mover(new BodyMover(renderShape->getBody()))
{

}

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

Uzupełnienie do menadżera zasobów

W poprzednim wpisie do naszego dema dodaliśmy moduł zarządzania zasobami. Dzisiaj dodamy do niego obsługę zasobów audio i czcionek. Jako że używam tylko i wyłącznie ładowania z pliku zip, zaprezentuje implementacje funkcji odpowiedzialnych tylko i wyłącznie za ładowanie z archiwum. Analogiczne funkcje ładowania z pliku możesz napisać sam. ;) Ścieżka czcionki jest pozostałością wersji rozwojowej przez co na razie nie musimy zaprzątać sobie nią głowy. Oto jak będzie wyglądać nasz moduł i implementacje wyżej wspomnianych funkcji:


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

    enum Sounds : int
    {

    } ;

    enum Fonts : int
    {
        NOTATOL
    };

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");
            const std::string TEXTURE_APP_ICON = std::string("textures/icon.png");

            const std::string FONT_NOTATOL = std::string("fonts/notalot.ttf");

            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;

        virtual void loadFonts(
                std::map<Fonts,
                std::shared_ptr<sf::Font>>& list) = 0;

        virtual void loadSoundBuffers(
                std::map<Fonts,
                std::shared_ptr<sf::SoundBuffer>>& list) = 0;
    };

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

        void loadFonts(
                std::map<Fonts,
                std::shared_ptr<sf::Font>>& list);

        void loadSoundBuffers(
                std::map<Fonts,
                std::shared_ptr<sf::SoundBuffer>>& list);
    };

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

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

        void loadFonts(
                std::map<Fonts,
                std::shared_ptr<sf::Font>>& list);

        void loadSoundBuffers(
                std::map<Fonts,
                std::shared_ptr<sf::SoundBuffer>>& list);

    private:
        const std::string m_fileName;

        sf::Font* getFontFromZip(std::string name) const;
        sf::Texture* getTextureFromZip(std::string name) const;
        sf::SoundBuffer* getSoundBufferFromZip(std::string name) const;
    };

public:
    class Resources
    {
    public:
        Resources();

        Resources(std::string zipFile);

        sf::Font* getFont(Assets::Fonts id) const;
        sf::Texture* getTexture(Assets::Textures id) const;
        sf::SoundBuffer* getSoundBuffer(Assets::Sounds id) const;

    private:
        std::shared_ptr<BaseLoader> m_loader;

        std::map<Textures,
            std::shared_ptr<sf::Texture>> m_textures;

        std::map<Fonts,
            std::shared_ptr<sf::Font>> m_fonts;

        std::map<Sounds,
            std::shared_ptr<sf::SoundBuffer>> m_soundBuffers;
    };

};

/* Zip loader. */
void Assets::ZipLoader::loadFonts(
        std::map<Fonts,
        std::shared_ptr<sf::Font> > &list)
{
    PHYSFS_init(nullptr);
    PHYSFS_addToSearchPath(m_fileName.c_str(), 1);

    std::shared_ptr<sf::Font> textureAppIcon(
                getFontFromZip(Paths::get()->FONT_NOTATOL));

    list[Assets::Fonts::NOTATOL] = textureAppIcon;

    PHYSFS_deinit();
}

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

    /* Na razie nie mamy zasobow
     * ktore moglibysmy ladowac */
    PHYSFS_deinit();
}

sf::Font*
Assets::ZipLoader::getFontFromZip(std::string name) const
{
    sf::Font* ptrToReturn = nullptr;

    if(PHYSFS_exists(name.c_str()))
    {
        std::cout << name + " - exists. \n";

        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);

        ptrToReturn = new sf::Font();
        ptrToReturn->loadFromMemory(buffer, readLength);

        PHYSFS_close(fileFromZip);

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

    return ptrToReturn;
}

sf::SoundBuffer*
Assets::ZipLoader::getSoundBufferFromZip(std::string name) const
{
    sf::SoundBuffer* soundBufferPtr = 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);

        soundBufferPtr = new sf::SoundBuffer();
        soundBufferPtr->loadFromMemory(buffer, readLength);

        PHYSFS_close(fileFromZip);

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

    return soundBufferPtr;
}

/* Resources. */
sf::Font* Assets::Resources::getFont(Assets::Fonts id) const
{
    return m_fonts.at(id).get();
}

sf::SoundBuffer* Assets::Resources::getSoundBuffer(Assets::Sounds id) const
{
    return m_soundBuffers.at(id).get();
}

Nowa arena

Część dotycząca tworzenia mapy powtarza się w każdym wpisie, aby uszczuplić naszą funkcję startową, przeniesiemy instrukcje dotyczące tworzenia mapy do nowej funkcji. Dodatkowo chcielibyśmy w niej zamieścić obiekty niewidzialnej ściany, przez co musimy zmodyfikować klasę DrawablePolygonBody i dodać do niej dodatkowe metody. Modyfikacje klasy DPB wyglądają następująco:


void DrawablePolygonBody::render(sf::RenderWindow& window) const
{
    if(m_isVisable){
        window.draw(*m_renderObj.get());
    }
}

void DrawablePolygonBody::setVisable(const bool value)
{
    this->m_isVisable = value;
}

bool DrawablePolygonBody::isVisable() const
{
    return this->m_isVisable;
}

I nasza funkcja do tworzenia mapy:


void createArea(
        b2World* world,
        std::vector<IdentifiedBody>& list,
        Assets::Resources& resources)
{
    IdentifiedBody mapPlatformTop(
                new DrawablePolygonBody(
                    createStaticBody(world, 800, 40)),
                BodyUserData::Type::Map);

    mapPlatformTop.getRenderShape()->setPosition(400.f, 0.f);
    mapPlatformTop.getRenderShape()->setTexture(
                *resources.getTexture(Assets::Textures::Map));

    IdentifiedBody mapPlatformBottom(
                new DrawablePolygonBody(
                    createStaticBody(world, 800, 40)),
                BodyUserData::Type::Map);

    mapPlatformBottom.getRenderShape()->setPosition(400.f, 600.f);
    mapPlatformBottom.getRenderShape()->setTexture(
                *resources.getTexture(Assets::Textures::Map));

    IdentifiedBody mapWallLeft(
                new DrawablePolygonBody(
                    createStaticBody(world, 40, 800)),
                BodyUserData::Type::Wall);

    mapWallLeft.getRenderShape()->setPosition(0.f, 400.f);
    mapWallLeft.getRenderShape()->setVisable(false);
    mapWallLeft.getRenderShape()->setTexture(
                *resources.getTexture(Assets::Textures::Wall));

    IdentifiedBody mapWallRight(
                new DrawablePolygonBody(
                    createStaticBody(world, 40, 800)),
                BodyUserData::Type::Wall);

    mapWallRight.getRenderShape()->setPosition(800.f, 400.f);
    mapWallRight.getRenderShape()->setVisable(false);
    mapWallRight.getRenderShape()->setTexture(
                *resources.getTexture(Assets::Textures::Wall));

    list.push_back(mapPlatformTop);
    list.push_back(mapPlatformBottom);
    list.push_back(mapWallLeft);
    list.push_back(mapWallRight);
}

Więcej kontroli

Musimy rozpatrzyć przypadek w którym to w przyszłości, nasza aplikacja będzie pobierała informację dotyczące sterowania z pliku. Napiszemy prostą klasę do przechowywania ustawień naszego sterowania, zastosowanie zostanie zaprezentowane w funkcji startowej aplikacji. Dzięki niej zyskamy dostęp do ustawień sterowania bez potrzeby modyfikacji pętli głównej. Jej kod wygląda jak poniżej:


class ControlKeys
{
public:
    const sf::Keyboard::Key MOVE_LEFT  = sf::Keyboard::Key::Left;
    const sf::Keyboard::Key MOVE_RIGHT = sf::Keyboard::Key::Right;
    const sf::Keyboard::Key MOVE_JUMP  = sf::Keyboard::Key::Space;
    const sf::Keyboard::Key KEY_ATTACK = sf::Keyboard::Key::A;
    const sf::Keyboard::Key KEY_DEFENCE = sf::Keyboard::Key::D;
};

Danie główne

Kod funkcji głównej i startowej po modyfikacjach wygląda następująco.


int example()
{
    ContactDetector myContactLister;
    std::unique_ptr<b2World> myWorld(createWorld());
    myWorld.get()->SetContactListener(&myContactLister);

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

    std::vector<IdentifiedBody> listWorldBodies;

    createArea(myWorld.get(), listWorldBodies, resources);

    /* 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;
    ControlKeys playerControl;

    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(playerControl.MOVE_JUMP))
        {
            playerItem.getMover()->move(BodyMover::Direction::Jump);
        }

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

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

        beforeGameLoop(*myWorld.get());

        for(auto& enemyItem : listEnemies)
        {
            break;
        }

        /* 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)
                {
                    break;
                }
            }
        }

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

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

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

        for(auto& item : listWorldBodies){
            item.getRenderShape()->render(window);
        }

        window.display();
    }
    return 0;
}

int main(int argc, char *argv[])
{
    example();
}

Jak możemy zauważyć kod dekoratora przeciwnika został wyrzucony z pętli. Decyzja ta podyktowana była tym że pracując nad kodem w „gałęzi rozwojowej” doszedłem do wniosku że upakowanie zachowania przeciwnika w dekorator bardzo zawęziło mi możliwości definiowania zachowania obiektu. Dodatkowo przenieśliśmy kod tworzenia mapy do nowej funkcji. Jak widzimy, pomimo zmian w strukturze "ewolucji" klas, nowe rozwiązanie jest kompatybilne ze wcześniej napisanym kodem. Kod skryptujący zachowanie dodamy przy innej okazji. Nowa mapa i tekstury przeciwników sprawiają że rezultat naszych prac wygląda następująco:

Mimo iż zakres wprowadzonych zmian w obrębie wpisu jest dość niewielki, zmiany te pozytywnie wpłyną na modułowość i elastyczność naszej aplikacji dzięki czemu jej modyfikowanie w przyszłości będzie prostsze niż kiedykolwiek.

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.