środa, 29 marca 2017

Początki fizyki Box2D

Powoli nadchodzi czas na bohatera który wkroczy na scenę i będzie się poruszał po poprzednio stworzonej mapie. Bohater jak i wszystkie elementy powinny podlegać określonym zasadom w momencie gdy następuje interakcja, spotkanie kilku elementów czy oddziaływanie między nimi. Znane jest pod hasłem fizyka gry. Mówiąc o tym ma się na myśli wszelkie grawitacje, kolizje, kształty obiektów czy charakterystyczne zachowania obiektów (np. odbicia, obroty). Cały taki mechanizm do wykorzystania możemy znaleźć w bibliotece pod nazwą Box2D. Jest to najbardziej popularną tego typu biblioteka i jest dostępna na wiele języków programowania. Działanie można porównać do pudełka do którego wrzucamy obiekty i od tego momentu wykonywane są na nich kalkulacje, przeliczenia do momentu aż zostaną uśpione bądź zniszczone.

Najpierw deklarujemy obiekt World. Jest to klasa z wyżej wspomnianej biblioteki box2d. Pozwala zarządzać wszystkimi fizycznymi istotami, dynamiczna symulacją i asynchronicznymi zapytaniami (query). Asynchroniczne znaczy dziejące się poza głównym wątkiem, czyli toczącą się gra. Czasami może się pojawiać sytuacja że coś trwa zbyt długo, a program wykonuje się sekwencyjnie. Spowodowałoby to zatrzymanie akcji, co jest niedopuszczalne. Asynchroniczny wątek jest niezależny dziejący się poza wątkiem głównym, nie zabiera mocy obliczeniowej przeznaczonej na akcje gry. Gdy się zakończy informuje o wyniku wątek główny. Drugim elementem koniecznym do zadeklarowania jest Box2DDebugRenderer który jest graficzną reprezentacją tektur i ciał w użytym pudełku box2d.

private World world;
private Box2DDebugRenderer b2dr;

Teraz tworzymy nowy obiekt dla world używając konstruktora w którym podajemy wektor grawitacji i stan uśpienia. Wektor grawitacji składa się z współrzędnych x i y. Wektor jest obiektem składającym się z modułu (długości), punktu zaczepienia oraz kierunku wraz z zwrotem. Dobrze obrazuje to po raz kolejny wikipedia ;d. U nas wektor będzie wskazywać w którą stronę działa grawitacja.


W konstruktorze PlayScreen dopisujemy

world = new World(new Vector2(0, 0), true);
b2dr = new Box2DDebugRenderer();

Podając w parametrach wektora (0, 0) spowodujemy tymczasowy brak grawitacji. Stan uśpienia world ustawiamy na true, dzięki czemu nie będą wykonywane na nim obliczenia. Obiekt będzie statyczny przez co obliczenia są zbędne oraz nie są wymagane by były uwzględniane przy fizyce. Tworzymy również nowy obiekt Box2DDebugRenderer. Dzięki czemu możemy wyświetlać obiekty Box2D wraz z ramką debugowania (pomocnicza zielona otoczka wokół obiektu).

Osadzanie w obiektach fizyki

Wszystkie obiekty które chcemy aby podlegały fizyce gry, potrzeba osadzić w odpowiednich przygotowanych do tego obiektach. To one umożliwią odpowiednie ich przetwarzanie. Teraz konkretnie do naszego zastosowania obiekty które chcemy objąć fizyką to m.in.: platformy, postacie, przeszkody i inne elementy które w przyszłości dołożymy.

Teraz wykonujemy w tym kierunku wymagane czynności. Tworzymy nowy obiekt BodyDef, który zawiera wszystkie dane do stworzenia stałego korpusu. Później do utworzonego ciała dodaje się kształt. Kolejnym krokiem jest utworzenie FixtureDef który jest pewnym znanym stanem obiektu, który może być kontynuowany (kształt, gęstość, tarcie itp.). Stan taki powinien być możliwy do powtórnego odtworzenia. Później przyjrzymy się bardziej tym pojęciom w praktyce, aby zdobyć trochę wyczucia czym one dokładnie są. Na końcu tworzymy również body, które są ciałami posiadającymi wiele fixtures (stanów), mogącymi być z różną orientacją i zmienną pozycją wewnątrz tego ciała.

W konstruktorze PlayScreen

BodyDef bdef = new BodyDef();
PolygonShape shape = new PolygonShape();
FixtureDef fdef = new FixtureDef();
Body body;

Pętla po warstwach

Podstawową pętle zakładam że znasz. Tu jednak skorzystamy z petli for each. Sam zaznaczam że mniej ją stosuję głownie z przyzwyczajenia, dlatego jest mniej intuicyjna dla mnie. Pętla for służy przede wszystkim do sekwencyjnego przeglądania zbiorów. Konstrukcja z grubsza wygląda tak

for(Typ nazwa_obiekt : nazwa_tablica){
     // mięcho ;d
}

Teraz analogia do obiektów z mapy

for(MapObject object : map.getLayers().get(2).getObjects().getByType(RectangleMapObject.class)) {
                       
}

Jako typ wraz z nazwą obiektu używamy MapObject, poszczególne elementy z całego obiektu jakim jest mapa właśnie taką formę przyjmą. Po dwukropku potrzebna nam jest kolekcja z jakiej będziemy te elementy wyciągać. Chcemy różnym warstwom ustawić różna logikę, jak pamiętamy każda warstwa była czymś innym (platformy, background). Jako kolekcje do przejścia po jej wszystkich elementach wyciągniemy tylko jedną warstwę. I dla każdej warstwy osobno zdefiniujemy logikę nową pętlą for each. 

map.getLayers().get(2).getObjects().getByType(RectangleMapObject.class)

Rozwińmy co tu właściwie po kolei wyciągamy. Z mapy bierzemy wszystkie warstwy, get(liczba) odnosimy się do konkretnej warstwy, bierzemy wszystkie obiekty które zawiera, oraz zawężamy do typu obiektu RectangleMapObject. Jak pamiętamy w tiled była to warstwa oznaczona kolorem fioletowym z ikoną różnych figur geometrycznych i była obiektową co umożliwia podpięcie logiki do niej.

Skąd wiadomo że jest to akurat warstwa 2?
Jednym z podejść jest spojrzeć w katalog assets naszego projektu w plik mapy .tmx i sprawdzić która z kolei jest warstwa obiektowa którą potrzebujemy.

Wewnątrz pętli

Tworzymy obiekt klasy Rectangle (prostokąt). I tu taki trochę trik. Obiekt object jest typu MapObject, a potrzebujemy później operować na obiekcie klasy Rectangle (wymaga tego libGDX). Trzeba to odpowiednio przekształcić, co nazywa się rzutowaniem. Rzutujemy znaczy, że konstrukcja zaczyna być traktowana jakby była właśnie typu na który ją rzutowaliśmy. Rzutujemy więc na RectangleMapObject i od teraz object jest tak traktowany. Możemy więc skorzystać z metody którą posiada getRectangle(), co w efekcie da nam zgodność z tym co oczekiwaliśmy (Rectangle).

Rectangle rect = ((RectangleMapObject) object).getRectangle();

Ustawiamy obiektowi bodyDef typ static. Cechą ich jest brak możliwości poruszania i działania na nich różnych sił. Jest idealnym wyborem dla platform, podłoża, ścian. Mniej jest przy tym typie wymaganej mocy obliczeniowej. Są jeszcze 2 możliwości: kinematic i dynamic. Obiekt dynamiczny może się poruszać, działają na niego siły oraz inne dynamiczne, statyczne i kinetyczne obiekty. Na kinematyczne ciała nie działają żadne siły ale mogą za to się poruszać.
  
bdef.type = BodyDef.BodyType.StaticBody;

Kolejną rzeczą jest ustawić odpowiednio pozycję. Bierzemy do tego punkty x, y prostokąt oraz dodajemy jeszcze połowę jego szerokości i wysokości aby punkt zaczepienia znajdował się w środku figury.

bdef.position.set(rect.getX() + rect.getWidth() / 2,
                  rect.getY() + rect.getHeight() / 2);

Następna linia odpowiada na utworzenie na podstawie powyższej wykonanej definicji bdef obiektu body w zbiorze obiektów world, którym jak było wcześniej wspomniane można dowolnie sobie zarządzać.
           
body = world.createBody(bdef);

Tutaj ustalamy kształt obiektu shape, w momencie utworzenia wskazaliśmy że jest typem PolygonShape (wielokąt). Metodą setAsBox doprecyzowuje że jest kwadratem. W parametrach podajemy ponownie środek szerokości i wysokości prostokąta kafelka naszej mapy (rect) podobnie jak przy ustalaniu pozycji.
            
shape.setAsBox(rect.getWidth() / 2, rect.getHeight() / 2);

Właściwie dlaczego tylko połowa ? Zastanówmy się. Załóżmy że chcemy sprawdzić czy 2 elementy się nachodzą w efekcie kolizji. Wymagałoby to sprawdzenia wszystkich kombinacji boków które mogą się nachodzić. Trochę liczenia, a przy dużej ilości obiektów to już jest różnica. Znacznie prościej sprawdzić odległości między środkami. I tak właśnie robi się w tego typu grach.

fdef.shape = shape;

Dalej ustalamy jeden z parametrów obiektowi który miał przechowywać stan. Ustalamy konkretnie kształt na ten kryjący się pod obiektem shape. shape jest tym co ustaliliśmy na kwadrat wraz z szerokością i wysokością.
Inaczej tłumacząc mamy większy obiekt z wieloma parametrami i zapewne metodami. I jeden z tych parametrów w tym przypadku shape nie jest w postaci prostych liczb, tylko jest innym obiektem, który gdzieś tam te wartości wewnątrz posiada.

Czy jest to potrzebne?
Domyślam się dlaczego, chodź może jest inny powód. Spójrzmy na to od strony autorów libGDX. Mamy obiekt klasy fdef który mówi tylko o stanach, gdzie jednym z parametrów jest właśnie kształt. Nie mogli przewidzieć o jaki kształt nam dokładnie chodzi, co będziemy używać, mamy zupełną dowolność, może to być nawet gwiazdka. Gdyby tak nawet było to od klasy fdef wymagałoby to całą masę konstruktorów dla różnych kształtów jakie mogą być przyjmowane w parametrach. Zupełny nonsens.
                       
body.createFixture(fdef);

W końcu tworzymy ciału (body) wcześniej zdefiniowane stany (fdef). Na obecny stan będzie to kształt prostokąta.

Podsumowanie

Podsumowując stworzyliśmy obiekt body który będzie podlegał fizyce gry, który jest typem statycznym i ma kształt kwadratu wielkości kafelka. Tyle tego, a efekt można zapisać w jednym zdaniu ;d

W celu wytestowania w render dopisujemy

b2dr.render(world, camera.combined);

wszystkie elementy powyższej linii pojawiły sie już wcześniej przy innych rzeczach. Używamy metody odpowiedzialnej za renderowanie, podając w parametrach zbiór obiektów i typ kamery.

Efekt poniżej. Z tym że mamy tu pewien problem. Mianowicie jest różnica między obszarem wyświetlanej warstwy, a obszarem debugowania czyli tym który jest ujęty w fizykę. To jednak rozwiążemy następnym razem.



Na dziś tyle. Pozdrawiam.

https://github.com/KrzysztofPawlak/WordCharger/tree/wpis8

2 komentarze:

  1. Krzysztof chyba ratujesz mi życie :) Tzn. w swoim projekcie też wykorzystuje libGDX, tylko że dla mnie jest to całkowicie nowe i przechodzenie przez całą dokumentację trochę zajmuje... Obecnie właśnie skoczyłam etap pierwszego ekranu, u Ciebie to wpis Kamera, widok, akcja. Jeszcze tylko muszę zaznajomić się z Githubem...
    Ojj chyba będę częściej do Ciebie zaglądać :)

    Czekam na kolejne etapy. Ola :)

    OdpowiedzUsuń
  2. Hehe To świetnie. Mam nadzieje że dotrzymasz kroku do końca ;d

    OdpowiedzUsuń