Spis treści
LibGDX udostępnia nam wiele klas do wykorzystania – tematem tego rozdziału jest klasa Actor, a kolejnego – klasa Stage. Poznamy także kilka dodatkowych klas, które przydają się podczas używania klasy Actor.
Pierwsze użycie klasy Actor
Klasa ta reprezentuje obiekt świata gry, któremu można zdefiniować kilka, lub wszystkie, z poniższych cech:
- położenie w świecie gry,
- rozmiar,
- skalę,
- rotację (obrót),
- przypisane akcje, które są obiektami typu Action, za pomocą których możemy zmieniać m. in. stan, położenie, oraz rozmiar, obiektu.
Klasę tę wykorzystujemy, gdy nasza gra będzie składała się z wielu różnych elementów, postaci, i przedmiotów, posiadających własne zestawy tekstur oraz zachowań. Aby z niej skorzystać, tworzymy nową klasę, która ją rozszerza.
Klasa Actor posiada dwie metody, które powinniśmy wywoływać na rzecz obiektów tej klasy – są to metody draw oraz act. Pierwsza odpowiedzialna jest za rysowanie obiektu na ekranie, a druga za aktualizację jego stanu.
Spójrz na poniższy przykład wykorzystania klasy Actor:
package com.kursjava.gamedev; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.scenes.scene2d.Actor; public class MovingActor extends Actor { private final Texture texture; public MovingActor(Texture texture, float x, float y) { this.texture = texture; this.setPosition(x, y); } @Override public void act(float delta) { super.act(delta); } @Override public void draw(Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); batch.draw(texture, getX(), getY()); } }
MovingActor rozszerza klasę Actor. Nadpisujemy dwie metody z klasy bazowej – act oraz draw. Dodałem także konstruktor, który oczekuje tekstury, która będzie skojarzona z tworzonym obiektem klasy MovingActor, oraz dwie wartości określające pozycję na ekranie:
public MovingActor(Texture texture, float x, float y) { this.texture = texture; this.setPosition(x, y); }
Zauważ, że w konstruktorze korzystamy z metody setPosition – ustawia ona pola x i y, które dziedziczymy po klasie bazowej Actor. Metodę setPosition, jak i wiele innych metod, także dostajemy do użytku dzięki rozszerzeniu klasy Actor.
Bazowa metoda draw w klasie Actor jest pusta, więc musimy ją nadpisać, aby cokolwiek zostało narysowane na ekranie. W naszej implementacji tej metody wywołujemy metodę bazową (pomimo, że jest pusta – być może w nowszych wersjach LibGDX będzie tam umieszczony jakiś istotny kod), a następnie, za pomocą obiektu batch przekazanego jako argument, rysujemy teksturę skojarzoną z naszym obiektem:
@Override public void draw(Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); batch.draw(texture, getX(), getY()); }
Metody getX i getY to kolejne metody, które dziedziczmy po klasie Actor. Argument parentAlpha to wartość przezroczystości obiektu nadrzędnego – możemy dzięki temu przeliczyć przezroczystość naszego obiektu uwzględniając przezroczystość obiektu, nad którym się on znajduje.
Klasa MovingActor zawiera także metodę act, ale na razie jedynie wywołujemy w niej metodę bazową:
@Override public void act(float delta) { super.act(delta); }
Metoda act przyjmuje jako argument czas w sekundach, jaki upłynął od jej poprzedniego wywołania. Ten czas, jak wiemy z rozdziału o upływie czasu, zwraca metoda Gdx.graphics.getDeltaTime. W przeciwieństwie do metody draw, metoda act w klasie Actor nie jest pusta i wykonuje sporo operacji związanych z obsługą akcji, które możemy przypisywać do obiektów typu Actor (oraz typów pochodnych), aby wpływać m. in. na zmianę rozmiaru i rotacji obiektu. To zagadnienie przedstawię w rozdziale o animacjach.
Do klasy dodamy jeszcze kod odpowiedzialny za zmianę położenia obiektu – zmieniłem metodę act, by wyglądała następująco:
@Override public void act(float delta) { super.act(delta); float deltaX = 0, deltaY = 0; if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) { deltaX = SPEED * delta; } else if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) { deltaX = -SPEED * delta; } if (Gdx.input.isKeyPressed(Input.Keys.UP)) { deltaY = SPEED * delta; } else if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) { deltaY = -SPEED * delta; } moveBy(deltaX, deltaY); }
W metodzie act sprawdzamy teraz, czy strzałki są naciśnięte – jeżeli tak, to wyznaczamy o ile mają się zmienić współrzędne x oraz y obiektu. Korzystamy z ruchu niezależnego od FPS, który poznaliśmy w rozdziale Ruch niezależny od FPS. Aby zmienić pozycję obiektu, korzystamy z metody moveBy, odziedziczonej z klasy bazowej. SPEED to stała, którą dodałem do klasy:
private final int SPEED = 200;
Musimy jeszcze zmodyfikować kod klasy głównej naszego programu, aby wykorzystać powyższą klasę (pominąłem pakiet i importy):
public class ActorExample extends ApplicationAdapter { private SpriteBatch batch; private Texture catImg; private MovingActor cat; @Override public void create () { batch = new SpriteBatch(); catImg = new Texture("cat.png"); cat = new MovingActor(catImg, 10, 10); } @Override public void render () { Gdx.gl.glClearColor(1, 1, 1, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); cat.act(Gdx.graphics.getDeltaTime()); batch.begin(); cat.draw(batch, 1); batch.end(); } @Override public void dispose () { batch.dispose(); catImg.dispose(); } }
W klasie głównej naszego programu, w metodzie create, tworzymy obiekt klasy MovingActor. W metodzie render wywołujemy metodę act naszego aktora, podając czas, jaki upłynął za pomocą poznanej już metody Gdx.graphics.getDeltaTime. Aby narysować nasz obiekt, wywołujemy jego metodę draw – przekazujemy jako argument obiekt typu SpriteBatch, za pomocą którego aktor narysuje się na ekranie. Zauważ, że nadal musimy wywołać metody begin i end na rzecz obiektu batch.
Po uruchomieniu tego przykładu będziemy mogli przesuwać za pomocą strzałek obiekt na ekranie:
Cechy obiektów typu Actor
Jak wspomniałem na początku rozdziału, typ Actor posiada kilka cech, z których można skorzystać, aby narysować na ekranie zmieniony obraz skojarzony z danym aktorem. Są to:
- Rotacja, którą ustawiamy za pomocą metody setRotation, której przekazujemy o ile stopni aktor ma zostać obrócony. Aktor obracany jest wokół podanego punktu, zwanego z angielskiego origin. Aby obrócić aktora wokół jego środka, należy ustawić origin za pomocą metody setOrigin na połowę wysokości i szerokości tekstury skojarzonej z aktorem.
- Kolor, którego możemy użyć, aby zmienić kolorystykę rysowanego obrazu i/lub ustawić jego częściową przezroczystość. Kolor ustawiamy za pomocą metody setColor, przekazując cztery składowe koloru RGBA: Red, Green, Blue, oraz Alpha (przezroczystość).
- Skala, którą ustawiamy za pomocą metody setScale. Ustawiając skalę aktora na wartość 2 spowodujemy, że jego wymiary zwiększą się dwukrotnie, natomiast wartość 0.5 spowoduje, że szerokość i wysokość będą o połowę mniejsze.
Zarówno skalowanie, jak i obroty, wymagają znajomości rozmiaru aktora. W tym celu należy wywołać metodę setSize, za pomocą której ustawimy szerokość i wysokość.
Jeżeli utworzymy teraz klasę dziedziczącą po klasie Actor i skorzystamy z powyższych metod do ustawienia różnych cech obiektów-aktorów, to w wyniku ich narysowania nie zobaczymy żadnych zmian. Stanie się tak, ponieważ do tej pory korzystaliśmy z najprostszej formy rysowania tekstur na ekranie:
batch.draw(img, 0, 0);
Aby „cechy” aktora zostały wzięte pod uwagę, skorzystamy z przeładowanej wersji metody draw obiektu batch, która przyjmuje o wiele więcej argumentów:
batch.draw( textureRegion, // tekstura skojarzona z aktorem getX(), // polozenie X na ekranie getY(), // polozenie Y na ekranie getOriginX(), // wspolrzedna X srodka obrotu getOriginY(), // wspolrzedna Y srodka obrotu getWidth(), // szerokosc tekstury getHeight(), // wysokosc tekstury getScaleX(), // skala szerokosci getScaleY(), // skala wysokosci getRotation() // rotacja w stopniach );
Za pomocą powyższej wersji metody draw będziemy rysować aktora na ekranie – wszystkie wywoływane metody, które widzisz powyżej, dziedziczmy z klasy Actor. Zwracają one różne cechy aktora, które zostaną wzięte pod uwagę podczas rysowania jego tekstury na ekranie. Ta wersja metody draw wymaga jako pierwszego argumentu obiektu typu TextureRegion, więc taki obiekt utworzymy w konstruktorze.
Jak widzisz, powyższa metoda nie ustawia nigdzie koloru, który ma zmodyfikować sposób rysowania tekstury. Aby ten kolor ustawić, musimy przed wywołaniem metody batch.draw, wywołać inną metodę – batch.setColor:
batch.setColor(getColor());
Metoda getColor to kolejna metoda dziedziczona z klasy Actor.
Czas na przykład – w poniższej klasie utworzyłem kilka obiektów klasy MyActor, która rozszerza klasę Actor, aby zaprezentować, jak można wpłynąć na rysowany obraz za pomocą opisanych wyżej cech klasy Actor. Przykład składa się z klasy głównej oraz klasy MyActor.
Klasa MyActor wygląda następująco:
package com.kursjava.gamedev; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.scenes.scene2d.Actor; public class MyActor extends Actor { private TextureRegion textureRegion; public MyActor(Texture texture, float x, float y) { this.textureRegion = new TextureRegion(texture); this.setPosition(x, y); this.setSize(texture.getWidth(), texture.getHeight()); } @Override public void draw(Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); batch.setColor(getColor()); batch.draw( textureRegion, // tekstura skojarzona z aktorem getX(), // polozenie X na ekranie getY(), // polozenie Y na ekranie getOriginX(), // wspolrzedna X srodka obrotu getOriginY(), // wspolrzedna Y srodka obrotu getWidth(), // szerokosc tekstury getHeight(), // wysokosc tekstury getScaleX(), // skala szerokosci getScaleY(), // skala wysokosci getRotation() // rotacja w stopniach ); } }
W konstruktorze tworzymy obiekt typu TextureRegion, którego oczekuje rozszerzona wersja metody batch.draw, której używamy w metodzie draw naszej klasy MyActor.
W konstruktorze ustawiamy także pozycję na ekranie za pomocą metody setPosition, a także wymiary aktora za pomocą metody setSize – rozmiar aktora jest wymagany, aby poprawnie go obracać i skalować. W naszym przypadku rozmiar aktora to po prostu rozmiar skojarzonej z nim tekstury.
Kod klasy głównej, która korzysta z powyższej klasy MyActor, jest następujący:
package com.kursjava.gamedev; import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; public class DrawingActorsExample extends ApplicationAdapter { private SpriteBatch batch; private Texture catImg; private MyActor normalActor; private MyActor transparentActor; private MyActor greenTintedActor; private MyActor biggerActor; private MyActor smallerActor; private MyActor rotatedActorLeftBottomOrigin; private MyActor rotatedActorCenter; @Override public void create () { batch = new SpriteBatch(); catImg = new Texture("cat.png"); normalActor = new MyActor(catImg, 25, 100); transparentActor = new MyActor(catImg, 85, 100); transparentActor.setColor(1, 1, 1, 0.5f); greenTintedActor = new MyActor(catImg, 145, 100); greenTintedActor.setColor(0, 1, 0, 1); biggerActor = new MyActor(catImg, 205, 100); biggerActor.setScale(2); smallerActor = new MyActor(catImg, 315, 100); smallerActor.setScale(0.5f); rotatedActorLeftBottomOrigin = new MyActor(catImg, 400, 100); rotatedActorLeftBottomOrigin.setRotation(180); rotatedActorCenter = new MyActor(catImg, 410, 100); rotatedActorCenter.setRotation(180); rotatedActorCenter.setOrigin( rotatedActorCenter.getWidth() / 2, rotatedActorCenter.getHeight() / 2 ); } @Override public void render () { Gdx.gl.glClearColor(1, 1, 1, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); batch.begin(); normalActor.draw(batch, 1); transparentActor.draw(batch, 1); greenTintedActor.draw(batch, 1); biggerActor.draw(batch, 1); smallerActor.draw(batch, 1); rotatedActorLeftBottomOrigin.draw(batch, 1); rotatedActorCenter.draw(batch, 1); batch.end(); } @Override public void dispose () { batch.dispose(); catImg.dispose(); } }
Najważniejsze w tym przykładzie jest utworzenie obiektów typu MyActor w metodzie create oraz przypisanie im różnych cech:
- obiekt normalActor to „zwykły” aktor, który zostanie po prostu narysowany na ekranie bez żadnych zmian:
normalActor = new MyActor(catImg, 25, 100);
- obiekt transparentActor zostanie narysowany z 50% przezroczystością, ponieważ ustawiliśmy dla niego kolor 1, 1, 1, 0.5f – ostatnia wartość wyznacza właśnie przezroczystość. Pierwsze trzy składowe ustawione na 1 spowodują, że składowa czerwona, zielona, oraz niebieska, kolorów tekstury aktora nie zostaną zmienione:
transparentActor = new MyActor(catImg, 85, 100); transparentActor.setColor(1, 1, 1, 0.5f);
- obiekt greenTintedActor zostanie narysowany w odcieniach zieleni – ustawiliśmy dla niego kolor, który ma wyzerowaną składową czerwoną oraz niebieską – spowoduje to, że wszystkie kolory tekstury „utracą” swoje czerwone i niebieskie składowe:
greenTintedActor = new MyActor(catImg, 145, 100); greenTintedActor.setColor(0, 1, 0, 1);
- obiekty biggerActor i smallerActor zostaną, odpowiednio, narysowane w powiększeniu i pomniejszeniu, ponieważ ustawiliśmy dla nich skalę:
biggerActor = new MyActor(catImg, 205, 100); biggerActor.setScale(2); smallerActor = new MyActor(catImg, 315, 100); smallerActor.setScale(0.5f);
- obiekt rotatedActorLeftBottomOrigin ma ustawioną wartość rotacji (obrotu) na 180 stopni – obrót zostanie wykonany wokół lewego, dolnego wierzchołka tekstury, ponieważ jest to domyślny punkt obrotu (origin):
rotatedActorLeftBottomOrigin = new MyActor(catImg, 400, 100); rotatedActorLeftBottomOrigin.setRotation(180);
- ostatni obiekt, rotatedActorCenter, zostanie także obrócony o 180 stopni, ale obrót zostanie wykonany wokół środka aktora, ponieważ punkt obrotu został ustawiony na środek za pomocą metody setOrigin:
rotatedActorCenter = new MyActor(catImg, 410, 100); rotatedActorCenter.setRotation(180); rotatedActorCenter.setOrigin( rotatedActorCenter.getWidth() / 2, rotatedActorCenter.getHeight() / 2 );
W metodzie render powyższego programu rysujemy na ekranie wszystkich aktorów. Program po uruchomieniu wyświetla następujące obrazy:
Pierwszy od lewej to „zwykły” aktor. Kolejny to aktor z 50% przezroczystością, a następny to aktor, którego kolory „utraciły” czerwoną i niebieską składową, więc jego tekstura zawiera jedynie odcienie zieleni.
Czwarty od lewej jest powiększony aktor, a za nim – pomniejszony.
Dwa ostatnie obrazy należą do obróconych aktorów. Zauważ, że pierwszy jest poniżej linii wszystkich pozostałych aktorów – stało się tak dlatego, ponieważ został on obrócony względem swojego lewego, dolnego wierzchołka. Drugi z obróconych aktorów został obrócony wokół swojego środka, ponieważ tak ustawiliśmy, za pomocą metody setOrigin, punkt obrotu.
Rysowanie aktorów w ten sposób daj nam możliwości tworzenia różnych efektów i animacji. Manipulowanie skalą, rotacją, położeniem itd. można łatwo osiągnąć za pomocą akcji (obiektów typu Action), które omówię w jednym z kolejnych rozdziałów.
Ruch pod określonym kątem
W grach często mamy potrzebę przesunąć obiekt pod pewnym kątem. Aby to zrobić, musimy odpowiednio wyznaczyć przesunięcie względem współrzędnej x oraz y, zakładając ruch z określoną szybkością.
Dla przykładu, aby przesunąć obiekt o 10 pikseli pod kątem 90 stopni (odmierzając kąt od początku osi x, w kierunku przeciwnym do ruchu wskazówek zegara), powinniśmy zmienić współrzędną y tego obiektu o 10 pikseli, a współrzędną x o 0 pikseli:
Czarne strzałki, podpisane jako x i y, to osie układu współrzędnych. Czerwona linia wyznacza wektor przesunięcia obiektu. Czarna kropkowana strzałka zaznacza kąt, pod jakim obiekt się porusza. Kąt ten zaczynamy liczyć od początku osi x i kontynuujemy w kierunku przeciwnym do ruchu wskazówek zegara. Na rysunku po prawej stronie zaprezentowane jest przesunięcie obiektu – kropkowany fragment wyznacza poprzednią pozycję obiektu (przed przesunięciem).
Analogicznie możemy zaprezentować przesunięcie obiektu, który porusza się pod kątem 120 stopni:
Zauważ, że tym razem zmieniły się obie współrzędne, ale nie o taką samą wartość – obiekt przesunął się „bardziej” w górę, niż w lewo.
Analogicznie moglibyśmy wyznaczyć przesunięcie dla np. kąta 180 stopni – wtedy obiekt przesuwałby się w lewo po osi x, a wartość jego współrzędnej y by się nie zmieniała. Podobnie dla kąta 0 stopni, tylko w tym przypadku obiekt przesuwałby się w prawo.
Aby wyznaczyć o ile współrzędne x oraz y obiektu powinny się zmienić, gdy porusza się on pod pewnym kątem, powinniśmy zastosować funkcje trygonometryczne sinus oraz cosinus. Jest to jednak tak często spotykane zagadnienie w grach, że LibGDX udostępnia dla naszego wygody klasę Vector2, z której możemy w tym celu skorzystać.
Klasa ta reprezentuje wektor – może ona nam posłużyć za wektor określający przesunięcie obiektu. Wektor definiowany jest za pomocą dwóch współrzędnych x oraz y. Możemy obrócić taki wektor o podany kąt, a jego współrzędne x oraz y zostaną dla nas automatycznie przeliczone.
Aby określić wektor, który ma służyć do wyznaczania przesunięcia współrzędnych x oraz y pewnego obiektu pod określonym kątem i na określoną odległość, możemy utworzyć obiekt typu Vector2 podając tylko wartość współrzędnej x. Jej wartość ustawimy na wartość „prędkości” naszego obiektu. Następnie, obrócimy wektor o kąt, pod jakim nasz obiekt ma się poruszać. Klasa Vector2 sama przeliczy nowe wartości dla swoich współrzędnych x oraz y po obrocie. Tak uaktualniony wektor będzie nam służył do przesuwania naszego obiektu.
Dla przykładu, poniższa definicja obiektu typu Vector2 będzie nam służyła do przesuwania naszego obiektu o 50 pikseli na sekundę pod kątem 180 stopni:
goLeftVector = new Vector2(50, 0).setAngle(180);
Z tego wektora będziemy korzystać w następujący sposób:
batch.draw( img, imgX + goLeftVector.x * timeElapsed, imgY + goLeftVector.y * timeElapsed );
Do współrzędnych x oraz y obrazu (reprezentowanych powyżej przez pola imgX oraz imgY) dodajemy wartości x i y wektora, przemnożone przez czas, jak upłynął od początku uruchomienia programu. Dzięki temu, wraz z upływem czasu, obraz będzie przesuwał się zgodnie z kierunkiem wyznaczanym przez kąt wektora z szybkością przekazaną do konstruktora w momencie jego tworzenia. Moglibyśmy zmieniać kąt wektora, aby symulować wykonywanie przez obiektu skrętów.
Poniższy prosty przykład pokazuje zastosowanie kilku wektorów do przesunięcia obrazu, który rysujemy na ekranie:
package com.kursjava.gamedev; import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.math.Vector2; public class AngleMovementExample extends ApplicationAdapter { private SpriteBatch batch; private Texture img; private int imgX = 225, imgY = 175; private Vector2 goLeftVector, goRightVector; private Vector2 goUpVector, goDownVector; private Vector2 go30DegreesAngleVector; private Vector2 go135DegreesAngleVector; private float timeElapsed; @Override public void create () { batch = new SpriteBatch(); img = new Texture("cat.png"); goLeftVector = new Vector2(50, 0).setAngle(180); goRightVector = new Vector2(50, 0).setAngle(0); goUpVector = new Vector2(25, 0).setAngle(90); goDownVector = new Vector2(25, 0).setAngle(270); go30DegreesAngleVector = new Vector2(75, 0).setAngle(30); go135DegreesAngleVector = new Vector2(75, 0).setAngle(135); } @Override public void render () { Gdx.gl.glClearColor(1, 1, 1, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); timeElapsed += Gdx.graphics.getDeltaTime(); batch.begin(); // statyczny obraz batch.draw(img, imgX, imgY); // obraz poruszajacy sie w lewo batch.draw( img, imgX + goLeftVector.x * timeElapsed, imgY + goLeftVector.y * timeElapsed ); // obraz poruszajacy sie w prawo batch.draw( img, imgX + goRightVector.x * timeElapsed, imgY + goRightVector.y * timeElapsed ); // obraz poruszajacy sie w gore batch.draw( img, imgX + goUpVector.x * timeElapsed, imgY + goUpVector.y * timeElapsed ); // obraz poruszajacy sie w dol batch.draw( img, imgX + goDownVector.x * timeElapsed, imgY + goDownVector.y * timeElapsed ); // obraz poruszajacy sie pod katem 30 stopni batch.draw( img, imgX + go30DegreesAngleVector.x * timeElapsed, imgY + go30DegreesAngleVector.y * timeElapsed ); // obraz poruszajacy sie pod katem 120 stopni batch.draw( img, imgX + go135DegreesAngleVector.x * timeElapsed, imgY + go135DegreesAngleVector.y * timeElapsed ); batch.end(); } @Override public void dispose () { batch.dispose(); img.dispose(); } }
W metodzie create tworzonych jest kilka wektorów, aby zaprezentować ruch pod różnym kątem. Pierwszy argument to „prędkość”, jaką chcemy nadać obiektowi – zakładamy, że jest to liczba pikseli na sekundę. Wartość ta różni się pomiędzy różnymi wektorami poniżej, aby przesuwane za ich pomocą obiekty poruszały się z różną szybkością:
goLeftVector = new Vector2(50, 0).setAngle(180); goRightVector = new Vector2(50, 0).setAngle(0); goUpVector = new Vector2(25, 0).setAngle(90); goDownVector = new Vector2(25, 0).setAngle(270); go30DegreesAngleVector = new Vector2(75, 0).setAngle(30); go135DegreesAngleVector = new Vector2(75, 0).setAngle(135);
W metodzie render rysowany jest tam sam obraz kilkukrotnie w różnych lokalizacjach z przesunięciem dzięki zastosowaniu utworzonych wcześniej wektorów:
// obraz poruszajacy sie w prawo batch.draw( img, imgX + goRightVector.x * timeElapsed, imgY + goRightVector.y * timeElapsed );
Pole timeElapsed zawiera czas w sekundach, jaki upłynął od początku działania programu, więc im dłużej program będzie działał, tym bardziej obrazy będą oddalać się od początkowej pozycji określanej przez pola imgX oraz imgY, które ustawiłem na takie wartości, aby statyczny obraz, rysowane na początku metody draw, był wyśrodkowany w oknie:
private int imgX = 225, imgY = 175;
Poniższa animacja przedstawia działanie powyższego programu:
Obraz znajdujący się w środku jest nieruchomy – wszystkie pozostałe obrazy oddalają się od niego, przesuwane za pomocą wyliczenia przesunięć współrzędnych x oraz y z zastosowaniem wektorów.
Wykrywanie kolizji
Gdy w grze występują obiekty, które mogą się poruszać, często zachodzi potrzeba sprawdzenia, czy ich pozycje częściowo się nie pokrywają, tzn. czy występuje kolizja tych obiektów:
Wykrywanie kolizji to niełatwe zadanie, szczególnie, gdy obiekty naszej gry mają nieregularne kształty. W najprostszym przypadku, możemy założyć, że obiekty będą opisywane przez prostokątne obszary – łatwo wtedy wykryć pomiędzy nimi kolizję sprawdzając położenie ich wierzchołków względem siebie. W tym prostym przypadku zakładamy, że obiekty nie mogą się obracać, lub po obrocie prostokąt, którego używamy do wykrywania kolizji, nie jest obracany.
LibGDX udostępnia także bardziej zaawansowaną funkcjonalność do wykrywania kolizji obiektów opisanych przez zestaw wierzchołków. Obiekty te mogą być także obrócone.
Spójrzmy na przykłady wykorzystania obu tych funkcjonalności.
Wykrywanie kolizji pomiędzy prostokątnymi obszarami
LibGDX udostępnia nam klasę Rectangle, która opisuje prostokątny obszar. Możemy ustawić jego lewy, dolny wierzchołek, a także szerokość i wysokość. Klasa ta zawiera metodę overlaps, której możemy przekazać inny obiekt typu Rectangle – jeżeli obszary opisane przez te obiekty nachodzą na siebie, to metoda zwróci wartość true.
Poniższa klasa CollisionActor zostanie użyta w przykładzie, w którym gracz będzie poruszał postacią, która nie będzie mogła przejść przez obiekty symbolizujące ściany bądź inne przeszkody:
package com.kursjava.gamedev; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.scenes.scene2d.Actor; public class CollisionActor extends Actor { private final Texture texture; private final Rectangle collisionRectangle; // 1 public CollisionActor(Texture texture, float x, float y) { this.texture = texture; this.setBounds(x, y, texture.getWidth(), texture.getHeight()); this.collisionRectangle = new Rectangle(x, y, getWidth(), getHeight()); // 2 } @Override public void draw(Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); batch.draw(texture, (int) getX(), (int) getY()); } @Override public void moveBy(float x, float y) { super.moveBy(x, y); collisionRectangle.setPosition((int) getX(), (int) getY()); } public boolean checkCollision(CollisionActor other) { return collisionRectangle.overlaps( other.collisionRectangle ); } }
Obiekty klasy CollisionActor zawierają pole collisionRectangle (1) typu Rectangle, które będzie opisywać obszar zajmowany w świecie gry przez dany obiekt. Początkowe współrzędne i rozmiar tego obszaru ustawiamy w konstruktorze (2). Ustawiamy w nim także, za pomocą metody setBounds, współrzędne obiektu na ekranie oraz jego szerokość i wysokość. Zamiast tej metody moglibyśmy wywołać metody setPosition i setSize, ale skorzystanie z pojedynczej metody jest wygodniejsze.
Gdy obiekt się porusza, musimy zaktualizować nie tylko jego współrzędne, ale także położenie obszaru opisywanego przez obiekt collisionRectangle. Zajmuje się tym przeładowana wersja metody moveBy:
@Override public void moveBy(float x, float y) { super.moveBy(x, y); collisionRectangle.setPosition((int) getX(), (int) getY()); }
Sprawdzanie kolizji pomiędzy dwoma obiektami odbywa się metodzie checkCollision. Korzysta ona z metody overlaps klasy Rectangle, o której wspominałem na początku rozdziału:
public boolean checkCollision(CollisionActor other) { return collisionRectangle.overlaps( other.collisionRectangle ); }
Zarówno w metodzie draw, jak i podczas aktualizacji położenia obiektu collisionRectangle w metodzie moveBy, korzystamy z rzutowania współrzędnych na typ int. Ma to na celu wyeliminowanie potencjalnych problemów z częścią ułamkową pozycji, która może spowodować, że kolizja będzie wykrywana przedwcześnie.
Klasa CollisionActor, używana w klasie głównej przykładu, reprezentuje zarówno obiekt, którym porusza gracz, jak i nieprzekraczalne ściany. Klasa główna wygląda następująco (pominąłem ciała dwóch ostatnich metod – opiszę je za moment):
package com.kursjava.gamedev; import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; public class SimpleCollisionsExample extends ApplicationAdapter { private static final int PLAYER_SPEED = 50; private SpriteBatch batch; private Texture catImg; private Texture blockImg; private CollisionActor player; private CollisionActor[] blocks; @Override public void create () { batch = new SpriteBatch(); catImg = new Texture("cat.png"); blockImg = new Texture("block.png"); player = new CollisionActor(catImg, 0, 0); blocks = new CollisionActor[3]; blocks[0] = new CollisionActor(blockImg, 40, 60); blocks[1] = new CollisionActor(blockImg, 40, 180); blocks[2] = new CollisionActor(blockImg, 230, 160); } @Override public void render () { Gdx.gl.glClearColor(1, 1, 1, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); handleKeyboard(); batch.begin(); for (CollisionActor block : blocks) { block.draw(batch, 1); } player.draw(batch, 1); batch.end(); } @Override public void dispose () { batch.dispose(); catImg.dispose(); blockImg.dispose(); } private void handleKeyboard() { // ... } private void movePlayer(float deltaX, float deltaY) { // ... } }
W klasie głównej wczytujemy obrazy w metodzie create oraz tworzymy cztery obiekty: obiekt gracza player oraz trzy „bloki” (symbolizujące nieprzekraczalne ściany). Wszystkie te obiekty są typu CollisionActor.
W metodzie draw wywołujemy metodę handleKeyboard obsługującą klawiaturę, która będzie miała za zadanie sprawdzać, czy gracz naciska strzałki na klawiaturze. Poza tym, w metodzie draw rysujemy wszystkie obiekty.
Spójrzmy teraz na implementację dwóch pozostałych metod: handleKeyboard oraz movePlayer:
private void handleKeyboard() { float deltaTime = Gdx.graphics.getDeltaTime(); float deltaX = 0, deltaY = 0; if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) { deltaX = PLAYER_SPEED * deltaTime; } else if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) { deltaX = -PLAYER_SPEED * deltaTime; } if (Gdx.input.isKeyPressed(Input.Keys.UP)) { deltaY = PLAYER_SPEED * deltaTime; } else if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) { deltaY = -PLAYER_SPEED * deltaTime; } if (deltaX != 0 || deltaY != 0) { // 1 movePlayer(deltaX, deltaY); } } private void movePlayer(float deltaX, float deltaY) { player.moveBy(deltaX, deltaY); for (CollisionActor block : blocks) { if (player.checkCollision(block)) { // 3 player.moveBy(-deltaX, -deltaY); // 4 break; } } }
W metodzie handleKeyboard sprawdzamy, czy gracz nacisnął strzałki na klawiaturze – jeżeli tak, to wyliczamy przesunięcie w odpowiednim kierunku. Na końcu tej metody wywołujemy metodę movePlayer, jeżeli faktycznie powinniśmy przesunąć postać gracza (1).
Wykrywanie kolizji pomiędzy obiektami odbywa się w metodzie movePlayer. Zauważ, że najpierw przesuwamy postać gracza (2), a później w pętli sprawdzamy kolizje pomiędzy nim a wszystkimi „blokami”. Jeżeli metoda checkCollision (3) zwróci true, oznacza to, iż gracz „wpadł” na nieprzekraczalny blok. W tym przypadku musimy „wycofać” postać gracza, tzn. cofnąć wykonany przez niego ruch – w przeciwnym razie, fragment obrazu postaci gracza znajdzie się na nieprzekraczalnym bloku. Aby to zrobić, ponownie wywołujemy metodę moveBy, ale przekazuje przeciwne wartości (4), niż poprzednio, dzięki czemu postać gracza wróci na swoje miejsce przed kolizją.
W wyniku działania tego programu możemy poruszać postacią na ekranie, ale nie możemy przekraczać pewnych obszarów:
Złożone kolizje
Poza kolizjami korzystającymi z prostokątnych obszarów, LibGDX udostępnia także skomplikowane, dokładne wyznaczanie kolizji pomiędzy obiektami o nieregularnych kształtach, z wzięciem pod uwagę, że obiekty te mogą być obrócone o pewien kąt.
Dodatkowo, LibGDX może dla nas wyznaczać wektor minimalnego przesunięcia, z którego możemy skorzystać, aby przesunąć nasz obiekt, by „stykał się” z obiektem, z którym zaszła kolizja, dając efekt dojścia do nieprzekraczalnej przeszkody, bez efektu wchodzenia tekstury jednego obiektu na drugi:
Na powyższym obrazie, po lewej stronie, widzimy przykład dwóch obiektów, które w wyniku kolizji „weszły” na siebie. Często w grach mamy potrzebę uniknięcia takich przypadków. Dla przykładu, nie chcemy, aby tekstura reprezentująca postać, którą porusza gracz, mogła „wejść” np. na ścianę. Na obrazie po prawej stronie widzimy przykład, gdy kolidujące ze sobą obiekty „stykają się” swoimi teksturami. Aby osiągnąć ten drugi efekt możemy skorzystać właśnie z wektora minimalnego przesunięcia, który może dla nas wyznaczyć LibGDX.
Klasą, którą będziemy stosować do wyznaczenia obszaru zajmowanego przez nasze obiekty, jest klasa Polygon z biblioteki LibGDX. Gdy tworzymy obiekt klasy Polygon, powinniśmy przekazać jako argument do konstruktora tablicę współrzędnych – każda para tych współrzędnych wyznacza punkt obszaru obiektu. Zbiór takich punktów wyznaczy obszar, który dany obiekt zajmuje, i który ma być brany pod uwagę podczas sprawdzania kolizji. Współrzędne punktów podajemy względem lewego, dolnego wierzchołka obiektu, zakładając, że punkt ten ma współrzędne 0, 0.
Dla przykładu, w poniższy sposób można utworzyć obiekt typu Polygon opisujący prostokąt o szerokości w oraz wysokości h:
collisionPolygon = new Polygon( new float[] { 0, 0, // lewy dolny wierzcholek w, 0, // prawy dolny wierzcholek w, h, // prawy gorny wierzcholek 0, h // lewy gorny wierzcholek } );
Jako argument konstruktora przekazałem tablicę – każde dwa elementy tej tablicy wyznaczają jeden punkt opisujący obszar – pierwsza wartość każdej pary to wartość X, a druga to wartość Y. Graficznie powyższy obszar można przedstawić następująco:
W przykładowym programie do tego rozdziału będziemy wykrywali kolizje pomiędzy obiektami o prostokątnym kształcie, a także o kształcie rombu:
Aby wyznaczyć punkty, które opiszą obszar tego rombu, wystarczy zauważyć, że każdy z jego wierzchołków znajduje się w połowie wysokości bądź szerokości prostokąta, w którym się on znajduje:
Gdzie w i h to, odpowiednio, wysokość i szerokość obszaru, w którym romb się znajduje, zaznaczonego na powyższy obrazku za pomocą szarych, przerywanych linii.
Obiekty klasy Polygon, poza nieregularnym kształtem, mogą być także obrócone bądź przeskalowane. Postać, którą w przykładowym programie będzie kierował gracz, będzie mogła się obracać w dowolną stronę, więc zanim dokonamy sprawdzenia, czy nie zaszły kolizje, zaktualizujemy obiekt typu Polygon, nadając mu odpowiednią rotację.
Przykładowy program składa się z klasy StationaryActor, PlayerActor, oraz klasy głównej ActorCollisionsExample. Spójrzmy najpierw na klasę StationaryActor, która będzie reprezentować różne obiekty na ekranie – pomiędzy nimi a graczem będziemy wykrywać kolizje.
package com.kursjava.gamedev; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.math.Polygon; import com.badlogic.gdx.scenes.scene2d.Actor; public class StationaryActor extends Actor { private final Texture texture; private final boolean canBeCollected; private final Polygon collisionPolygon; public StationaryActor( Texture texture, boolean canBeCollected, float x, float y, float[] collisionVertices) { this.texture = texture; this.canBeCollected = canBeCollected; this.setBounds(x, y, texture.getWidth(), getHeight()); this.collisionPolygon = new Polygon(collisionVertices); // 1 this.collisionPolygon.setPosition(x, y); // 2 } @Override public void draw(Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); if (isVisible()) { // 3 batch.draw(texture, getX(), getY()); } } public Polygon getCollisionPolygon() { // 4 return collisionPolygon; } public boolean canBeCollected() { return canBeCollected; } }
Ta prosta klasa zawiera teksturę oraz obiekt typu Polygon (1), który wyznaczy jej obszar podczas wykrywania kolizji. Punkty opisujące obszar obiektu zostaną przekazane jako argument konstruktora. Musimy jeszcze ustawić położenie obszaru collisionPolygon na ekranie (2).
Klasa posiada także pole canBeCollected. Jeżeli zajdzie kolizja pomiędzy graczem a obiektem, który można zbierać, to ustawimy pole visible takiego obiektu na false, aby przestał być rysowany na ekranie (3).
Metoda getCollisionPolygon (4) zwraca obszar zajmowany przez obiekt. Jeżeli obiekty opisywane przez klasę StationaryActor mogłyby się poruszać w świecie gry, to przed zwróceniem obiektu collisionPolygon powinniśmy uaktualnić jego położenie na ekranie. Są to jednak nieruchome obiekty, więc raz ustawione położenie w konstruktorze (2) wystarczy.
Kolejna klasa, PlayerActor, reprezentuje postać, którą porusza gracz. Początek klasy jest następujący:
public class PlayerActor extends Actor { private final static int ROTATION_SPEED = 180; private final TextureRegion textureRegion; private final Polygon collisionPolygon; private final Vector2 movementVector; public PlayerActor( Texture texture, float x, float y, float[] collisionVertices) { textureRegion = new TextureRegion(texture); collisionPolygon = new Polygon(collisionVertices); movementVector = new Vector2(100, 0).setAngle(90); // 1 this.setRotation(90); // 2 this.setBounds(x, y, texture.getWidth(), texture.getHeight()); // punkt na obszarze tekstury gracza, wokół którego // gracz powinien być obracany this.setOrigin(getWidth() / 2, getHeight() / 2); // 3 }
Poza teksturą, klasa zawiera pole collisionPolygon do wykrywania kolizji, oraz obiekt movementVector, którego będziemy używać to wyznaczania przesunięcia postaci gracza względem współrzędnych x oraz y pod określonym kątem. Początkowo ustawiamy kąt poruszania się na 90 stopni (1). Ustawiamy także rotację aktora na 90 stopni (2). W linii (3) ustawiamy środek aktora jak punkt, wokół którego powinien on być obracany.
Kolejna metoda to act:
@Override public void act(float delta) { super.act(delta); if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) { // 1 rotateBy(-ROTATION_SPEED * delta); // 2 movementVector.setAngle(getRotation()); } else if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) { // 3 rotateBy(ROTATION_SPEED * delta); // 4 movementVector.setAngle(getRotation()); } if (Gdx.input.isKeyPressed(Input.Keys.UP)) { // 5 moveBy(movementVector.x * delta, movementVector.y * delta); // 6 } else if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) { // 7 moveBy(-movementVector.x * delta, -movementVector.y * delta); // 8 } }
Za pomocą strzałek ← →, gracz może obracać w lewo i prawo postać gracza. W liniach (1) (3) obsługujemy te klawisze i obracamy aktora oraz wektor jego przesunięcia o wyliczoną wartość obrotu. Wartość obrotu jest niezależna od FPS, ponieważ ROTATION_SPEED przemnażamy przez czas, jaki upłynął od ostatniego wywołania tej metody (2) (4). Wartość ROTATION_SPEED ustaliliśmy na 180 na początku klasy PlayerActor, więc postać gracza będzie się obracać z prędkością 180 stopni na sekundę.
Do poruszania się do przodu i do tyłu należy użyć strzałek ↑ ↓ (5) (7) Do wyliczenia przesunięcia postaci gracza korzystamy z wektora przesunięcia movementVector (6) (8).
Ponieważ postać gracza może się obracać, do jej narysowania używamy rozszerzonej wersji metody draw obiektu typu Batch. Korzystaliśmy już niej w poprzednich rozdziałach:
@Override public void draw(Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); batch.draw( textureRegion, // tekstura skojarzona z aktorem getX(), // polozenie X na ekranie getY(), // polozenie Y na ekranie getOriginX(), // wspolrzedna X srodka obrotu getOriginY(), // wspolrzedna Y srodka obrotu getWidth(), // szerokosc tekstury getHeight(), // wysokosc tekstury getScaleX(), // skala szerokosci getScaleY(), // skala wysokosci getRotation() // rotacja w stopniach ); }
Ostatnią metodą jest checkCollision:
public void checkCollision(StationaryActor obj) { Polygon otherPolygon = obj.getCollisionPolygon(); // 1 // 2 Polygon thisPolygon = collisionPolygon; thisPolygon.setPosition(getX(), getY()); thisPolygon.setOrigin(this.getOriginX(), this.getOriginY()); thisPolygon.setRotation(this.getRotation()); // 3 if (!thisPolygon.getBoundingRectangle().overlaps( otherPolygon.getBoundingRectangle()) ) { return; } Intersector.MinimumTranslationVector mtv = // 4 new Intersector.MinimumTranslationVector(); if (Intersector.overlapConvexPolygons( // 5 collisionPolygon, obj.getCollisionPolygon(), mtv) ) { if (obj.canBeCollected()) { obj.setVisible(false); } else { this.moveBy( mtv.normal.x * mtv.depth, mtv.normal.y * mtv.depth ); } } }
Na początku metody przypisujemy do pomocniczych zmiennych dwa obiekty typu Polygon – ten z przekazanego jako argument obiektu typu StationaryActor (1), a także ten opisujący obiekt PlayerActor (2). Jako, że postać gracza może się poruszać i obracać, musimy ustawić pozycję oraz rotację obiektu typu Polygon, którego używamy do wykrywania kolizji dla tego obiektu.
W linii (3) wykonujemy początkowe sprawdzenie, czy prostokąty, w których zawarte są obydwa obiekty typu Polygon, nachodzą na siebie. Takie wczesne sprawdzenie pozwala szybko wyeliminować sytuację, w której na pewno nie mogłoby dojść do kolizji obiektów:
Złożone wykrywanie kolizji jest kosztownym procesem i najlepiej ograniczyć jego wykonywanie do minimum. Taki jest właśnie cel sprawdzenia wykonywanego w linii (3).
W linii (4) tworzymy obiekt typu MinimumTranslationVector, do którego LibGDX zapisze minimalne przesunięcie obiektu w przypadku wykrycia kolizji. Ten wektor będzie pozwalał na takie przesunięcie obiektu, aby był on maksymalnie „dosunięty” do drugiego obiektu, z którym nastąpiła kolizja.
Do wykrywanie złożonych kolizji pomiędzy dwoma obiektami typu Polygon, korzystamy z klasy Intersector, którą udostępnia nam LibGDX. Korzystamy z metody overlapConvexPolygons klasy Intersector, która przyjmuje jako argument dwa obiekty typu Polygon i obiekt typu MinimumTranslationVector. Jeżeli zachodzi kolizja pomiędzy obszarami opisanymi przez oba obiekty typu Polygon, metoda ta zwraca true oraz wypełnia obiekt typu MinimumTranslationVector, który przekazujemy jej jako argument:
if (Intersector.overlapConvexPolygons( // 5 collisionPolygon, obj.getCollisionPolygon(), mtv) )
Jeżeli zachodzi kolizja i obiekt, z którym zaszła kolizja, możesz zostać „zjedzony”, to ustawimy jego wartość visible na false, aby nie był już rysowany. W przeciwnym razie, przesuwamy postać gracza korzystając z obiektu mtv klasy MinimumTranslationVector, który został wypełniony w metodzie overlapConvexPolygons, aby obiekt gracza znalazł się przy krawędzi obiektu, z którym zaszła kolizja. Do przesunięcia korzystamy z metody moveBy, którą dziedziczymy z klasy Actor:
if (obj.canBeCollected()) { obj.setVisible(false); } else { this.moveBy( mtv.normal.x * mtv.depth, mtv.normal.y * mtv.depth ); }
Pełny kod tej klasy jest następujący:
package com.kursjava.gamedev; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.math.Intersector; import com.badlogic.gdx.math.Polygon; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.Actor; public class PlayerActor extends Actor { private final static int ROTATION_SPEED = 180; private final TextureRegion textureRegion; private final Polygon collisionPolygon; private final Vector2 movementVector; public PlayerActor( Texture texture, float x, float y, float[] collisionVertices) { textureRegion = new TextureRegion(texture); collisionPolygon = new Polygon(collisionVertices); movementVector = new Vector2(100, 0).setAngle(90); this.setRotation(90); // wstępna rotacja postaci gracza this.setBounds(x, y, texture.getWidth(), texture.getHeight()); // punkt na obszarze tekstury gracza, wokół którego // gracz powinien być obracany this.setOrigin(getWidth() / 2, getHeight() / 2); } @Override public void act(float delta) { super.act(delta); if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) { rotateBy(-ROTATION_SPEED * delta); movementVector.setAngle(getRotation()); } else if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) { rotateBy(ROTATION_SPEED * delta); movementVector.setAngle(getRotation()); } if (Gdx.input.isKeyPressed(Input.Keys.UP)) { moveBy(movementVector.x * delta, movementVector.y * delta); } else if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) { moveBy(-movementVector.x * delta, -movementVector.y * delta); } } @Override public void draw(Batch batch, float parentAlpha) { super.draw(batch, parentAlpha); batch.draw( textureRegion, // tekstura skojarzona z aktorem getX(), // polozenie X na ekranie getY(), // polozenie Y na ekranie getOriginX(), // wspolrzedna X srodka obrotu getOriginY(), // wspolrzedna Y srodka obrotu getWidth(), // szerokosc tekstury getHeight(), // wysokosc tekstury getScaleX(), // skala szerokosci getScaleY(), // skala wysokosci getRotation() // rotacja w stopniach ); } public void checkCollision(StationaryActor obj) { Polygon otherPolygon = obj.getCollisionPolygon(); Polygon thisPolygon = collisionPolygon; thisPolygon.setPosition(getX(), getY()); thisPolygon.setOrigin(this.getOriginX(), this.getOriginY()); thisPolygon.setRotation(this.getRotation()); if (!thisPolygon.getBoundingRectangle().overlaps( otherPolygon.getBoundingRectangle()) ) { return; } Intersector.MinimumTranslationVector mtv = new Intersector.MinimumTranslationVector(); if (Intersector.overlapConvexPolygons( collisionPolygon, obj.getCollisionPolygon(), mtv) ) { if (obj.canBeCollected()) { obj.setVisible(false); } else { this.moveBy( mtv.normal.x * mtv.depth, mtv.normal.y * mtv.depth ); } } } }
Pozostaje nam użyć dwóch powyższych klas w klasie głównej gry. Spójrzmy na kilka najważniejszych metod z tej klasy. Na końcu rozdziału znajdziesz pełny kod źródłowy klasy głównej ActorCollisionExample.
Postać gracza przechowujemy w polu player, a wszystkie pozostałe obiekty świata w tablicy gameObjects:
private PlayerActor player; private StationaryActor[] gameObjects;
W metodzie create wczytujemy tekstury oraz tworzymy obiekty, które będzie widać na ekranie:
@Override public void create () { batch = new SpriteBatch(); cat = new Texture("cat.png"); box = new Texture("wall.png"); diamond = new Texture("diamond.png"); food = new Texture("food.png"); player = new PlayerActor( // 1 cat, 10, 10, getRectangularVertices(cat.getWidth(), cat.getHeight()) ); gameObjects = new StationaryActor[15]; gameObjects[0] = createBox(10, 80); gameObjects[1] = createBox(120, 80); gameObjects[2] = createBox(230, 80); gameObjects[3] = createBox(230, 150); gameObjects[4] = createBox(10, 200); gameObjects[5] = createFood(95, 90); gameObjects[6] = createFood(225, 125); gameObjects[7] = createFood(285, 125); gameObjects[8] = createFood(160, 250); gameObjects[9] = createFood(205, 250); gameObjects[10] = createFood(205, 90); gameObjects[11] = createDiamond(135, 140); gameObjects[12] = createDiamond(30, 140); gameObjects[13] = createDiamond(30, 245); gameObjects[14] = createDiamond(300, 205); }
Tworząc obiekt typu PlayerActor (1), przekazujemy jako ostatni argument tablicę wierzchołków opisującą prostokątny obszar, który będzie używany do wykrywania kolizji pomiędzy graczem a innymi obiektami. Zaraz zobaczymy, jak działa wykorzystywana w tym celu metoda getRectangularVertices.
Obiekty świata gry tworzymy korzystając z trzech pomocniczych metod, które zwracają obiekty typu StationyActor z odpowiednio ustawionymi polami:
private StationaryActor createBox(float x, float y) { return new StationaryActor( box, false, x, y, getRectangularVertices( box.getWidth(), box.getHeight() ) ); } private StationaryActor createDiamond(float x, float y) { return new StationaryActor( diamond, true, x, y, getDiamondVertices( diamond.getWidth(), diamond.getHeight() ) ); } private StationaryActor createFood(float x, float y) { return new StationaryActor( food, true, x, y, getRectangularVertices( food.getWidth(), food.getHeight() ) ); }
Metody te tworzą obiekty typu StationaryActor z odpowiednią teksturą, wartością true/false dla pola canBeCollected, oraz wierzchołkami opisującymi obszar służący do wykrywania kolizji.
Tablice z wierzchołkami zwracają dwie pomocnicze metody getRectangularVertices oraz getDiamonVertices:
private float[] getRectangularVertices(int width, int height) { return new float[] { 0, 0, // lewy dolny wierzcholek width, 0, // prawy dolny wierzcholek width, height, // prawy gorny wierzcholek 0, height // lewy gorny wierzcholek }; } private float[] getDiamondVertices(int width, int height) { return new float[] { width / 2.0f, 0, // środek dolnej krawędzi width, height / 2.0f, // środek prawej krawędzi width / 2.0f, height, // środek górnej krawędzi 0, height / 2.0f // środek lewej krawędzi }; }
Metody te wyznaczają cztery wierzchołki dla obszarów, odpowiednio, prostokątnego oraz w kształcie rombu. Na początku rozdziału prezentowałem, dlaczego te wartości wyznaczane są w taki właśnie sposób.
Rysowanie obiektów i wykrywanie kolizji odbywa się w metodzie render. W pętli rysujemy kolejne obiekty oraz sprawdzamy kolizję pomiędzy graczem a konkretnym obiektem. Na końcu rysujemy postać gracza:
@Override public void render () { Gdx.gl.glClearColor(1, 1, 1, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); player.act(Gdx.graphics.getDeltaTime()); batch.begin(); for (StationaryActor gameObject : gameObjects) { gameObject.draw(batch, 1); player.checkCollision(gameObject); } player.draw(batch, 1); batch.end(); }
Po uruchomieniu przykładu z tego rozdziału, na ekranie zobaczymy:
Zauważ, że postać gracza znajduje się w obrębie prostokąta, w którym znajduje się niebieski romb, ale kolizja nie jest na razie wykryta – dzieje się tak dzięki złożonemu wykrywaniu kolizji, jakie zastosowaliśmy. Dopiero po zetknięciu się gracza z teksturą rombu, wykryta zostanie kolizja, a romb zniknie.
Pełny kod źródłowy klasy głównej jest następujący:
package com.kursjava.gamedev; import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; public class ActorCollisionExample extends ApplicationAdapter { private SpriteBatch batch; private Texture cat; private Texture box; private Texture diamond; private Texture food; private PlayerActor player; private StationaryActor[] gameObjects; @Override public void create () { batch = new SpriteBatch(); cat = new Texture("cat.png"); box = new Texture("wall.png"); diamond = new Texture("diamond.png"); food = new Texture("food.png"); player = new PlayerActor( cat, 10, 10, getRectangularVertices(cat.getWidth(), cat.getHeight()) ); gameObjects = new StationaryActor[15]; gameObjects[0] = createBox(10, 80); gameObjects[1] = createBox(120, 80); gameObjects[2] = createBox(230, 80); gameObjects[3] = createBox(230, 150); gameObjects[4] = createBox(10, 200); gameObjects[5] = createFood(95, 90); gameObjects[6] = createFood(225, 125); gameObjects[7] = createFood(285, 125); gameObjects[8] = createFood(160, 250); gameObjects[9] = createFood(205, 250); gameObjects[10] = createFood(205, 90); gameObjects[11] = createDiamond(135, 140); gameObjects[12] = createDiamond(30, 140); gameObjects[13] = createDiamond(30, 245); gameObjects[14] = createDiamond(300, 205); } @Override public void render () { Gdx.gl.glClearColor(1, 1, 1, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); player.act(Gdx.graphics.getDeltaTime()); batch.begin(); for (StationaryActor gameObject : gameObjects) { gameObject.draw(batch, 1); player.checkCollision(gameObject); } player.draw(batch, 1); batch.end(); } @Override public void dispose () { batch.dispose(); cat.dispose(); box.dispose(); diamond.dispose(); food.dispose(); } private StationaryActor createBox(float x, float y) { return new StationaryActor( box, false, x, y, getRectangularVertices( box.getWidth(), box.getHeight() ) ); } private StationaryActor createDiamond(float x, float y) { return new StationaryActor( diamond, true, x, y, getDiamondVertices( diamond.getWidth(), diamond.getHeight() ) ); } private StationaryActor createFood(float x, float y) { return new StationaryActor( food, true, x, y, getRectangularVertices( food.getWidth(), food.getHeight() ) ); } private float[] getRectangularVertices(int width, int height) { return new float[] { 0, 0, // lewy dolny wierzcholek width, 0, // prawy dolny wierzcholek width, height, // prawy gorny wierzcholek 0, height // lewy gorny wierzcholek }; } private float[] getDiamondVertices(int width, int height) { return new float[] { width / 2.0f, 0, // srodek dolnej krawedzi width, height / 2.0f, // srodek prawej krawedzi width / 2.0f, height, // srodek gornej krawedzi 0, height / 2.0f // srodek lewej krawedzi }; } }