Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Notatki programisty: Czas na akcje, czyli piszemy postać gracza w aplikacji Box2D/SFML

W poprzednich wpisach poruszyłem temat graficznego prezentowania ciał Box2D w SFML. Dotychczas operowaliśmy na ciałach statycznych czyli takich które posiadają stałe miejsce w przestrzeni świata fizycznego i na które nie działa żadna siła.. Co prawda wprawialiśmy je w ruch lecz odbywało się to w dość brutalny sposób przy użyciu transformacji. Ruch ten nie był związany z żadną silą fizyczną, np. grawitacją. W tym wpisie zmienimy ten stan rzeczy i poznamy ciała dynamiczne oraz wprawimy je w ruch bardziej „naturalnym” sposobem. Zapraszam do lektury.

Trochę modyfikacji

Pierwszą rzeczą jaką zrobimy jest napisanie nowej funkcji tworzącej ciało statyczne. Możemy tutaj zastosować znaną metodykę programowania „magic of copy-pase” a następnie zmodyfikować przedstawioną w poprzednich wpisach funkcję do tworzenia ciał. Po modyfikacjach, kod nowej funkcji prezentuje się następująco: b2Body* createStaticBody( b2World* world, const double height, const double width) { b2PolygonShape bodyShape; bodyShape.SetAsBox( (height/2)*G_PIXELS_TO_METERES, (width/2)*G_PIXELS_TO_METERES); b2FixtureDef bodyFixture; bodyFixture.density = 1.f; bodyFixture.friction = 0.2f; bodyFixture.shape = &bodyShape; b2BodyDef bodyDef; bodyDef.type = b2_staticBody; b2Body* myBody = world->CreateBody(&bodyDef); myBody->CreateFixture(&bodyFixture); return myBody; } Główna modyfikacja polega na tym że wielkość ciała przekazujemy w parametrach funkcji dzięki czemu dostajemy bardziej elastyczną funkcję do tworzenia klocków. Następnym krokiem jest stworzenie ciała które będzie naszą postacią. Modyfikacje w stosunku do dotychczasowych funkcji są dość subtelne. Komentarze wyjaśniają kluczowe zmiany. b2Body* createPlayerBody(b2World* world) { /* Umownie nasz bohater * bedzie kwadratem 40x40 px. */ b2PolygonShape bodyShape; bodyShape.SetAsBox(20*G_PIXELS_TO_METERES, 20*G_PIXELS_TO_METERES); b2FixtureDef bodyFixture; bodyFixture.density = 0.1f; bodyFixture.friction = 0.2f; bodyFixture.shape = &bodyShape; /* Typ ciala definiujemy w b2BodyDef * w przypadku ruchomego obiektu gracza * zastosujemy cialo dynamiczne. */ b2BodyDef bodyDef; bodyDef.type = b2_dynamicBody; b2Body* myBody = world->CreateBody(&bodyDef); myBody->CreateFixture(&bodyFixture); /* Domyslnie kazde cialo posiada * "naprawiona" rotacje, czyli * fizycznie sie nie obraca, w naszym * przykladzie chcemy jednak aby cialo * bylo bardziej *wyluzowane*. */ myBody->SetFixedRotation(false); return myBody; } Ostatnią zmianą jaką wprowadzimy jest zmiana typu std::unique_ptr na std::shared_ptr w klasie DrawablePolygonBody. Zmiana podyktowana jest pewnym ułatwieniem które zaobserwujemy w funkcji głównej. Ale to za chwile, w tym momencie przejdźmy do omówienia sposobu poruszania ciała dynamicznego.

Grunt to akcja

Sposób w jaki wprawimy nasze ciało w ruch będzie następujący: najpierw pobierzemy obecną prędkość ciała, zwiększymy ją a następnie przypiszemy nową zwiększoną prędkość ciału. Jako że kod wyraża więcej niż tysiąc słów, czas się z nim zapoznać: int main(int argc, char *argv[]) { /* Tworzenia swiata. */ std::unique_ptr<b2World> myWorld(createWorld()); /* Tekstury. */ sf::Texture textureMap; if(!textureMap.loadFromFile("textureMap.png")){ std::cout << "textureMap problem \n"; } sf::Texture texturePlayer; if(!texturePlayer.loadFromFile("textureHero.png")){ std::cout << "texturePlayer problem \n"; } /* Ciala naszej mapy. */ DrawablePolygonBody bodyPlatform(createStaticBody(myWorld.get(), 300, 40)); bodyPlatform.setPosition(300.f, 450.f); bodyPlatform.setTexture(textureMap); DrawablePolygonBody bodySecondPlatform(createStaticBody(myWorld.get(), 100, 40)); bodySecondPlatform.setPosition(600.f, 450.f); bodySecondPlatform.setTexture(textureMap); /* Cialo bohatera. */ DrawablePolygonBody myPlayerBody(createPlayerBody(myWorld.get())); myPlayerBody.setTexture(texturePlayer); myPlayerBody.setPosition(300.f, 50.f); /* Dla latwiejszego rysowania wszystkie * obiekty dodajemy do wektora. */ std::vector<DrawablePolygonBody> listWorldBodies; listWorldBodies.push_back(bodyPlatform); listWorldBodies.push_back(bodySecondPlatform); listWorldBodies.push_back(myPlayerBody); /* Okno SFML. */ sf::RenderWindow window( sf::VideoMode(800, 600, 32), std::string("Box2d - SFML"), sf::Style::Default); window.setFramerateLimit(60); /* Petla glowna. */ while(window.isOpen()) { /* Zdarzenia. */ sf::Event myEvent; while(window.pollEvent(myEvent)) { if(myEvent.type == sf::Event::Closed){ window.close(); } /* Poruszanie cialem bohatera. */ if(sf::Keyboard::isKeyPressed(sf::Keyboard::Space)) { const b2Vec2 oldVelocity = myPlayerBody.getBody()->GetLinearVelocity(); const b2Vec2 newVelocity = b2Vec2(oldVelocity.x, -5.25f); myPlayerBody.getBody()->SetLinearVelocity(newVelocity); } if(sf::Keyboard::isKeyPressed(sf::Keyboard::D)) { const b2Vec2 oldVelocity = myPlayerBody.getBody()->GetLinearVelocity(); const b2Vec2 newVelocity = b2Vec2(oldVelocity.x + 0.12f, oldVelocity.y); myPlayerBody.getBody()->SetLinearVelocity(newVelocity); } if(sf::Keyboard::isKeyPressed(sf::Keyboard::A)) { const b2Vec2 oldVelocity = myPlayerBody.getBody()->GetLinearVelocity(); const b2Vec2 newVelocity = b2Vec2(oldVelocity.x - 0.12f, oldVelocity.y); myPlayerBody.getBody()->SetLinearVelocity(newVelocity); } } /* Logika. */ beforeGameLoop(*myWorld.get()); myPlayerBody.update(); /* Render. */ window.clear(sf::Color::Black); for(DrawablePolygonBody& item : listWorldBodies){ item.render(window); } window.display(); } return 0; } Główną zmianą w kodzie jaką możemy zaobserwować jest część w sekcji obsługi zdarzeń. Zgodnie z powyżej przedstawionym założeniem dotyczącym wprawiania obiektów w ruch, pobieramy prędkość, modyfikujemy w zależności od potrzeb i akceptujemy. W tym przypadku przydaje się nam furtka jaką sobie zostawiliśmy w postaci funkcji getBody(). Wszystkie obiekty które rysujemy dodajemy do obiektu std::vector dzięki czemu w pętli głównej renderowanie odbywa się za pomocą prostej pętli for. Gdybyśmy w klasie DrawablePolygonBody zostawili typ std::unique_ptr operacja dodania obiektu do wektora by się nie powiodła. Dodatkowo, ustawiliśmy limit klatek na sekundę, okna na 60 by nasza symulacja nie przebiegała zbyt szybko. Po uruchomieniu programu, powita nas poniżej przedstawiony widok. Aby zobaczyć rezultat do czego nam się przydało wyłączenie „naprawionej” rotacji, wystarczy podjechać do krawędzi platformy i zobaczyć jak ciało będzie reagować.

Więcej elegancji

Aby zwiększyć czytelność naszego kodu, napiszemy prostą klasę opakowującą funkcjonalność ruchu która będzie dekoratorem ciała Box2D. Dzięki temu zyskamy wcześniej wspomnianą przejrzystość kodu oraz klasę która z powodzeniem będzie mogła nam w przyszłości posłużyć za dekorator dla ciał które będą naszymi przeciwnikami. Interfejs i implementacja przedstawia się następująco. class BodyMover { public: enum Direction : int { Right, Left, Jump }; BodyMover(b2Body* baseBody); void move(Direction moveDir); void setJumpForce(const float newBodySpeed); void setMoveSpeedChangeValue(const float newBodySpeed); private: b2Body* m_body; float m_bodySpeedChangeValue; float m_bodyJumpForce; }; BodyMover::BodyMover(b2Body* baseBody) : m_body(baseBody), m_bodySpeedChangeValue(0.12f), m_bodyJumpForce(5.25f) {} void BodyMover::setMoveSpeedChangeValue(const float newBodySpeed) { this->m_bodySpeedChangeValue = newBodySpeed; } void BodyMover::setJumpForce(const float newBodySpeed) { this->m_bodyJumpForce = newBodySpeed; } void BodyMover::move(Direction moveDir) { switch(moveDir) { case Direction::Right: { const b2Vec2 oldVelocity = m_body->GetLinearVelocity(); const b2Vec2 newVelocity = b2Vec2(oldVelocity.x + m_bodySpeedChangeValue, oldVelocity.y); m_body->SetLinearVelocity(newVelocity); break; } case Direction::Left: { const b2Vec2 oldVelocity = m_body->GetLinearVelocity(); const b2Vec2 newVelocity = b2Vec2(oldVelocity.x - m_bodySpeedChangeValue, oldVelocity.y); m_body->SetLinearVelocity(newVelocity); break; } case Direction::Jump: { const b2Vec2 oldVelocity = m_body->GetLinearVelocity(); const b2Vec2 newVelocity = b2Vec2(oldVelocity.x, - m_bodyJumpForce); m_body->SetLinearVelocity(newVelocity); break; } } } Użycie jej jest bardzo proste i myślę że nie wymaga większego komentarza.Nasza funkcja główna od razu zyskuje na atrakcyjności: int main(int argc, char *argv[]) { /* Tworzenia swiata. */ std::unique_ptr<b2World> myWorld(createWorld()); /* Tekstury. */ sf::Texture textureMap; if(!textureMap.loadFromFile("textureMap.png")){ std::cout << "textureMap problem \n"; } sf::Texture texturePlayer; if(!texturePlayer.loadFromFile("textureHero.png")){ std::cout << "texturePlayer problem \n"; } /* Obiekty mapy. */ DrawablePolygonBody bodyPlatform(createStaticBody(myWorld.get(), 300, 40)); bodyPlatform.setPosition(300.f, 450.f); bodyPlatform.setTexture(textureMap); DrawablePolygonBody bodySecondPlatform(createStaticBody(myWorld.get(), 100, 40)); bodySecondPlatform.setPosition(600.f, 450.f); bodySecondPlatform.setTexture(textureMap); /* Obiekt gracza i jego *poruszacza*. */ DrawablePolygonBody myPlayerBody(createPlayerBody(myWorld.get())); myPlayerBody.setTexture(texturePlayer); myPlayerBody.setPosition(300.f, 50.f); BodyMover playerMove(myPlayerBody.getBody()); /* Ewidencja cial. */ std::vector<DrawablePolygonBody> listWorldBodies; listWorldBodies.push_back(bodyPlatform); listWorldBodies.push_back(bodySecondPlatform); listWorldBodies.push_back(myPlayerBody); /* Okno SFML. */ sf::RenderWindow window( sf::VideoMode(800, 600, 32), std::string("Box2d - SFML"), sf::Style::Default); window.setFramerateLimit(60); /* Petla glowna. */ while(window.isOpen()) { /* Zdarzenia. */ sf::Event myEvent; while(window.pollEvent(myEvent)) { if(myEvent.type == sf::Event::Closed){ window.close(); } if(sf::Keyboard::isKeyPressed(sf::Keyboard::Space)) { playerMove.move(BodyMover::Direction::Jump); } if(sf::Keyboard::isKeyPressed(sf::Keyboard::D)) { playerMove.move(BodyMover::Direction::Right); } if(sf::Keyboard::isKeyPressed(sf::Keyboard::A)) { playerMove.move(BodyMover::Direction::Left); } } /* Logika. */ beforeGameLoop(*myWorld.get()); myPlayerBody.update(); /* Render. */ window.clear(sf::Color::Black); for(DrawablePolygonBody& item : listWorldBodies){ item.render(window); } window.display(); } return 0; }

Na zakończenie

Po tym wpisie można z pełną stanowczością stwierdzić że w naszej aplikacji zaczęło się coś dziać. Co prawda skakanie naszego ciała nie jest jeszcze doskonałe i nasz bohater może odbić się w górę z powietrza ale mamy już solidną bazę do dalszego rzeźbienia rozgrywki naszej aplikacji-gry. Oczywiście zapraszam do samodzielnego sprawdzenia kodu.

Jak zawsze dzięki za uwagę. 

programowanie gry hobby

Komentarze

0 nowych
karol221-10   13 #1 09.01.2017 21:31

Dobry poradnik. SFML-em bawiłem się dość dawno temu. Nie jestem pewny, ale SFML nie ma chyba portu dla Androida, a ta platforma mnie obecnie interesuje. Byłoby idealnie, gdybyś zrobił jakieś porządne materiały o Libgdx + Box2D, obojętnie pod jakim językiem. Szkoda, że na takie tematy informacji po polsku trzeba ze świecą szukać ...

biomen   8 #2 09.01.2017 21:45

@karol221-10
SFML nie ma portu na Androida. Stety albo stety.
Co do LibGDX, sam ostatnimi czasy się przyglądałem GDX'owi ze względu na właśnie wsparcie platform mobilnych. Ostatnio na warsztacie.GD znalazłem projekt:
http://warsztat.gd/projects/under_sun/media
Całkiem fajnie to wygląda i wszystko jest w jednym frameworku.

Myślę że znając sposoby jak wykonać poszczególne rzeczy dość łatwo można samemu przepisać prezentowane programy na Jave/LibGDX. Jedyne co musiałbyś zrobić to ogarnąć jak tam wygląda struktura aplikacji i jak rysować/teksturować obiekty. A najlepiej napisać sobie wrapper na LibGDX który byłby podobny w użyciu do SFML'a. Sam zacząłem taki pisać ale brak czasu skutecznie odwlókł projekt w przyszłość. ;)

  #3 10.01.2017 13:20

Java dev matt ma o LibGDX cała serię na YT po polsku, jakby kto pytał

karol221-10   13 #4 11.01.2017 15:57

@dx2 (niezalogowany): Widziałem to. Ale w tym kursie brakuje mi właśnie wspomnienia o obsłudze silnika fizyki Box2D, który byłby mi potrzebny przy jednym z moich projektów.