Artykuł opisuje bibliotekę Box2D dla C++, która służy do symulowania fizyki w czasie rzeczywistym w grach 2D. (artykuł)
Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Zarejestruj się!
dział serwisuArtykuły
kategoriaInne artykuły
artykuł[C++] Box2D w pigułce
Autor: 'kubawal'

[C++] Box2D w pigułce

[artykuł] Artykuł opisuje bibliotekę Box2D dla C++, która służy do symulowania fizyki w czasie rzeczywistym w grach 2D.

Fizyka 2d z biblioteką Box2d

Box2d jest biblioteką napisaną w C++, która pozwala tworzyć bardzo realistyczne symulacje dwuwymiarowego świata.
Jej zaletami są szybkość i możliwość używania bez (wybitnych) zdolności w zakresie fizyki :P
Posiada także porty na inne języki (w tym Java, JavaScript i Flash)

Z użyciem Box2d zostało napisanych wiele gier, w tym bardzo popularne Angry Birds! (Kiedy nauczysz się co nieco o box'ie zrozumiesz, że wcale się przy tym tak bardzo nie napracowali. Wszystko dzięki Box2d!)

Bibliotekę można pobrać stąd: https://code.google.com/p​/box2d/downloads/detail​?name=Box2D_v2.3.0.7z&can=2&q=(wersja 2.3.0 używana w tym tutorialu)
Nowsze wersje biblioteki mogą być hostingowane na innych serwerach.

Kilka podstawowych definicji

Ciało sztywne (ang. rigid body) - ciało, które nie może zmienić swojego kształtu. Wszystkie ciała symulowane przez Box2d są ciałami sztywnymi.
Wiązanie (ang. constraint, joint) - działanie, które w pewnym stopniu ogranicza ruch ciała
Ciało statyczne - ciało które nie posiada masy (lub posiada nieskończoną masę). Nie działają na nie żadne siły (włącznie z grawitacją). Koliduje z ciałami dynamicznymi. Nie może samo zmienić swojej pozycji.
Ciało kinematyczne - ciało które nie posiada masy (lub posiada nieskończoną masę). Nie działają na nie żadne siły (włącznie z grawitacją). Koliduje z ciałami dynamicznymi. Po nadaniu mu prędkości nie zmienia jej.
Ciało dynamiczne - "normalne" ciało. Posiada masę, działają na nie wszystkie siły, koliduje z wszystkimi innymi ciałami

Twoja pierwsza symulacja!

Najprostszy przykład symulacji Box2d.
Program symuluje spadanie klocka na wiszącą w powietrzu platformę.
Omówienia w komentarzach.
C/C++
#include <Box2d/Box2d.h> // dołączamy nagłówek
// pamiętaj o odpowiednich lib'ach!

int main()
{
    b2World world( b2Vec2( 0.0f, - 10.0f ) ); // tworzymy świat z grawitacją o wartości 10 skierowaną w dół
   
    ////
    // teraz tworzymy "ziemię" jako ciało statyczne - będzie ono "wisiało" w miejscu
    b2BodyDef groundDef; // ciała domyślnie są statyczne
    groundDef.position.Set( 0.0f, - 10.0f ); // ustawiamy pozycję ciała
   
    b2Body * ground = world.CreateBody( & groundDef ); // i tutaj twotrzymy ciało. Nie ma ono na razie przypisanego żadnego kształtu.
   
    b2PolygonShape groundShape; // a więc tworzymy go
    groundShape.SetAsBox( 100.0f / 2, 20.0f / 2 ); // i ustawiamy jako prostokąt o bokach 100 i 20
   
    ground->CreateFixture( & groundShape, 1.0f ); // na końcu "podpinamy" go do ciała. Drugi parametr to gęstość.
   
    ////
    // tworzymy spadający "klocek" jako ciało dynamiczne - będzie on mógł normalnie się poruszać i spadać
   
    b2BodyDef boxDef;
    boxDef.type = b2_dynamicBody; // musimy poinformować Box2d, że ciało ma być dynamiczne
    boxDef.position.Set( 0.0f, 4.0f ); // tak jak poprzednio
   
    b2Body * box = world.CreateBody( & boxDef ); // nic nowego
   
    b2PolygonShape boxShape;
    boxShape.SetAsBox( 2.0f / 2, 2.0f / 2 ); // tak jak poprzednio
   
    // teraz stworzymy kształt dla klocka
    // chcemy ustawić dodatkowe parametry, takie jak tarcie, więc musimy użyć dłuższej metody niż poprzednio
    b2FixtureDef boxFixDef; // tworzymy definicję fikstury (fragmentu ciała)
    boxFixDef.shape = & boxShape; // ustawiamy kształt...
    boxFixDef.density = 1.0f; // ...gęstość...
    boxFixDef.friction = 0.3f; // ...i tarcie
   
    box->CreateFixture( & boxFixDef ); // no i tworzymy fiksturę
   
    // super, mamy już gotowe wszystkie elementy naszej symulacji!
   
    ////
   
    float time = 1.0f; // nasz świat będziemy symulować przez sekundę
    int steps = 100.0f; // symulację podzielimy na 100 kroków
    float timeStep = time / steps; // więc fizykę aktualizować będziemy co 1/100 sekundy.
   
    // te dwie wartości mają wpływ na dokładność symulacji. Im większe liczby, tym fizyka będzie dokładniejsza, ale wolniejsza
    int velocityIt = 8, positionIt = 3; // autor Box2d zaleca wartości 8 i 3 więc nie będziemy tego zmieniać
   
    // no to działamy!
    for( int i = 0; i < steps; i++ )
    {
        world.Step( timeStep, velocityIt, positionIt ); // oto magiczna linijka. Tutaj odbywa się symulacja naszego świata
        // używamy już wcześniej obliczonych wartości
       
        b2Vec2 pos = box->GetPosition(); // pobieramy pozycję klocka
       
        printf( "Krok %i : %4.2f, %4.2f", i, pos.x, pos.y ); // i ją drukujemy
    }
   
    // i koniec. Klasa b2World sama zajmie się usunięciem naszych wytworów :)
}

Wynik powienien być mniej więcej taki:


Krok 0 : 0.00, 4.00

Krok 1 : 0.00, 3.99

Krok 2 : 0.00, 3.98

...

Krok 97 : 0.00, 1.25

Krok 98 : 0.00, 1.13

Krok 99 : 0.00, 1.01

Omówienia wymaga tylko ta linijka:
C/C++
groundShape.SetAsBox( 100.0f / 2, 20.0f / 2 ); // i ustawiamy jako prostokąt o bokach 100 i 20
Otóż, metoda SetAsBox() przyjmuje połowy długości boków prostokąta. Należy to zapamiętać.

Jak działa Box2d?

Symulacja Box2d jest reprezentowana przez świat - klasę b2World.
Do każdego świata mogą być dodawane ciała - klasa b2Body (oraz wiązania, ale o tym później)
Każde ciało składa się z tzw. fikstur - klasa b2Fixture
Fikstury stanowią różne fragmenty jednego ciała. Moga mieć przypisane tarcie, gęstość i opór.
Poza tym każda fikstura ma swój kształt.
Fikstury w jednym ciele nie mogą się względem siebie przemieszczać.
Oznacza to, że fikstury przywiązane do jednego ciała poruszają się razem.

Wszystkie klasy Box2d mają przyrostek b2.

Box2d jest oparta na koncepcie tzw. fabryki.
Oznacza to, że schemat tworzenia dzieci będzie taki:
C/C++
Wlasciciel wl; // przyszły właściciel obiektu cos
KlasaDef cosDef; // klasa zawierająca wszystkie parametry potrzebne do utwotrzenia klasy Klasa
// wypełniane pól cosDef
Klasa * cos = wl.addKlasa( cosDef ); // i tutaj właściciel alokouje i tworzy nowy obiekt Klasa za pomoca wartości z cosDef
//...
wl.destroyKlasa( cos ); // poźniej właściciel niszczy i dealokuje ten obiekt

Symulacja Box2d nie jest "ciągła", lecz przybliżana w dyskretnych momentach za pomocą funkcji b2World::Step().
Aktualizuje ona symulację o podany czas (przyrost czasu od poprzedniego wywołania)

Zatem ogólny zarys programu korzystającego z Box2d będzie taki:
C/C++
b2World world( /*...*/ );

// dodawanie ciał, fikstur i wiązań
// inicjalizacja innych zasobów programu

while( /*...*/ )
// pętla główna
{
    float deltaTime = /*...*/; // przyrost czasu (w sekundach)
   
    world.Step( deltaTime, /*...*/ );
   
    // reszta logiki programu/gry, renderowanie, obsługa zdarzeń itp.
}

Symulacja Box2d jest bardziej stabilna i realistyczna, jeśli program/gra oparty jest na pętli stałokrokowej
Jeśli nie wiesz, co to jest polecam przeczytać sobie ten artykuł: http://temporal.pr0.pl/devblog​/download/arts/fixed_step​/fixed_step.pdf
Jest tam nawet wytłumaczenie, dlaczago tak jest

Z przyczyn technicznych, Box2d operuje na metrach, nie pikselach.
Tak czy inaczej, wszystkie wymiary muszą znajdować się między -10 a 10.
Dlatego też wszystkie wartości wektorowe lub wyrażające prędkość (liniową), przesunięcie czy pozycje muszą być skalowane, jeśli za jednostki używasz pikseli.
Dla gier jako współczynnik skalowania stosuje się zazwyczaj 0.02, ale jeśli symulujesz bardzo duże lub bardzo małe obiekty pamiętaj, żeby dobrze go dostosować.

Box2d jako jednostki obrotu (kątu) i predkości kątowej używa radianów i radianów na sekundę.
Jeśli chcesz używać stopni używaj tych makr:
C/C++
#define DGtoRD(deg) (b2_pi * (deg) / 180.0f)
#define RDtoDG(rad) ((rad) * (180.0f / b2_pi))

Klasy pomocnicze

b2Vec2

Implementacja wektora 2D.
Posiada pola x i y wyrażające przesunięcie na osiach X i Y

C/C++
b2Vec2 v( 0.483, 5.35474 );
v.x += 0.2;
v.y *= 0.05;

v.Set( 0.2, 0.4 ); // ustaw nowe wartości

float l = v.Length(); // długość wektora

b2Vec2 other = v;
v += other; // dodawanie wektorów
v *= 0.34; // mnożenie x i y przez 0.34

b2AABB

Reprezentuje prostokąt o bokach prostopadłych do osi X i Y.

C/C++
b2AABB aabb;
aabb.lowerBound = lt; // lewy górny wierzchołek
aabb.upperBound = rd; // prawy dolny wierzchołek

b2Vec2 extent = aabb.GetExtent(); // środek we współrzędnych lokalnych ((upperBound - lowerBound) * 0.5)
b2Vec2 center = aabb.GetCenter(); // środek we współrzędnych globalnych (GetExtent() + lowerBound)

Kształty, czyli b2Shape

Kształty w Box2d reprezentuje klasa b2Shape i jej dzieci.

Mamy do dyspozycji:
  • koło
  • wielokąt wypukły
  • krawędź (linię)
  • kilka połączonych krawędzi (łańcuch, chain)

Można łatwo obliczyć AABB danego kształtu (nie dotyczy b2ChainShape)
C/C++
b2PolygonShape polygon; // dowolny kształt
//...

b2AABB aabb;
polygon.ComputeAABB( & aabb, b2Transform(), 0 );

b2CircleShape

Reprezentuje koło (wypełnione w środku). Posiada promień m_radius i pozycję środka m_p.

C/C++
b2CircleShape c;
c.m_radius = 0.3;
c.m_p.Set( 0.56, 0.576 );

b2PolygonShape

Reprezentuje wielokąt (wypełniony w środku). Tworzy się go z pozycji poszczególnych wierzchołków.
Nie może mieć ich więcej niż b2_maxPolygonVerticies!

C/C++
b2PolygonShape polygon;

polygon.SetAsBox( 0.35 / 2, 0.26 / 2 ); // tworzy prostokąt. ZA ARGUMENTY PRZYJMUJE WYSOKOŚĆ I SZEROKOŚĆ PODZIELONE PRZEZ 2

const int vxNum = 6;
b2Vec2 vx[ vxNum ] = { /*...*/ };

polygon.Set( vx, vxNum ); // można także ręcznie ustawić pozycje wierzchołków

b2Vec2 vx2 = polygon.GetVertex( 2 ); // pozycja wierzchołka 2
polygon.GetVertexCount(); // liczba wierzchołków

b2EdgeShape

Stanowi krawędź, linię między dwoma punktami. Nie ma grubości ani masy.
Krawędzie nie kolidują ze sobą. Może występować jedynie jako ciało statyczne.
Definiuje się ją za pomocą dwóch punktów, które ma łączyć.

C/C++
b2EdgeShape edge;
edge.Set( vertex1, vertex2 );

edge.m_vertex1; edge.m_vertex2; // pierwszy i drugi punkt (UWAGA: nie mylić z m_vertex0!)

b2ChainShape

Łańcuch z krawędzi. Właściwości takie same jak krawędź.

C/C++
const int vxNum = 9;
b2Vec2 vx[ vxNum ] = { /*...*/ };

b2ChainShape c1, c2;

c1.CreateChain( vx, vxNum ); // łączy podane punkty osobnymi krawędziami (pierwszy z drugim, drugi z trzecim itp.)

c2.CreateLoop( vx, vxNum ); // to samo co CreateChain(), tylko że dodatkowo łączy ostatni punkt z pierwszym

Iteracja po osobnych krawędziach w łańcuchu:
C/C++
b2ChainShape ch;
//...
for( int i = 0; i < ch.GetChildCount(); i++ )
{
    b2EdgeShape ed;
    ch.GetChildEdge( & ed, i );
    // zrób coś z ed
}

Fikstury - b2Fixture

Fikstury, jak było mówione wcześniej stanowią różne fragmenty jednego ciała.
Często ciało ma tylko jedną fiksturę, ale mnogość fikstur jest przydatna:
  • gdy ciało ma mieć skomplikowany kształt, np wielokąt o liczbie wierzchołków większej niż [tt]b2_maxPolygonVerticies[/tt]
  • gdy różne fragmenty ciała maja mieć rózne paramatry, np. tarcie
  • gdy kilka obiektów ma sie poruszać jako całość, nawet jeśli nie są połączone
Fikstury w jednym ciele nie kolidują ze sobą

Fikstura może mieć przypisane:
  • gęstość (ang. density) - ma ona wpływ na masę fikstury, domyślnie 1.0
  • tarcie (ang. friction) - zwykle pomiędzy 0 a 1; 0 oznacza brak tarcia
  • "odbijalność" (potrzebne tłumaczenie) (ang. restitution) - sprawia, że obiekt podczas kolizji odbija się; zwykle pomiędzy 0 a 1, 0 oznacza, że ciało nie będzie sie odbijać, a 1, że odbije się z taką samą siłą, jak miała poprzednio.
  • swój kształt (klasa b2Shape i jej dzieci)
  • sensor - jeśli fikstura jest sensorem, nie koliduje z innymi ciałami; może ona natomiast informować, kiedy "najechała" na inne ciało

C/C++
b2PolygonShape shape;
// tworzymy kształt...

b2Body * body; // ciało, do którego chcemy dodać fiksturę (oczywiście musi to być istniejące ciało)
//...

// wypełniamy parametry fikstury
b2FixtureDef fixDef;
fixDef.shape = & shape; // kształt, jedyny parametr, który musi być ustawiony
fixDef.density = 1.0f; // gęstość
fixDef.friction = 0.3f; // tarcie
fixDef.restitution = 0.1f; // odbicie

b2Fixture * fix1 = body->CreateFixture( & fixDef ); // i tutaj tworzymy fiksturę

b2Fixture * fix2 = body->CreateFixture( & shape, 1.0 ); // jest także alternatywna metoda - fiksturę tworzymy za pomocą tylko kształtu i gęstości

Z jednego kształtu lub jednej definicji fikstury możemy stworzyć wiele fikstur należących do różnych ciał.
Ponadto, podczas tworzenia fikstury kształt jest klonowany i alokowany, więc nie trzeba martwić się o zakresy widoczności.
Już stworzona b2Fixture oferuje nam wiele możliwości:
C/C++
fix->GetShape(); // wskaźnik do kształtu
fix->GetBody(); // wskaźnik do ciała, w którym jest ta fikstura
fix->GetAABB( 0 ); // trochę mniej dokładne AABB kształtu. Jeśli chcesz uzyskać dokładny wynik użyj metody z paragrafu o [tt]b2Shape[/tt]

fix->GetDensity(); // zwraca gęstość
fix->SetDensity( 0.67 ); // ustawia gęstość
// tak samo dla parametrów friction, restitution i sensor
Raz stworzona fikstura nie może zmieniać swojego kształtu!

Filtrowanie kolizji

Jeśli nie chcemy, by niektóre fikstury ze soba kolidowały, możemy uzyć filtrowania kolizji.
Każda fikstura może przynależeć do jednej lub kilku kategorii (maksymalnie 16).
Jeśli mamy ustawioną kategorię, możemy ustawić, czy dana fikstura ma kolidować z fiksturami z danej grupy, czy nie.

Jesli na przykład chcemy, żeby A nie kolidowało z B, ale kolidowało z C; natomiast B powinno kolidować z C.
C/C++
#define CATEGORY(num) (1 << (num)) // kategoria x to włączony bit x w słowie (16-bitowym)

ADef.filter.categoryBits = CATEGORY( 0 ); // A ma kategorię 0
ADef.filter.maskBits = CATEGORY( 2 ); // i może kolidować z kategorią 2 (C)

BDef.filter.categoryBits = CATEGORY( 1 ); // B ma kategorię 1
BDef.filter.maskBits = CATEGORY( 2 ); // i może kolidować z kategorią 2 (C)

CDef.filter.categoryBits = CATEGORY( 2 ); // C ma kategorię 2
CDef.filter.maskBits = CATEGORY( 1 ) | CATEGORY( 0 ); // i może kolidować z kategoriami 1 (B) oraz 0 (A)

Domyślne wartość categoryBits to 0x0000, a maskBits 0xFFFF. Oznacza to, że domyślnie fikstura nie ma przypisanej żednej kategorii, ale może kolidować z wszystkimi kategoriami.

Ciała - b2Body

Ciało to kilka fikstur połączonych razem.
Definiują je następujące własności:
  • typ (ang. type) - ciało dynamiczne, statyczne lub kinematyczne
  • pozycja (ang. position) - przesunięcie wszystkich punktów ciała względem punktu (0, 0) świata
  • kąt (ang. angle) (w radianach)
  • opór (ang. damping) - podobnie jak tarcie, tylko, że występuje przy każdym ruchu; zazwyczaj między 0 a 0.1
  • skalowanie grawitacji (ang. gravity scale) - między 0 a 1. 0 oznacza brak wpływu grawitacji na ciało.
  • stały obrót (ang. fixed rotation) - jeśli ciało ma ten parametr włączony, nie może zmienić swojago kątu obrotu.
  • pocisk (ang. bullet) - ciało z tym parametrem będzie miało właczony mechanizm CCD (Continous Physics)

Ciało tworzymy za pomocą metody CreateBody() klasy b2World:
C/C++
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody; // typ (domyślnie b2_staticBody)
bodyDef.position.Set( 0.0f, 3.5f ); // pozycja
bodyDef.angle = 0.5f; // obrót
bodyDef.damping = 0.04f; // opór powietrza (domyślnie 0.0f)
bodyDef.gravityScale = 0.0f; // skala grawitacji (domyślnie 1.0f)
bodyDef.fixedRotation = true; // stały obrót (domyślnie wyłączone)
bodyDef.bullet = false; // CCD (domyślnie wyłączone)
// z tych parmetrów obowiązkowe jest tylko ustawienie pozycji

b2Body * body = world.CreateBody( & bodyDef );

// ...
// poźniej możemy oczywiście to ciało usunąć (automatycznie niszczy to wszystkie fikstury przywiązane do tego ciała)
world.DestroyBody( body );

Działania na ciałach:
C/C++
body->GetPosition(); // pozycja
body->GetAngle(); // kąt obrotu (W RADIANACH!)
body->GetLocalCenter(); // środek ciężkości we współrzędnych lokalnych
body->GetGlobalCenter(); // środek ciężkości we współrzędnych globalnych
body->GetMass(); // masa (kg)
body->GetLinearVelocity(); // prędkość liniowa ciała (wektor)
body->GetAngularVelocity(); // prędkość kątowa ciała (radiany na sekundę)
body->GetType(); // typ ciała
body->GetWorld(); // wskaźnik do świata, w którym ciało jest utworzone

body->GetWorldPoint( b2Vec2( 0.0f, 8.7f ) ); // konwertuje współrzędne lokalne na globalne
body->GetLocalPoint( b2Vec2( 3.5f, 5.6f ) ); // globalne na lokalne

Siły vs impulsy vs prędkości

Do każdego ciała możemy zaaplikować liniową albo kątową siłę (ang. force), implus (ang. impluse) lub prędkość (ang. velocity).
Aplikując prędkość ustawiamy bezwzględną szybkość, z jaką się porusza/obraca. Można to porównać do biegnącego chłopca, który nagle zderzył się ze ścianą. Ta sciana "zmieniła" jego prędkość do 0.
Aplikując impuls przez chwilę działamy z dużą siłą na ciało. Na przykładzie chłopca: chłopiec biegnie i nagle ktoś mocno popchnął. Chłopiec zwiększa swoją prędkość, ale po chwili dodatkowe przyspieszenie znika, i chłopiec biegnie dalej.
Aplikując siły przez cały krok symulacji działamy na ciało. Na przykładzie chłopca: chłopiec biegnie, a ktoś za nim ciągnie przywiązaną do niego linę.
Jeśli chłopiec początkowo biegł z siłą F1, a ktoś zaczął go ciągnąć do tyłu z siłą F2, to wynikowa siła poruszania się chłopca będzie wynosiła F1 - F2.
Dodatkowo aplikowanie siły jest zależne od kroku czasowego. Im większy krok czasowy, tym jedna siła będzie miała różny wpływ na ciało.

C/C++
body->SetLinearVelocity( b2Vec2( 0.0f, 2.4f ) ); // ustawiamy bezwzględną prędkość liniową
body->SetAngularVelocity( 3.5f ); // ustawiamy bezwzględną prędkość kątową (radiany/sekundę)

body->ApplyLinearImpulse( force, point ); // implus do punktu point (we współrzędnych globalnych)
body->ApplyAngularImpulse( 3.5f ); // impuls kątowy o sile 3.5

body->ApplyForce( force, point ); // aplikuję siłę do punktu point (we współrzędnych globalnych)
body->ApplyForceToCenter( b2Vec2( 0.5f, 3.45f ) ); // aplikuje siłę na środek ciężkości ciała. Działanie na środek ciężkości ciała nigdy nie zmienia jego obrotu!
body->ApplyTorque( 34.f ); // aplikuje moment obrotowy (siłę kątową)

Świat - b2World

Klasa świata jest klasą niezbędną do stworzenia symulacji. Świat może zawierać ciała i wiązania.

Symulowanie

Świat symulujemy za pomocą metody Step().
Dokładność i szybkość symulacji wyznaczają następujące czynniki:
  • krok czasowy - odstęp czasu pomiędzy kolejnymi wywołaniami metody Step() (im mniejszy, tym lepiej; większa stabilność symulacji przy stałym)
  • dokładność symulowania pozycji i dokładność symulowania prędkości - zwiększa dokładność symulacji, zmniejsza szybkość
  • liczba ciał i wiązań - świat z większą liczbą ciał i wiązań będzie symulował się wolniej
  • różnica między rozmiarami ciał - kolizja bardzo dużego ciała z bardzo małym nie będzie zbyt dokładna
  • zastosowane skalowanie - Box2d działa najlepiej dla wielkości od -10 do 10.

C/C++
float deltaTime; // krok czasowy
int positionIt, velocityIt; // dokładność symulowania pozycji i prędkości (zalecane wartości to 3 i 8)

world.Step( deltaTime, velocityIt, positionIt );

Iteracja po elementach świata

Iteracja po wszystkich ciałach:
C/C++
for( b2Body * body = world.GetBodyList(); b; b = b->GetNext() )
{
    // zrób coś z body
}
Nigdy nie usuwaj ciała podczas iterowania! Sprawi to, że wywołanie metody GetNext() na tym ciele spowoduje crash programu!
Ciało jest alokowane i dealokowane w metodach CreateBody() i DestroyBody(). Po zniszczeniu ciała wskaźnik będzie wskazywał na nieistniejący (już) obiekt.
Zamiast tego zapisz wskaźnik na następne ciało:
C/C++
b2Body * next = NULL;
for( b2Body * body = world.GetBodyList(); b; b = next )
{
    next = body->GetNext();
   
    // teraz możesz już spokojnie usunąć body
}

Iteracja po wszystkich kolizjach występujących w tym kroku:
C/C++
for( b2Contact * c = world.GetContactList(); c; c = c->GetNext() )
{
    b2Fixture * fixtureA = c->GetFixtureA();
    b2Fixture * fixtureB = c->GetFixtureB();
   
    // fixtureA i fixtureB kolidują ze sobą
}

AABB Query i RayCast

Te metody szukają wszystkich ciał spełniających pewien warunek.
AABB Query (ang. pobieranie po AABB) szuka ciał które w całości bądź częściowo leżą w podanym regionie.
RayCast (ang. rzutowanie przez promień) szuka ciał, które leżą na odcinku między dwoma punktami

Pierwsza metoda:
C/C++
class MyAABBQueryCallback
    : public b2QueryCallback
{
public:
    // ta metoda wywoływana jest dla każdej fikstury w regionie
    bool ReportFixture( b2Fixture * fixture )
    {
        // zrób coś
       
        return true; // kontynuuj wyszukiwanie
        // lub
        return false; // nie szukaj dalej
    }
};

MyAABBQueryCallback cb;
b2AABB aabb; // region wyszukiwania
aabb.lowerBound.Set( - 1.0f, - 1.0f );
aabb.upperBound.Set( 1.0f, 1.0f );

myWorld->Query( & cb, aabb ); // wyszukaj!

RayCast:
C/C++
class MyRayCastCallback
    : public b2RayCastCallback
{
public:
    float ReportFixture( b2Fixture * fixture, const b2Vec2 & point, const b2Vec2 & normal, float32 fraction )
    // kolejno argumenty: znaleziona fikstura, punkt pierwszego kontaktu promienia z fiksturą, normalna (wektor prostopadły do płaszczyzny której jako pierwszy dotknął promień), odległość od początka promienia (między 0 a 1)
    {
        // zrób coś
       
        return 1; // szukaj dalej
        // lub
        return 0; // nie szukaj dalej
        // lub
        return fraction; // przestaw punkt końcowy promienia na point
    }
};

MyRayCastCallback cb;
b2Vec2 point1( - 1.0f, 0.0f ); // początek promienia

b2Vec2 point2( 3.0f, 1.0f ); // koniec promienia

world.RayCast( & cb, point1, point2 ); // szukaj!

Listenery, czyli "coś się stało"

Listenery czyli nasłuchiwacze wywołują określoną metodę, gdy coś się stanie.
Mamy dwa rodzaje listener'ów:
  • destruction listener (ang. nasłuchiwacz usunięcia) - wywołuje odpowiednią metodę, gdy fikstura lub wiązanie zostało zniszczone
  • contact listener (ang. nasłuchiwacz kolizji) - wywołuje określoną metodę, gdy występuje kolizja

Destruction listener:
C/C++
struct MyDestructionListener
    : b2DestructionListener
{
    // metoda wywoływana zaraz przed zniszczeniem fikstury
    void SayGoodbye( b2Fixture * fixture )
    {
       
    };
   
    // metoda wywoływana zaraz przed zniszczeniem wiązania (o tym później)
    vois SayGoodbye( b2Joint * joint )
    {
    }
};

MyDestructionListener l;
world.SetDestructionListener( & l );

Contact listener:
C/C++
struct MyContactListener
    : b2ContactListener
{
   
    // metoda wywoływana na początku każdej kolizji
    void BeginContact( b2Contact * c )
    {
        b2Fixture * fixtureA = c->GetFixtureA();
        b2Fixture * fixtureB = c->GetFixtureB();
       
        // fixtureA i fixtureB kolidują ze sobą
    }
   
    // metoda wywoływana po skończeniu każdego kontaktu
    void EndContact( b2Contact * c )
    {
        b2Fixture * fixtureA = c->GetFixtureA();
        b2Fixture * fixtureB = c->GetFixtureB();
       
        // fixtureA i fixtureB już ze sobą nie kolidują
    }
};

MyContactListener l;
world.SetContactListener( & l );

Nigdy nie usuwaj żadnego ciała, wiązania ani fikstury podczas callbacków BeginContact() i EndContact()!
Próba zrobienia tego spowoduje błąd podobny do tego:
Assertion failed:
line: xxx
expression: IS_LOCKED(world)
Zamiast tego zapamietaj fiksturę do zniszczenia:
C/C++
std::queue < b2Body *> forDestroy;

struct MyContactListener
    : b2ContactListener
{
    void BeginContact( b2Contact * c )
    {
        //...
        if( haveToDestroyBody )
        {
            forDestroy.push( body );
        }
    }
};

// gdzieś w pętli głównej

while( !forDestroy.empty() )
{
    world.DestroyBody( forDestroy.front() );
    forDestroy.pop();
}

Wiązania - b2Joint

Nie, nie chodzi o takie dżointy :)
W Box2d wiązania (jointy) (jak już było wcześniej powiedziane) w pewnym stopniu ograniczają ruch ciała.
Każde wiązanie musi występować pomiędzy dwoma ciałami, z czego co najmniej jedno z nich musi być dynamiczne.
Każdy joint jest potomkiem klasy abstrakcyjnej b2Joint i ma parametry:
  • 2 ciała, do których jest przywiązany
  • punkty zaczepu - punkty w ciałach, w których joint jest "przypięty"
  • kolizja - ciała połączone wiązaniami mogą ze sobą nie kolidować

Tworzymy je w następujący sposób:
C/C++
b2XXXJointDef jointDef; // np b2PrismaticJointDef

jointDef.Initialize( /* parametry różnią się w zależności od typu wiązania */ );
jointDef.collideConnected = true; // czy połączone ciała mają kolidować?

b2XXXJoint * joint =( b2XXXJoint * ) world.CreateJoint( & jointDef ); // tworzymy joint

// ...

world.DestroyJoint( joint ); // oczywiście możemy go później usunąć
 
Każdy rodzaj wiązania odziedzicza po klasie b2Joint następujące metody:
C/C++
joint->GetBodyA(); // wskaźnik na pierwsze ciało
joint->GetBodyB(); // wskaźnik na drugie ciało
joint->GetAnchorA(); // punkt zaczepu w ciele pierwszym
joint->GetAnchorB(); // punkt zaczepu w ciele drugim
joint->GetCollideConnected(); // czy połączone ciała mają ze sobą kolidować?

Niektóre wiązania udostępniają napęd (ang. motor). Oznacza to, że jeśli wiązanie ma coś wspólnego z obracaniem się, będzie się obracało ze stałą prędkością
C/C++
jointDef.motorEnabled = true; // włączamy motor
jointDef.maxMotorTorque = 20; // maksymalna siła, jakiej może użyć motor
jointDef.motorSpeed = 36; // docelowa prędkość obrotu (radiany/sekundę)
Motor obsługują wiązania revolute, gear i wheel.

distance joint - utrzymanie dystansu

Distance joint utrzymuje stały dystans między dwoma punktami dwóch ciał.
Ten joint może być "twardy" - wtedy nie będzie pozwalał na zmianę dystansu niezlażnie od sił działających na ciała, albo "miękki" - będzie działał jak sprężyna dążąca do utrzymania stałego dystansu
"twardość" reguluje się następującymi wartościami:
  • częstotliwość drgań - im więcej, tym joint będzie twardszy
  • współczynnik tłumienia - tłumienie wychyleń, między 0 a 1. 1 całkowicie wyłącza miękkość

[Box2D] Distance Joint
[Box2D] Distance Joint

C/C++
b2DistanceJointDef jointDef;

jointDef.Initialize( myBodyA, myBodyB, myBodyA->GetWorldCenter(), myBodyB->GetWorldCenter() ); // w tym przypadku łączymy ze sobą środki ciężkości tych dwóch ciał

jointDef.collideConnected = true;

// miękkość
jointDef.frequencyHz = 4.0f; // częstotliwość drgania
jointDef.dampingRatio = 0.45f; // tłumienie

revolute joint - "przypięcie" jednego ciała do drugiego

Ten joint pozwala utrzymać jeden punkt danego ciała w tym samym miejscu, co inny w drugim ciele.
Innymi słowy, "przypszpila" on jakieś ciało do innego w danym punkcie.
Klasa ta udostępnia motor, który pozwala regulować wzajemny obrót tych dwóch ciał.
Możemy także ustalić limit - do jakiego kątu względem kątu początkowego mogą obrócić się ciała.

Zastosowaniem revolute jointów mogą być np. koła, wiatraki, łańcuchy.

[Box2D] Revolute Joint
[Box2D] Revolute Joint

C/C++
b2RevoluteJointDef jointDef;

// pierwsza metoda
jointDef.Initialize( bodyA, bodyB, bodyB->GetWorldCenter() ); // inicjalizacja. Trzeci argument to punkt zaczepu w koordynatach globalnych. W tym punkcie muszą znajdować się fragmenty obydwu tych ciał! (wyjątek: patrz zielona ramka poniżej)

// można tez użyć alternatywnego sposobu, kiedy ciała początkowo na siebie nie nachodzą
jointDef.bodyA = bodyA;
jointDef.bodyB = bodyB;
jointDef.localAnchorA = bodyA->GetLocalCenter(); // ustawiamy punkt zaczepu w pierwszym ciele. Jest on we współrzędnych LOKALNYCH ciała
jointDef.localAnchorB = bodyB->GetLocalCenter(); // i w drugim
// obie te metody będą miały taki sam skutek, ponieważ punkt zaczepu drugiego ciała zostanie natychmiast "przyciągnięty" do punkt zaczepu w pierwszym ciele

jointDef.collideConnected = false; // ciała połączone revolute jointem NIE MOGĄ ze sobą kolidować

// limit
jointDef.enableLimit = true; // musimy włączyć limit
jointDef.referenceAngle = 75.5f; // kąt uznawany za "początkowy", od niego liczą sie limity (domyślnie 0)
jointDef.lowerAngle = 32.f; // minimalny obrót
jointDef.upperAngle = 89.5f; // maksymalny obrót. Pamiętaj o radianach!

// motor
jointDef.enableMotor = true; // musimy włączyć motor
jointDef.motorSpeed = 46.f; // docelowa prędkość
jointDef.maxMotorTorque = 100.0f; // maksymalna siła, jakiej może użyć motor, kiedy próbuje uzyskać prędkość docelową

Punkty zaczepu nie muszą koniecznie być w miejscach, gdzie znajdują się fikstury ciała. Mogą znajdować się "gdzieś", ciała i tak będą połączone.
Wtedy te ciała będą się poruszać jak dwie kulki na końcu sznurka, który w pewnym miejscu jest trzymany w powietrzu.
W takim wypadku można włączyć parametr collideConnected

prismatic joint - ruch po osi

Prismatic joint przypomina przypiętą do pierszego ciała szynę, po której może poruszać sie drugie.
W prismatic joint początkowo ciała udostępniają wspólny punkt, tak jak w revolute joint.
Jednak kąt między nimi jest stały. Ponadto dawana jest oś, względem której ciała mogą się poruszać.
Punkt przypięcia tu jest jeden - wyobraź sobie, że przez niego przechodzi dana oś i ciała poruszają się wzdłuż niej.

To wiązanie udostępnia motor, który ustawia prędkość poruszania się po szynie oraz limit - jak bardzo ciała mogą sie od siebie "rozjechać"

Zastosowaniem prismatic jointów mogą być np. windy, przesuwające się platformy, tłoki.

[Box2D] Prismatic Joint
[Box2D] Prismatic Joint

C/C++
b2PrismaticJointDef jointDef;

b2Vec2 moveAxis( 1.0f, 0.0f ); // ciała będa się poruszać poziomo

jointDef.Initialize( bodyA, bodyB, b2Vec2( 35.f, 52.0f ), moveAxis ); // inicjalizacja

jointDef.collideConnected = true;

// limit
jointDef.enableLimit = true;
jointDef.lowerTranslation = 0.0f; // minimalne przesunięcie
jointDef.upperTranslation = 50.0f; // maksymalne przesunięcie

// motor
jointDef.enableMotor = true;
jointDef.motorSpeed = 30.0f; // prędkość przesuwania się
jointDef.maxMotorForce = 300.0f; // maksymalna siła, jakiej może użyć motor

pulley joint - układ ciężarków

Pulley joint tworzy układ ciężarków - im jeden jest niżej, tym drugi wyżej.
Ten joint ma w sumie 4 punkty zaczepu - 2 na symulowanych ciałach i 2 z których te ciała się "zawieszają".
Jeśli d1 będzie długością pierwszego, a d2 to dla jednego układu bloczków:
d1 + d2 * wp = const
Pojawia sie tu czynnik wp - "przełożenie", czyli jak bardzo działanie na pierwszy ciężarek będzie miało wpływ na drugi
Np. dla wp = 0.5 pociągnięcie ciężarka pierwszego o 2 metry w dół spowoduje podniesienie drugiego o 1 metr.

[Box2D] Pulley Joint
[Box2D] Pulley Joint

C/C++
b2PulleyJointDef jointDef;

b2Vec2 anchor1 = bodyA->GetWorldPoint( 0.0f, - 1.0f ); // punkt zaczepu w pierwszym ciele
b2Vec2 anchor2 = bodyB->GetWorldCenter(); // w drugim

b2Vec2 block1( 3.4f, 2.5f ); // punkt "zwieszania" się pierwszego ciężarka
b2Vec2 block2( 5.4f, 2.5f ); // i drugiego

float ratio = 1.0f; // przełożenie

jointDef.Initialize( bodyA, bodyB, block1, block2, anchor1, anchor2, ratio ); // inicjalizacja

mouse joint - sprężyna

Mouse joint działa podobnie do distance joint'a, tylko że przyciąga jedno ciało do dowolnego punktu w świecie.
Ponieważ ten joint działa tylko na jedno ciało, jako drugie powinno być podane dowolne istniejące statyczne ciało.

C/C++
b2MouseJointDef jointDef;

jointDef.bodyA = myStaticBody; // jakieś statyczne ciało
jointDef.bodyB = body; // ciało, na które chcemy działać

jointDef.target.Set( 0.5f, 24.f ); // punkt, do którego chcemy przyciągać ciało, jednocześnie będący punktem zaczepienia

jointDef.maxForce = 300.0f; // maksymalna siła, z jaką może być przyciagane ciało

jointDef.frequencyHz = 4.0f; // podobnie jak w distance joincie
jointDef.dampingRatio = 0.5f;

// później na utworzonym już joincie:
joint->SetTarget( 0.45f, 34.f ); // zmiana punktu docelowego (ale punktu zaczepu już nie!)

wheel joint - amortyzatory

Wheel joint to połączenie distance, revolute i prismatic jointa.
Pozwala on poruszać się po jednej osi, jednocześnie przyciągając go do pewnego punktu na tej osi. Ponadto pozwala mu sie swobodnie obracać.
To wiązanie powstało tylko w jednym celu: koła w samochodzie/rowerze/motocyklu.

[Box2D] Whell Joint
[Box2D] Whell Joint

Oto fragment realnej gry w którym tworzone jest zawieszenie do samochodu:
C/C++
// amortyzatory
b2WheelJointDef spr;
spr.collideConnected = false;
spr.enableMotor = true;
spr.motorSpeed = 0.0f;
spr.dampingRatio = 4.0f;
spr.frequencyHz = 0.4f;
spr.maxMotorTorque = 1000.0f;

b2Vec2 axis( 0.0f, 1.0f ); // pionowo w dół

spr.Initialize( carBody, leftWheelBody, carOriginPos + b2Vec2( 20.0f, 60.0f ), axis );
world.CreateJoint( & spr );

spr.Initialize( carBody, rightWheelBody, carOriginPos + b2Vec2( 90.0f, 60.0f ), axis );
world.CreateJoint( & spr );
Najlepszą techniką tworzenia takich konstrukcji jest najpierw stworzenie odpowiednich ciał w odpowiednich miejscach, a dopiero potem łączenie ich jointami.

rope joint - lina

Rope joint ustala maksymalny dystans między dwoma punktami dwóch ciał.

C/C++
b2RopeJointDef jointDef;

jointDef.bodyA = bodyA;
jointDef.bodyB = bodyB;
jointDef.localAnchorA = bodyA->GetLocalCenter();
jointDef.localAnchorB = bodyB->GetLocalCenter();
jointDef.maxLength = 30.0f; // maksymalny dystans

Testbed

Nie będę zamieszczał tutaj jednego przykładu zbierającego wszystkie klasy zaprezentowane tutaj.
Natomiast w paczce z Box2d otrzymujemy kod bardzo fajnego programu o nazwie Testbed.
Po kompilacji pozwala testować, bawić się różnymi aspektami Box2d.
Natomiast gorąco polecam analizę kodu źródłowego tego programu.

Wskazówki optymalizacyjne

1. Jeśli kilka obiektów będzie się poruszać razem, utwórz je w ramach jednego ciała.
2. Nie twórz ciała w punkcie (0, 0), a potem dopiero go przesuwaj. Od razu pola position struktury b2BodyDef.
3. Wszystkie statyczne fikstury stwórz w ramach jednego ciała statycznego.
4. Użyj metody b2Body::SetSleep(), jeśli chwilowo nie potrzebujesz symulować danego ciała.

Linki zewnętrzne

Informacje - anglojęzyczne

www.box2d.org/manual.html - Oficjalny manual. Podstawowe źródło informacji o Box2d
kubawal.cba.pl/box2dapi/2.3​/html/API/annotated.html - dokumentacja wersji 2.3 (taką samą otrzymujemy w downloadzie, ale w razie problemów wrzuciłem ją na mój serwer :) )
www.iforce2d.net/b2tut/ - Najbardziej rozbudowany kurs internetowy o Box2d
http:/​/www.emanueleferonato.com​/category/box2d/ - ciekawy blog o Box2d. Przykłady prezentowane są w ActionScript'cie, ale z przełożeniem na c++ nie powinno być problemów :)

Informacje - polskojęzyczne

Ze znalezieniem takowych miałem spore problemy, ale znalazłem parę rzeczy:
http://pawel1503.cba.pl​/index.php/660​/integracja-sfmla-i-box2d-czyli​-kilka-wskazowek-dla-poczatkuj​acych​/ - artykuł o integracji Box2d i SFML. Użyte wersja Box2d i SFML nie są najnowszego wydania, a ponadto uważam, że sposób prezentowany przez autora jest raczej słaby, ale to zawsze coś :)
http://software.com.pl​/box2d-fizyczny-swiat-w-pudelku/ - rozbudowany, lecz odrobinkę przestarzały tutorial.

Programy i biblioteki wspomagające Box2d

Box2D JSON Loader: http://www.iforce2d.net​/b2djson/ - pozwala odczytywać i zapisywać całe symulacje do plików JSON. Niezwykle przydatne przy dużych światach.
R.U.B.E Editor: http://www.iforce2d.net​/rube-free/ - darmowy edytor WYSIWYG dla Box2D. Pozwala szybko stworzyć świat po czym przekonwertować go na kod c++ który stworzy to, co widzisz w edytorze.
R.U.B.E Premium: http://www.iforce2d.net/rube/ - komercyjna wersja powyższego. Nie testowałem osobiście, ale user feedback jest podobno znakomity :)