19 stycznia 2013

Box2D - czyli pierwsze starcie z fizyką

Wymagania:
  • SVN
  • Visual C++ 2010
Dziś nikt nie będzie rozmawiał o wzorach, równaniach i innych wyprowadzeniach. Tym zajęli się inni, a my skorzystamy z ich ciężkiej pracy.
Tytułowy Box2D, to silnik fizyczny autorstwa Erina Catto, przeznaczony do symulowania fizyki ciał sztywnych w świecie dwuwymiarowym.
Aktualnie jest on najlepszym tego typu narzędziem. Możemy także wykorzystać bardziej zaawansowane rozwiązania typu Havok czy PhysX, jednak przekonanie ich do współpracy z naszą dwuwymiarową rzeczywistością wymaga trochę pracy. Poza tym, omawiana tu biblioteka jest najczęściej wykorzystywanym rozwiązaniem problemu symulacji w grach 2D. Na zachętę dodam, że samo Angry Birds zostało napisane w oparciu o Boxa2D.   

Przygotowanie środowiska 

Jak nieśmiało wspomniałem na początku, korzystać będziemy z Visual Studio 2010 (link do wersji Express). Samą bibliotekę checkout'ujemy korzystając z repozytorium na serwerach Google (http://box2d.googlecode.com/svn/trunk/). Gdy źródła będą już na naszym dysku (w tym tutorialu wykorzystuję rewizję nr 251) możemy przekopiować je do naszej solucji i zacząć tworzenie pierwszej, prostej symulacji. 

Tworzymy świat

Każda symulacja fizyczna składa się  z dwóch podstawowych elementów - świata oraz ciał, których ruch modelowany jest w jego obrębie. Elementy takie jak jointy, sensory czy motory są elementami działającymi bezpośrednio na ciała i służą do określonej zmiany zachowań obiektów. Ich użycie pokazane będzie w następnych częściach kursu. 
Pierwszą rzeczą którą musimy stworzyć jest więc świat. Jedyną informacją którą będziemy przekazywać podczas jego konstrukcji jest grawitacja wyrażona jako wektor siły.
b2World *pWorld = new b2World(b2Vec2(0.0f, -9.8f));
Warto zaznaczyć, że typ przekazanego wektora to  b2Vec2. Jak nazwa wskazuje, jest to wektor dwuwymiarowy, zawierający pola  x oraz y. Istnieje kilka wariantów jego konstrukcji, posiada także szereg przydatnych metod - ciekawskich odsyłam do pliku b2Math.h. Stworzyliśmy zatem świat, którego grawitacja jest analogiczna do naszej ziemskiej.

Niech się stanie ruch

Plac zabaw został zatem zbudowany. Czas zainwestować w zabawki. Pierwszą rzeczą jaką musimy zrobić to odpowiednie wypełnienie struktury b2BodyDef. Zawiera ona wszystkie niezbędne informacje, które musi znać silniki fizyki przed stworzeniem nowego obiektu. Oto kilka podstawowych:
  • b2BodyType type
  • b2Vec2 position
  • float32 angle
  • b2Vec2 linearVelocity
  • float32 angularVelocity
Tworzone ciało może być dynamiczne (b2_dynamicBody), kinematyczne (b2_kinematicBody) lub statyczne (b2_staticBody).
Obiekty dynamiczne będą najczęściej przez nas wykorzystywane. Poddają się one każdym aspektom symulacji oraz kolidują ze wszystkimi fizycznymi elementami świata. Mają niezerową masę. 
Obiekty kinematyczne nie są poddawane efektom działających na nie sił, mogą się jednak poruszać poprzez przyłożenie im prędkości. Nie kolidują z innymi ciałami kinematycznymi lub statycznymi. Mają nieskończoną masę (~0). 
Ciała statyczne nie podlegają symulacji ruchu. Można je przesuwać, poprzez manualną zmianę ich pozycji, ale z wielu względów nie jest to zalecane. Podobnie jak obiekty kinematyczne mają nieskończoną masę oraz nie kolidują ze sobą.
Początkowa pozycja i kąt (w radianach) to następne elementy struktury. Podajemy je we współrzędnych świata. Warto zaznaczyć tu, że jak na porządny silnik fizyki przystało, Box2D operuje jednostkami systemu MKS. Każdy kto miał fizykę w podstawówce pewnie wie jak rozwinąć ten skrót. Dlatego, jeśli projektujemy naszą grę i zdecydujemy się na wykorzystanie fizyki, podstawową jednostkę gry nie mogą być piksele (200 pikselowy obiekt będzie mieć 200 metrów). 
Jeśli chcemy by po stworzeniu obiekt poruszał się ustawiamy wektor prędkości liniowej lub kątowej. 
Bardziej ciekawskie osoby, zapoznają się pewnie z całością struktury b2BodyDef, której pełną definicję można znaleźć w pliku b2Body.h. Mając wyjaśnione wszystkie podstawowe zagadnienia, możemy w końcu dodać ciało do świata:
b2BodyDef bodyDefinition;
 
bodyDefinition.type            = b2_dynamicBody;
bodyDefinition.position        = b2Vec2(0.0f, 3.0f);
bodyDefinition.angle           = 0.0f;
bodyDefinition.linearVelocity  = b2Vec2_zero;
bodyDefinition.angularVelocity = 0.1f;
 
b2Body *pFirstBody = pWorld->CreateBody(&bodyDefinition);
I cóż my tu mamy. Na samym początku wypełnianie wyjaśnionych wcześniej elementów struktury. Następnie zaś obiekt dodawany jest do świata. Gdy operacja zakończy się powodzeniem, otrzymujemy wskaźnik na ciało.

Let's play

Czas wiec uruchomić machinę i wystartować animację. Obliczanie równań ruchu będziemy przeprowadzać co klatkę, dla ustalonego kroku czasowego, który powinien być stały (Fix your timestep!). Musimy podać też liczbę iteracji dla solvera, którego działanie podzielone jest na dwie fazy: wyliczania prędkości i pozycji. Więcej informacji o jego działaniu możemy znaleźć w manualu. Z naszego punktu widzenia istotna jest informacja, że im większa liczba iteracji, tym symulacja jest dokładniejsza, jednak tracimy na wydajności. Trzeba zatem wypracować złoty środek, który zależy jednak od konkretnego projektu.
unsigned int uVelocityIterations = 8;
unsigned int uPositionIterations = 6;
 
pWorld->Step(1.0f / 60.0f, uVelocityIterations, uPositionIterations);
Uruchamiamy zatem naszą symulację z prędkością sześćdziesięciu klatek na sekundę (na razie czysto teoretycznie) z odpowiednią liczbą iteracji dla poszczególnych faz. Teraz przydałaby się informacja na temat położenia naszego ciała. Uzyskujemy ją wołając metodę  GetPosition() dla utworzonego ciała. Zwraca ona wektor dwuwymiarowy. Metoda  GetAngle() zwraca aktualny kąt w radianach. I to w zasadzie wszystko. Całość prezentuje się następująco:
unsigned int uVelocityIterations = 8;
unsigned int uPositionIterations = 6;
 
const unsigned int uFramesCount = 60;
for(unsigned int i = 0; i < uFramesCount; i++)
{
    pWorld->Step(1.0f / 60.0f, uVelocityIterations, uPositionIterations);
 
    b2Vec2 position = pFirstBody->GetPosition();
    float fAngle    = pFirstBody->GetAngle();
 
    printf("Position x: %.3f, y: %.3f\n", position.x, position.y);
    printf("Angle: %.3f\n", fAngle);
}

Na sam koniec traktujemy wskaźnik pWorld operatorem delete

No cóż, efekt na razie mizerny, ale nikt cudów nie obiecywał. W następnych częściach pogadam trochę o integracji fizyki z OpenGLem. Mam nadzieję, że nikt po drodze nie zasnął. 
Solucję do tej części można pobrać tu

Brak komentarzy:

Prześlij komentarz