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
https://github.com/KrzysztofPawlak/WordCharger/tree/wpis8