Notatki programisty: Modelowanie świata i modyfikacje edytora dema technologicznego SFML/Box2D
W poprzednim wpisie zaprezentowane zostały podstawy funkcjonowania edytora siatkowego do naszego dema. W niniejszym wpisie wprowadzimy do niego kilka kluczowych zmian. Co więcej przygotujemy rdzeń naszej aplikacji pod przyszłą obsługe modelu mapy. Zapraszam do lektury.
Jeśli coś to tam ktoś, jeśli ble to mi się chce
W poprzednim wpisie została przedstawiona prosta klasa do rysowania mapy. Niestety, główna funkcja rysująca opierała się o szereg warunków które decydowały o tym jak będzie wyglądać rysowany obiekt. Rozwiązanie to można uznać za poprawne w przypadku gdy do rysowania będziemy mieli mniej niż 10 elementów. Wraz z wzrostem warunków, czytelność naszego kodu zaczyna diametralnie spadać. Mając wszystkie te ograniczenia i niedogodności na uwadze chciałbym zaproponować następującą modyfikację klasy Graphics z RenderEngine:
struct Graphics : public Base { Graphics(); ~Graphics(); void render(InfoForEngine &data); struct Painter { struct Base { void virtual draw( GameMap::Item* item, sf::RenderWindow* window) = 0; }; struct MapFlor : public Base { void draw( GameMap::Item* item, sf::RenderWindow* window); }; struct MapWall : public Base { void draw( GameMap::Item* item, sf::RenderWindow* window); }; struct MapRotatingPlatform : public Base { void draw( GameMap::Item* item, sf::RenderWindow* window); }; struct MapInvisableWall : public Base { void draw( GameMap::Item* item, sf::RenderWindow* window); }; struct Player : public Base { void draw( GameMap::Item* item, sf::RenderWindow* window); }; }; private: std::map< std::string, std::shared_ptr<Painter::Base>> m_painters; };
Okej, co tu się stało? Plan był taki aby algorytm rysowania każdego elementu przenieść do nowej klasy dzięki czemu każda strategia rysowania jest hermetyzowana w obrębie tylko jednego klasy. Jako że algorytm jakiego będziemy używać zależy od tego jakie ID ma obiekt mapy, wszystkie obiekty trzymamy w std::map. Po prostu mapujemy ID itemu mapy z algorytmem rysowania. Po wprowadzeniu tej modyfikacji nasz konstruktor i klasa rysująca, z klasy Graphics, będą wyglądać jak poniżej:
RenderNT::RenderEngine::Graphics::Graphics() { m_painters[GameObjRegister::get()->list.at( GameObjRegister::Index::MapFlor)] = std::shared_ptr<Graphics::Painter::Base>( new Graphics::Painter::MapFlor()); m_painters[GameObjRegister::get()->list.at( GameObjRegister::Index::MapWall)] = std::shared_ptr<Graphics::Painter::Base>( new Graphics::Painter::MapWall()); m_painters[GameObjRegister::get()->list.at( GameObjRegister::Index::MapRotatedPlatform)] = std::shared_ptr<Graphics::Painter::Base>( new Graphics::Painter::MapRotatingPlatform()); m_painters[GameObjRegister::get()->list.at( GameObjRegister::Index::MapInvisableWall)] = std::shared_ptr<Graphics::Painter::Base>( new Graphics::Painter::MapInvisableWall()); m_painters[GameObjRegister::get()->list.at( GameObjRegister::Index::Player)] = std::shared_ptr<Graphics::Painter::Base>( new Graphics::Painter::Player()); } void RenderNT::RenderEngine::Graphics::render(InfoForEngine &data) { int startI = 0; int finishI = data.map->height; int startJ = 0; int finishJ = data.map->width; bool widthCondition = ( data.renderSurface.WidthX.x >= 0 && data.renderSurface.WidthX.y <= data.map->width); bool heightCondition = ( data.renderSurface.HeightY.x >= 0 && data.renderSurface.HeightY.y <= data.map->height); if(widthCondition && heightCondition) { startI = data.renderSurface.HeightY.x; finishI = data.renderSurface.HeightY.y; startJ = data.renderSurface.WidthX.x; finishJ = data.renderSurface.WidthX.y; } for(int i = startI; i < finishI; ++i) { for(int j = startJ; j < finishJ; ++j) { GameMap::Item* item = &data.map->array[ i ][ j ]; try { Graphics::Painter::Base* painter = this->m_painters.at(item->ID).get(); painter->draw(item, data.window); } catch (const std::out_of_range& ex) { } } } }
Jak widzimy nasza klasa do rysowania mapy została poważnie odchudzona. Oczywiście zdajemy sobie sprawę z tego że nie zawsze będziemy posiadali kod który obsługuje wszystkie ID obiektów mapy przez co jako zabezpieczenia używamy bloku try‑catch by w trakcie działania aplikacja się nam ona nie wyłożyła. Poniżej zostały zaprezentowane implementacje funkcji rysujących:
void RenderNT::RenderEngine::Graphics::Painter:: MapFlor::draw( GameMap::Item* item, sf::RenderWindow* window) { sf::Vector2f position(item->x, item->y); sf::RectangleShape shape(sf::Vector2f(0,0)); shape.setSize(sf::Vector2f(item->width, item->height)); shape.setPosition(position); shape.setFillColor(sf::Color::Red); window->draw(shape); } void RenderNT::RenderEngine::Graphics::Painter:: MapWall::draw( GameMap::Item* item, sf::RenderWindow* window) { sf::Vector2f position(item->x, item->y); sf::RectangleShape shape(sf::Vector2f(0,0)); shape.setSize(sf::Vector2f(item->width, item->height)); shape.setPosition(position); shape.setFillColor(sf::Color::Yellow); window->draw(shape); } void RenderNT::RenderEngine::Graphics::Painter:: MapRotatingPlatform::draw( GameMap::Item* item, sf::RenderWindow* window) { sf::Vector2f position(item->x, item->y); sf::RectangleShape shape(sf::Vector2f(0,0)); shape.setSize(sf::Vector2f(item->width, item->height)); shape.setPosition(position); shape.setFillColor(sf::Color::Cyan); window->draw(shape); } void RenderNT::RenderEngine::Graphics::Painter:: MapInvisableWall::draw( GameMap::Item* item, sf::RenderWindow* window) { sf::Vector2f position(item->x, item->y); sf::RectangleShape shape(sf::Vector2f(0,0)); shape.setSize(sf::Vector2f(item->width, item->height)); shape.setPosition(position); shape.setFillColor(sf::Color::Blue); window->draw(shape); } void RenderNT::RenderEngine::Graphics::Painter:: Player::draw( GameMap::Item* item, sf::RenderWindow* window) { sf::Vector2f position(item->x, item->y); sf::RectangleShape shape(sf::Vector2f(0,0)); shape.setSize(sf::Vector2f(item->width, item->height)); shape.setPosition(position); shape.setFillColor(sf::Color::Green); window->draw(shape); }
Ktoś mógłby zapytać: „dlaczego tak komplikujemy sobie życie skoro kod się różni przeznaczonym kolorem, nie łatwiej zrobić jakiś sprytny przełącznik? To jak używanie koparki do przesadzania kwiatów”. Odpowiedź w zależności od humoru brzmi: „because we can” lub bardziej sensownie. Mianowicie w przyszłości nie mamy pewności jak będzie wyglądać implementacja funkcji rysujących stąd lepiej zabezpieczyć się na przyszłość, pod rozbudowę. Nasz kod będzie łatwiejszy w modyfikacji a budowa naszej aplikacji bardziej modułowa.
Narzędzia to podstawa
Sytuacja analogiczna co do wyżej przedstawionej, znajduje się w sekcji edytora. Mianowicie gdy tworzymy mapę, mamy do dyspozycji proste menu z wyborem elementu i dla każdego obiektu definiujemy sposób w jaki ma zostać zmodyfikowany element. W przypadku stosowania prymitywnych warunków musielibyśmy pisać całe litanie. Przygotujemy sobie prostą klasę ze sposobami na modyfikację obiektu GameMap::Item. Roboczo będzie ona wyglądać następująco:
struct MapModifier { struct Base { virtual void change(GameMap::Item* item) = 0; }; struct asEmpty : public Base { void change(GameMap::Item* itemPtr); }; struct asRotatingPlatform : public Base { void change(GameMap::Item* itemPtr); }; struct asStandardItem : public Base { void change(GameMap::Item* itemPtr); }; struct asStandardMapTextureUp : public Base { void change(GameMap::Item* itemPtr); }; static void init(std::map< GameObjRegister::Index, std::shared_ptr<MapModifier::Base>>& map); };
I implementacja:
void MapModifier::asEmpty::change( GameMap::Item* itemPtr) { } void MapModifier::asRotatingPlatform::change( GameMap::Item* itemPtr) { itemPtr->height = 40; itemPtr->width = 120; } void MapModifier::asStandardItem::change( GameMap::Item* itemPtr) { itemPtr->height = 40; itemPtr->width = 40; } void MapModifier::asStandardMapTextureUp::change( GameMap::Item* itemPtr) { itemPtr->height = 40; itemPtr->width = 40; } void MapModifier::init( std::map< GameObjRegister::Index, std::shared_ptr<MapModifier::Base>>& map) { map[GameObjRegister::Index::Empty] = std::shared_ptr<MapModifier::Base>( new MapModifier::asEmpty()); map[GameObjRegister::Index::MapRotatedPlatform ] = std::shared_ptr<MapModifier::Base>( new MapModifier::asRotatingPlatform()); map[GameObjRegister::Index::MapFlor] = std::shared_ptr<MapModifier::Base>( new MapModifier::asStandardItem()); map[GameObjRegister::Index::Player] = std::shared_ptr<MapModifier::Base>( new MapModifier::asStandardItem()); map[GameObjRegister::Index::MapWall] = std::shared_ptr<MapModifier::Base>( new MapModifier::asStandardItem()); map[GameObjRegister::Index::MapFlorUp] = std::shared_ptr<MapModifier::Base>( new MapModifier::asStandardMapTextureUp()); }
Po wszystkich tych modyfikacjach funkcja startowa naszego edytora będzie wyglądać jak poniżej:
void Editor::run() { int width = 0; std::cout << "Get width (X): "; std::cin >> width; int height = 0; std::cout << "Get height(Y): "; std::cin >> height; std::shared_ptr<GameMap> map(new GameMap(width,height)); sf::RenderWindow window( sf::VideoMode(800,600, 16), std::string("hello"), sf::Style::Close); sf::View camera; camera = window.getDefaultView(); sf::Vector2f startCameraPosition = camera.getCenter(); const int viewMoveValue = 5; RenderNT render(&window); render.refresh(camera); MouseManager mouseIndex(&window); /* Modyfikatory obiektow mapy. */ std::map< GameObjRegister::Index, std::shared_ptr<MapModifier::Base>> theModders; MapModifier::init(theModders); /* Ustawiamy stan poczatkowy na pusty element. */ GameObjRegister::Index selectedIndex = GameObjRegister::Index::Empty; MapModifier::Base* magicPoint = theModders.at(selectedIndex).get(); while(window.isOpen()) { sf::Event myEvent; while(window.pollEvent(myEvent)) { if(myEvent.type == sf::Event::Closed){ window.close(); } if(sf::Keyboard::isKeyPressed(sf::Keyboard::W)) { Json::Generator dec(map.get()); QJsonDocument jsonDoc = dec.getJsonDoc(); const std::string fileName = std::string("hello.json"); Json::File fileDec; fileDec.saveJsonMap(jsonDoc, QString::fromStdString(fileName)); std::cout << std::string("Saved as: ") + fileName; } if(sf::Keyboard::isKeyPressed(sf::Keyboard::R)) { const std::string fileName = std::string("hello.json"); Json::File fileDec; QJsonDocument jsonDoc = fileDec.readJsonMap(QString::fromStdString(fileName)); Json::Generator dec; map.reset(dec.getGameMapFromJson(jsonDoc)); std::cout << std::string("Readed from: ") + fileName; } if(sf::Keyboard::isKeyPressed(sf::Keyboard::Tab)) { int input = 0; for(auto& pair :GameObjRegister::get()->list ) { std::cout << "No. " << pair.first << " Content: " << pair.second << "\n"; } std::cin >> input; try { selectedIndex = (GameObjRegister::Index)input; magicPoint = theModders.at(selectedIndex).get(); } catch (const std::out_of_range& ex){ std::cout << ex.what() << "\n"; magicPoint = nullptr; } } if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) { if(camera.getCenter().x >= startCameraPosition.x+10) { render.refresh(camera); camera.move(-viewMoveValue, 0); window.setView(camera); } } if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) { if(camera.getCenter().x + (window.getSize().x/2) <= (map->width*40)-10) { render.refresh(camera); camera.move(viewMoveValue, 0); window.setView(camera); } } if (sf::Keyboard::isKeyPressed(sf::Keyboard::Up)) { if(camera.getCenter().y >= startCameraPosition.y+10) { render.refresh(camera); camera.move(0, -viewMoveValue); window.setView(camera); } } if (sf::Keyboard::isKeyPressed(sf::Keyboard::Down)) { if(camera.getCenter().y + (window.getSize().y/2) <= (map->height*40)-10) { render.refresh(camera); camera.move(0, viewMoveValue); window.setView(camera); } } if (sf::Mouse::isButtonPressed(sf::Mouse::Left)) { MouseManager::Info infoMouse = mouseIndex.getIndex(map.get(), &camera); if (infoMouse.isOk) { GameMap::Item* itemPtr = &map->array[infoMouse.x][infoMouse.y]; const std::string selectedIndexValue = GameObjRegister::get()->list.at(selectedIndex); if(itemPtr->ID != selectedIndexValue && magicPoint != nullptr) { itemPtr->ID = selectedIndexValue; magicPoint->change(itemPtr); } } } } /* Render */ window.clear(sf::Color::Black); render.drawMesh(); render.render(*map); window.display(); } }
Nasz własny Czarnobyl
Do tej pory tworzyliśmy świat przy pomocy bezpańskich metod, listy ewidencji też leżały luzem. Generalnie nie posiadaliśmy żadnej spójnej klasy w której moglibyśmy zamknąć wszystkie niezbędne elementy dla naszej symulacji. Aby zmienić ten stan rzeczy wprowadzimy do naszego dema następującą klasę:
class GameWorld { public: GameWorld(Assets::Resources& resources); MovableBody* getPlayer(); ContactDetector* getContactDetector(); void prepareWorld(); std::vector<std::shared_ptr<IdentifiedBody>> listIdentifiedBodies; std::vector<std::shared_ptr<MovableBody>> listMovableBodies; b2World* getBoxWorld() const; private: std::shared_ptr<b2World> m_boxWorld; ContactDetector m_contactDetector; std::shared_ptr<MapBuilder::Base> m_builder; };
Dodatkowo aby uzyskać bardziej niezależną budowę aplikacji, algorytm którym decyduje o sposóbie tworzenia świata wrzucamy do nowej klasy dziedziczącej po MapBuilder::Base. Dzięki temu zabiegowi będziemy mogli w przyszłości dysponować kilkoma oddzielnymi modułami do budowania mapy. Po oswojeniu się ze strategiami z tekstu o mapach i algorytmach w edytorze, zasada działania powinna być jasna. Na ten moment skupimy się na przeniesieniu kodu z poprzednich przykładów do klasy naszego budowniczego. Oto jak będzie wyglądać moduł budowniczych:
struct MapBuilder { struct Base { public: Base(b2World* world) : m_world(world) {} virtual void createArea( std::vector<std::shared_ptr<IdentifiedBody>>& list, Assets::Resources& resources) = 0; virtual void createDynamicBodies( std::vector<std::shared_ptr<MovableBody>>& list, Assets::Resources& resources); protected: b2World* m_world; }; struct TechDemo : public Base { TechDemo(b2World* world); void createArea( std::vector<std::shared_ptr<IdentifiedBody>>& list, Assets::Resources& resources); void createDynamicBodies( std::vector<std::shared_ptr<MovableBody>>& list, Assets::Resources& resources); }; };
Dodatkowo aby uporządkować kod, dotychczasowe funkcje odpowiedzialne za tworzenie ciał Box2D przeniesiemy do nowej klasy aby nic w projekcie nie było pozostawione same sobie, oto jak będzie wyglądać nasz nowy nagłówek:
struct BoxCreators { static b2World* createWorld(); static b2Body* createStaticBody( b2World* world, const double height, const double width); static b2Body* createDynamicBody( b2World* world, const float width, const float height); static b2Body* createPlayerBody(b2World* world); };
Okej, na ten moment mamy nowe klasy do przechowywania danych symulacji, sposobu budowania mapy i trzymania starych funkcji w jednym miejscu. Implementacje nowych klas prezentują się jak poniżej. GameWorld:
GameWorld::GameWorld(Assets::Resources& resources) : m_boxWorld(BoxCreators::createWorld()), m_builder(new MapBuilder::TechDemo(m_boxWorld.get())) { m_boxWorld.get()->SetContactListener(&m_contactDetector); m_builder->createArea(listIdentifiedBodies, resources); m_builder->createDynamicBodies(listMovableBodies, resources); } ContactDetector* GameWorld::getContactDetector() { return &m_contactDetector; } MovableBody* GameWorld::getPlayer() { for(std::shared_ptr<MovableBody>& item : listMovableBodies) { if(item->getBodyType() == BodyUserData::Type::Player){ return item.get(); } } return nullptr; } b2World* GameWorld::getBoxWorld() const { return m_boxWorld.get(); } void GameWorld::prepareWorld() { const float32 timeStep = 1.0f / 60.0f; const int32 velocityIterations = 6; const int32 positionIterations = 2; m_boxWorld->Step( timeStep, velocityIterations, positionIterations); }
Moduł budowy map:
MapBuilder::TechDemo::TechDemo(b2World* world) : Base(world) { } void MapBuilder::TechDemo::createArea( std::vector<std::shared_ptr<IdentifiedBody>>& list, Assets::Resources& resources) { IdentifiedBody mapPlatformTop( new DrawableBodyGenerated( BoxCreators::createStaticBody(m_world, 800, 40)), BodyUserData::Type::Map); mapPlatformTop.getRenderBody()->setPosition(400.f, 0.f); mapPlatformTop.getRenderBody()->setTexture( *resources.getTexture(Assets::Textures::Map)); IdentifiedBody mapPlatformBottom( new DrawableBodyGenerated( BoxCreators::createStaticBody(m_world, 800, 40)), BodyUserData::Type::Map); mapPlatformBottom.getRenderBody()->setPosition(400.f, 600.f); mapPlatformBottom.getRenderBody()->setTexture( *resources.getTexture(Assets::Textures::Map)); IdentifiedBody mapWallLeft( new DrawableBodyGenerated( BoxCreators::createStaticBody(m_world, 40, 800)), BodyUserData::Type::Wall); mapWallLeft.getRenderBody()->setPosition(0.f, 400.f); mapWallLeft.getRenderBody()->setVisable(false); mapWallLeft.getRenderBody()->setTexture( *resources.getTexture(Assets::Textures::Wall)); IdentifiedBody mapWallRight( new DrawableBodyGenerated( BoxCreators::createStaticBody(m_world, 40, 800)), BodyUserData::Type::Wall); mapWallRight.getRenderBody()->setPosition(800.f, 400.f); mapWallRight.getRenderBody()->setVisable(false); mapWallRight.getRenderBody()->setTexture( *resources.getTexture(Assets::Textures::Wall)); list.push_back(std::make_shared<IdentifiedBody>(mapPlatformTop)); list.push_back(std::make_shared<IdentifiedBody>(mapPlatformBottom)); list.push_back(std::make_shared<IdentifiedBody>(mapWallLeft)); list.push_back(std::make_shared<IdentifiedBody>(mapWallRight)); } void MapBuilder::TechDemo::createDynamicBodies( std::vector<std::shared_ptr<MovableBody>>& list, Assets::Resources& resources) { 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::PlayerAnimated)); MovableBody playerItem( new DrawableBodyAnimated( BoxCreators::createDynamicBody( m_world, playerBodySize.x, playerBodySize.y), new Animator( shapeForAnimation, oneFrameSize)), BodyUserData::Type::Player); playerItem.getRenderBody()->setPosition(400.f, 10.f); playerItem.getMover()->setJumpForce(4.5f); playerItem.getMover()->setMaxSpeed(3.5); /* Enemy A. */ MovableBody enemyItem( new DrawableBodyGenerated( BoxCreators::createDynamicBody(m_world, 90, 60)), BodyUserData::Type::Enemy); enemyItem.getRenderBody()->setTexture( *resources.getTexture(Assets::Textures::Enemy)); enemyItem.getRenderBody()->setPosition(600.f, 50.f); enemyItem.getMover()->setJumpForce(3.f); enemyItem.getMover()->setMaxSpeed(2.5f); /* Enemy B. */ MovableBody enemyItemB( new DrawableBodyGenerated( BoxCreators::createDynamicBody(m_world, 90, 60)), BodyUserData::Type::Enemy); enemyItemB.getRenderBody()->setTexture( *resources.getTexture(Assets::Textures::Enemy)); enemyItemB.getRenderBody()->setPosition(150.f, 50.f); enemyItemB.getMover()->setJumpForce(3.f); enemyItemB.getMover()->setMaxSpeed(2.5f); /* Ewidencja wrogow. */ list.push_back(std::make_shared<MovableBody>(playerItem)); list.push_back(std::make_shared<MovableBody>(enemyItem)); list.push_back(std::make_shared<MovableBody>(enemyItemB)); }
Implementacje funkcji tworzenia zostały bez zmian. Po tych wszystkich zabiegach funkcja startowa naszego dema technologicznego wygląda o wiele schludniej:
void CoreApps::startExample() { 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); 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); } /* 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(); } }
Dzięki powyżej przedstawionym modyfikacjom, naszym następnym celem będzie zbudowanie silnika do budowy map w oparciu o obiekt GameMap z edytora. Ale to temat na przyszły tekst. Jak zawsze dzięki za uwagę!