Notatki programisty: integracja edytora z rdzeniem aplikacji SFML/Box2D
Słowem wstępu: jest to już piętnasty wpis z serii „Notatki programisty”. Przez ostatnie czternaście wpisów kod rozrósł się do dość pokaźnych rozmiarów. Według SourceMonitora kod zbliża się do granicy 4 tysięcy linii kodu, czy to dużo czy mało. Trudno mi powiedzieć. Mimo wszystko: uznałem że jest to doskonały moment do publikacji całego prezentowanego projektu w ramach artykułów. Przy prezentowaniu kodu, niestety, nie obyło się bez drobnych potknięć. Opublikowany kod posiada naniesione poprawki do błędów wykrytych przy integracji kodu. Na serwerze znajdują się dwie gałęzie w repozytorium. W niniejszym wpisie wykorzystamy kod zawarty w gałęzi: Release1 i właśnie to jej kod będziemy rozwijać w następnych tekstach. Link.
Budujemy mosty dla Pana starosty
Dotychczas w aplikacji nie posiadaliśmy żadnego pomostu między naszym prototypem edytora a faktycznym rdzeniem aplikacji w którym odbywała się symulacja świata. Pierwszą cegiełką mającą na celu zbudowanie odpowiedniego połączenia będzie dodanie algorytmu tworzenia świata na podstawie obiektu GameMap. Jak wiemy z poprzednich wpisów, w naszym demie świat jest tworzony przy pomocy specjalnego modułu MapBuilder przez co to właśnie jego w pierwszej kolejności musimy zmodyfikować. Do klasy dodamy nową klasę FromMap która będzie wyglądać następująco:
struct FromMap : public Base { FromMap(b2World* world, GameMap& map); void createArea( std::vector<std::shared_ptr<IdentifiedBody>>& list, Assets::Resources& resources); void createDynamicBodies( std::vector<std::shared_ptr<MovableBody>>& list, Assets::Resources& resources); GameMap* m_map; protected: struct FactoryMovable { struct Base { virtual ~Base() {} virtual MovableBody* create( b2World* world, Assets::Resources& resources, GameMap::Item* item) = 0; }; struct asPlayer : public Base { MovableBody* create( b2World *world, Assets::Resources& resources, GameMap::Item* item); }; struct asEnemy : public Base { MovableBody* create( b2World *world, Assets::Resources &resources, GameMap::Item* item); }; }; struct FactoryStatic { struct Base { virtual ~Base() {} virtual IdentifiedBody* create( b2World* world, Assets::Resources& resources, GameMap::Item* item) = 0; }; struct asMapFlor : public Base { IdentifiedBody* create( b2World *world, Assets::Resources& resources, GameMap::Item* item); }; struct asMapWall : public Base { IdentifiedBody* create( b2World *world, Assets::Resources& resources, GameMap::Item* item); }; }; void initFactoryMovable(); std::map<std::string, std::shared_ptr<FactoryMovable::Base>> m_factoryMovable; void initFactoryStatic(); std::map<std::string, std::shared_ptr<FactoryStatic::Base>> m_factoryStatic; };
Nowa klasa będzie służyć do tworzenia obiektów GameWorld na podstawie naszego modelu mapy. Co warto zauważyć: funkcje odpowiedzialne za tworzenie obiektu mapy na podstawie identyfikatora wydzieliliśmy do pomniejszych klasy dzięki czemu wyizolowaliśmy je od reszty kodu. Następnie je zmapujemy z identyfikatorem z klasy GameObjRegister. Podobny mechanizm zastosowaliśmy w naszym edytorze przez co warto zapoznać się z tym patentem na programowanie gdyż dosyć często będzie nam pomocny. Przykłady mapowania algorytmów były omawiane wcześniej stąd powyższy widok nie powinien nas przerażać. Czas na implementacje nowego budowniczego i jego pomocników:
/* From Map Model */ MapBuilder::FromMap::FromMap(b2World* world, GameMap& map) : Base(world), m_map(&map) { this->initFactoryMovable(); this->initFactoryStatic(); } void MapBuilder::FromMap::initFactoryMovable() { try { m_factoryMovable[GameObjRegister::get()->list.at(GameObjRegister::Index::Enemy)] = std::shared_ptr<FactoryMovable::Base>(new FactoryMovable::asEnemy()); m_factoryMovable[GameObjRegister::get()->list.at(GameObjRegister::Index::Player)] = std::shared_ptr<FactoryMovable::Base>(new FactoryMovable::asPlayer()); } catch (const std::out_of_range& ex){ std::cout << __func__ << " " << ex.what() << "\n"; } } void MapBuilder::FromMap::initFactoryStatic() { try { m_factoryStatic[GameObjRegister::get()->list.at(GameObjRegister::Index::MapFlor)] = std::shared_ptr<FactoryStatic::Base>(new FactoryStatic::asMapFlor()); m_factoryStatic[GameObjRegister::get()->list.at(GameObjRegister::Index::MapWall)] = std::shared_ptr<FactoryStatic::Base>(new FactoryStatic::asMapWall()); } catch (const std::out_of_range& ex) { std::cout << __func__ << " " << ex.what() << "\n"; } } void MapBuilder::FromMap::createArea( std::vector<std::shared_ptr<IdentifiedBody>>& list, Assets::Resources& resources) { for(int i = 0; i < m_map->height; ++i) { for(int j = 0; j < m_map->width; ++j) { GameMap::Item* item = &m_map->array[ i ][ j ]; MapBuilder::FromMap::FactoryStatic::Base* ptr = nullptr; try { ptr = m_factoryStatic.at(item->ID).get(); } catch (const std::out_of_range& ex){ ptr = nullptr; } if(ptr){ list.push_back( std::shared_ptr<IdentifiedBody>( ptr->create(m_world, resources, item))); } } } } void MapBuilder::FromMap::createDynamicBodies( std::vector<std::shared_ptr<MovableBody>>& list, Assets::Resources& resources) { for(int i = 0; i < m_map->height; ++i) { for(int j = 0; j < m_map->width; ++j) { GameMap::Item* item = &m_map->array[ i ][ j ]; MapBuilder::FromMap::FactoryMovable::Base* ptr = nullptr; try { ptr = m_factoryMovable.at(item->ID).get(); } catch (const std::out_of_range& ex){ ptr = nullptr; } if(ptr){ list.push_back( std::shared_ptr<MovableBody>( ptr->create(m_world, resources, item))); } } } } /* Algorytmy tworzenia */ MovableBody* MapBuilder::FromMap::FactoryMovable:: asPlayer::create(b2World *world, Assets::Resources& resources, GameMap::Item* item) { sf::Vector2f oneFrameSize(104.f, 150.f); sf::Vector2f playerBodySize(75.f, 100.f); sf::RectangleShape* shapeForAnimation = new sf::RectangleShape(); shapeForAnimation->setSize(playerBodySize); shapeForAnimation->setOrigin( shapeForAnimation->getSize().x/2, shapeForAnimation->getSize().y/2); shapeForAnimation->setPosition(sf::Vector2f(250, 250.f)); shapeForAnimation->setTexture( resources.getTexture( Assets::Textures::Player)); b2Body* myBody = BoxCreators::createDynamicBody(world, playerBodySize.x,playerBodySize.y); Animator* animatorPtr = new Animator(shapeForAnimation,oneFrameSize); DrawableBodyAnimated* drawablePtr = new DrawableBodyAnimated(myBody, animatorPtr); MovableBody* playerItem = new MovableBody(drawablePtr, BodyUserData::Type::Player); playerItem->getRenderBody()->setPosition(item->x, item->y); playerItem->getMover()->setJumpForce(4.5f); playerItem->getMover()->setMaxSpeed(3.5); return playerItem; } MovableBody* MapBuilder::FromMap::FactoryMovable:: asEnemy::create(b2World *world, Assets::Resources &resources, GameMap::Item* item) { b2Body* body = BoxCreators::createDynamicBody(world, 90, 60); DrawableBodyGenerated* drawable = new DrawableBodyGenerated(body); MovableBody* enemyItemB = new MovableBody(drawable,BodyUserData::Type::Enemy); enemyItemB->getRenderBody()->setTexture(*resources.getTexture(Assets::Textures::Enemy)); enemyItemB->getRenderBody()->setPosition(item->x, item->y); enemyItemB->getMover()->setJumpForce(3.f); enemyItemB->getMover()->setMaxSpeed(2.5f); return enemyItemB; } IdentifiedBody* MapBuilder::FromMap::FactoryStatic:: asMapWall::create( b2World *world, Assets::Resources &resources, GameMap::Item* item) { b2Body* boxBody = BoxCreators::createStaticBody( world, item->width, item->height); IdentifiedBody* newItem = new IdentifiedBody( new DrawableBodyGenerated(boxBody), BodyUserData::Type::Wall); newItem->getRenderBody()->setPosition(item->x, item->y); newItem->getRenderBody()->setTexture( *resources.getTexture(Assets::Textures::Wall)); return newItem; } IdentifiedBody* MapBuilder::FromMap::FactoryStatic:: asMapFlor::create( b2World *world, Assets::Resources &resources, GameMap::Item* item) { b2Body* boxBody = BoxCreators::createStaticBody( world, item->height, item->width); IdentifiedBody* newItem = new IdentifiedBody( new DrawableBodyGenerated(boxBody), BodyUserData::Type::Map); newItem->getRenderBody()->setPosition(item->x, item->y); newItem->getRenderBody()->setTexture( *resources.getTexture(Assets::Textures::Map)); return newItem; }
Okej, czyli ekipę budowlaną mamy z głowy. Teraz konieczne jest aby nasz pracownik biura budowlanego potrafił obsłużyć klienta który przychodzi do niego z planem budowy. Do klasy GameWorld dodamy nowy konstruktor który będzie prezentować się następująco:
/* Nowy konstruktor */ GameWorld::GameWorld(Assets::Resources &resources, GameMap &map): m_boxWorld(BoxCreators::createWorld()), m_builder(new MapBuilder::FromMap(m_boxWorld.get(), map)) { m_boxWorld.get()->SetContactListener(&m_contactDetector); m_builder->createArea(listIdentifiedBodies, resources); m_builder->createDynamicBodies(listMovableBodies, resources); }
W tym momencie mamy wszystko czego nam potrzeba do zastosowania naszego pomostu. Dodajmy do klasy CoreApps która przechowuje klasy startowe naszych przykładów, nową funkcje core() która naszym przykładem:
void CoreApps::core(GameMap& map) { sf::RenderWindow* windowItem = new sf::RenderWindow( sf::VideoMode(800, 600, 32), std::string("SFML/Box2D - tech demo"), sf::Style::Default); std::shared_ptr<sf::RenderWindow> window(windowItem); std::shared_ptr<ControlKeys>playerControl(new ControlKeys()); FpsStabilizer stabilizer(60); Assets::Resources resources("data.zip"); GameWorld world(resources, map); MovableBody* playerPtr = world.getPlayer(); ContactDetector* contactDetector = world.getContactDetector(); while(window->isOpen()) { /* OTHER */ stabilizer.work(); for(auto& item : world.listMovableBodies){ item->getRenderBody()->setColor(sf::Color::White); } /* EVENTS */ sf::Event myEvent; while(window->pollEvent(myEvent)) { if(myEvent.type == sf::Event::Closed){ window->close(); } } if(sf::Keyboard::isKeyPressed(playerControl->MOVE_JUMP)) { playerPtr->getMover()->move( BodyMover::Direction::Jump); } if(sf::Keyboard::isKeyPressed(playerControl->MOVE_RIGHT)) { playerPtr->getMover()->move( BodyMover::Direction::Right); DrawableBodyAnimated* ptr = (DrawableBodyAnimated*)playerPtr->getRenderBody(); ptr->getAnimator()->setAnimation(0); } if(sf::Keyboard::isKeyPressed(playerControl->MOVE_LEFT)) { playerPtr->getMover()->move( BodyMover::Direction::Left); DrawableBodyAnimated* ptr = (DrawableBodyAnimated*)playerPtr->getRenderBody(); ptr->getAnimator()->setAnimation(1); } if(sf::Keyboard::isKeyPressed(sf::Keyboard::Escape)){ window->close(); } /* BOX2D */ world.prepareWorld(); for(auto& enemyItem : world.listMovableBodies) { break; } const bool contactCondition = (!contactDetector->isContactListIsEmpty()) && contactDetector->isContactListContains( ContactDetector::Contact::Type::PlayerTouchEnemy); if(contactCondition) { std::vector<ContactDetector::Contact::Info> enemyContacts = contactDetector->getContactList( ContactDetector::Contact::Type::PlayerTouchEnemy); if(!enemyContacts.empty()){ for(auto& contact : enemyContacts) { break; } } } /* RENDER */ window->clear(sf::Color::Black); for(auto& item : world.listMovableBodies){ item->getRenderBody()->update(); item->getRenderBody()->render(*window); } for(auto& item : world.listIdentifiedBodies){ item->getRenderBody()->render(*window); } window->display(); } }
Jeszcze wywołanie z funkcji Editor::run() naszego rdzenia pod klawiszem:
if(sf::Keyboard::isKeyPressed(sf::Keyboard::F2)) { CoreApps::core(*map.get()); }
I viola. Efekt prezentuje się jak poniżej:
Co prawda moglibyśmy przeprowadzić głębszą integrację edytora z rdzeniem poprzez przekazanie wskaźnika do okna aby zarówno edytor jak i rdzeń pracował w jednym oknie aczkolwiek uznałem to za zbędną kosmetykę.
Jak zawsze dzięki za uwagę!