Notatki programisty: zawód? Magazynier, czyli piszemy menadżer zasobów w aplikacji SFML/Box2D
18.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ę!