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