Blog (35)
Komentarze (574)
Recenzje (0)
@biomenNotatki programisty: Dobra zmiana, czyli restrukturyzacja aplikacji Box2D/SFML

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

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

613618

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ę!

Wybrane dla Ciebie
Komentarze (0)