Stany maszynowe
Na początku skorzystamy z tzw. stanów (State) jest to model
w którym system może znajdować się w jednym z skończonej listy stanów. Każdy kolejny
stan jest uzależniony od poprzedniego, co znaczy tyle że jest ustalona
kolejność jaki stan może wystąpić po jakim.
Stany tak jak mówi dokumentacja
wyróżniają metody które są wykonywane podczas wejścia w stan oraz wyjścia z
niego. Właśnie w ten sposób będzie realizowania animacja, podczas wejścia w
stan będzie następowało odgrywanie klatek przez odpowiednio długi czas, a przy
wyjściu ze stanu będzie następowało zatrzymywane, aby tym razem odegrać coś
innego. Animację można porównać do przycisku play (wejście w stan) i stop (wyjście)
odtwarzacza.
Do mierzenia czasu każdego stanu, aby odpowiednio określać
czas wyświetlania animacji potrzebne będą dwa stany ten obecny i poprzedni.
Dzięki porównaniom obu dowiemy się albo o zmianie akcji albo o kontynuacji
obecnej. Da nam to podstawę do określania czasu wyświetlania odpowiedniej
animacji. Ponieważ użyliśmy impulsu do nadawania prędkości ciału, wiemy również
że trwa on określoną ilość czasu dopóki impuls kończy swoje odziaływanie. Każdy
impuls albo podtrzymuje dany stan, albo powoduje przejście do innego.
public State currentState; public State previousState;
Bohater podczas gry będzie przechodził po różnych stanach. Np. może być wstawiony… nie no kiepski żart. Ponieważ dotyczy to głównie grafiki jaką mamy dostępną to co możemy wyróżnić to: bieganie, skakanie, spadanie i ewentualnie stanie w miejscu. Użyjemy do tego typu wyliczeniowego, dzięki czemu określimy skończony zbiór stanów po jakich możemy się poruszać. Zapis poniżej oznacza tyle że State może być w jednym z poniższych stanów np. State.FALLING. Typ wyliczeniowy oznacza się słowem kluczowym enum.
public enum State {FALLING, JUMPING, STANDING, RUNNING}
Wykorzystamy również klasę Animation, która przechowuje listę obiektów do wyświetlenia w określonej sekwencji. Każdy obiekt animacji zwie się tłumacząc na polski kluczem ramki (key frame), zaś sekwencja kilku takich kluczy tworzy animację. W animacji 2D animacje tworzy się z TextureRegions, co jak pamiętamy jest wyciętym fragmentem z wczytanego pliku całej grafiki. Definiujemy cztery różne animacje, które będziemy widzieć: stanie, bieganie, skakanie i spadanie w dół. Ponieważ chcemy dla każdej tej czynności w inny sposób użyć grafik.
private Animation heroRun; private Animation heroJump; private Animation heroFall; private Animation heroStand;
Do szczęścia potrzeba jeszcze nam zmienną która będzie
przechowywać czas stanu oraz zmienną typu boolean aby określać w którą stronę porusza
się nasz bohater. Wystarczy tylko stwierdzić czy idzie on w prawo, a zaprzeczeniem
będzie kierunek przeciwny i to nam powinno wystarczyć.
private float stateTimer; private boolean runRight;
W konstruktorze musimy przypisać wartości początkowe, aby
mieć jakieś informacje w momencie rozpoczęcia gry
currentState = State.STANDING; previousState = State.STANDING; stateTimer = 0; runRight = true;
Pora na stworzenie animacji dla każdego ze stanów. W grafice
jak widzimy mamy 7 różnych pozycji bohatera. Pierwsze 5 przeznaczymy na poruszanie
się, 6 pozycja będzie wyświetlana w momencie opadania, zaś 7 w momencie skoku. Do
stworzenia animacji potrzeba podać 2 parametry: czas 1 klatki i TextureRegion.
Obiekty Animacji
Aby zrobić to dla biegu potrzebujemy dodać aż 5 klatek. Możemy
to osiągnąć przez dodanie je do tablicy którą nazwiemy frames. Poźniej całą
tablicę przekażemy właśnie jako 2 parametr.
Array<TextureRegion> frames = new Array<TextureRegion>();
<TextureRegion> między kwadratowymi nawiasami oznacza że będzie to tablica złożona z obiektów TextureRegion.
Teraz do tablicy dodajmy 5 elementów robiąc to metodą
.add(). W parametrze podajemy TextureRegion tworząc nowy taki obiekt i ponownie
podając wymagane parametry pozycji każdej takiej klatki. Użyjemy do tego pętli przechodzącej
5 krotnie, za każdym razem zmienimy tylko pozycję punktu startowego na osi x
miejsca gdzie zaczyna wycinać pożądana grafikę. Iterując kolejno i * 100 uzyskamy wartości (100, 200, 300, 400, 500) czyli dokładnie tam gdzie chcemy.
for (int i = 0; i < 4; i++) { frames.add(new TextureRegion(batteryhero.getTexture(), i * 100, 133, 100, 100)); }
Teraz kluczowe przypisane obiektowi poruszania heroRun nowego obiektu animacji wraz z czasem trwania klatki i całą tablicą
heroRun = new Animation(0.1f, frames);
Na koniec wyczyścimy tablicę frame ponieważ nie będziemy już z niej póki co korzystać
frames.clear();
Dla postoju tworzymy animację z 1 klatki podając bezpośrednio
obszar
heroStand = new Animation(0.1f, new TextureRegion(batteryhero.getTexture(), 0, 133, 100, 100));
podobnie dla skoku
heroJump = new Animation(0.1f, new TextureRegion(batteryhero.getTexture(), 6 * 100, 133, 100, 100));
I upadku
heroFall = new Animation(0.1f, new TextureRegion(batteryhero.getTexture(), 5 * 100, 133, 100, 100));
Wykrywanie stanu bohatera
Stwórzmy teraz metodę która będzie wykrywała w jakim stanie
znajduje się aktualnie nasz bohater. Jest to dosyć proste, wykorzystamy do tego
prędkość jaka nadaliśmy przy użyciu klawiszów sterowania. Jak pamiętamy do
sterowania użyliśmy impulsów które nadają obiektowi odpowiednią prędkość. I tak
sprawdzając czy prędkość jest dodatnia czy ujemna można określić w którą stronę
porusza się bohater.
Kierunki
dla x gdy prędkość > 0 oznacza w prawo
dla x gdy prędkość < 0 oznacza w prawo
dla x gdy prędkość = 0 oznacza że stoi w miejscu
dla y gdy prędkość > 0 oznacza w prawo
dla y gdy prędkość < 0 oznacza w prawo
dla y gdy prędkość = 0 oznacza że stoi w miejscu
public State getState() { if (b2dBody.getLinearVelocity().y > 0) { return State.JUMPING; } else if (b2dBody.getLinearVelocity().y < 0) { return State.FALLING; } else if (b2dBody.getLinearVelocity().x != 0) { return State.RUNNING; } else { return State.STANDING; } }
State w pierwszej linii metody określa, że typem zwracanym powinien być typ State. Więc musimy jakoś w tej metodzie to osiągnąć. W ciele widzimy warunki, które w każdym przypadku zwrócą stan, ale z różną wartością wyliczeniową. Prędkość pobieramy z debugowanego obszaru bohatera. W trzecim warunku (b2dBody.getLinearVelocity().x != 0) określamy tylko czy bohater jakkolwiek się przemieszcza na osi x, bez względu czy jest to prawo czy w lewo. Ostatni warunek else oznacza że żaden poprzedni nie nastąpił i w ten sposób określamy że postać nic nie robi, a to też jest stan. Można od teraz śmiało powiedzieć że nic nie robienie to też czynność, więc mamy wytłumaczenie następnym razem ;d.
Wybieranie klatki do
wyświetlenia
Na obecny moment mamy stworzone animacje składające się z
klatek, wiemy co aktualnie robi nasz bohater, wiec co potrzeba nam więcej?
Powinniśmy zacząć w odpowiednim momencie co wyświetlać. Co trzeba wziąć pod
uwagę to częstość odświeżania pętli gry, co mamy w metodzie update podając czas
dt oraz powinniśmy jeszcze wiedzieć którą klatkę aktualnie wyświetlać z
animacji. Będzie to mało istotne w przypadkach gdzie umieściliśmy tylko 1
klatkę, jednak przy poruszaniu mamy ich aż 5.
Ustalanie widocznej tekstury osiągamy metodą setRegion(). Mamy
dostęp do niej ponieważ kolejno dziedziczymy z Sprite, a później TextureRegion,
tam zawarta jest właśnie ta metoda. W metodzie setRegion powinniśmy podać
odpowiedni TextureRegion. Musimy stworzyć więc metodę która za nas będzie wybierała/zwracała
odpowiednią 1 teksturę biorąc pod uwagę wszystkie powyższe założenia.
TextureRegion getFrame(float dt) {
Wewnątrz niej najpierw sprawdzamy obecny stan metodą którą
wcześniej stworzyliśmy
currentState = getState();
Inicjujemy obiekt TextureRegion, który później przypiszemy i
zwrócimy
TextureRegion region;
Tworzymy warunek wielokrotnego wyboru switch. W zależności od
obecnego stanu zostanie wykonany odpowiedni przypadek (case)
switch (currentState) { case JUMPING: region = (TextureRegion) heroJump.getKeyFrame(stateTimer); break; case RUNNING: region = (TextureRegion) heroRun.getKeyFrame(stateTimer, true); break; case FALLING: region = (TextureRegion) heroFall.getKeyFrame(stateTimer); break; case STANDING: default: region = (TextureRegion) heroStand.getKeyFrame(stateTimer); break; }
Każdy przypadek wygląda identycznie. Przypisujemy w nim
odpowiedni region, czyli klucz ramki umieszczony w odpowiedniej animacji. Zostaje
on dodatkowo rzutowany na typ TextureRegion który potrzebujemy uzyskać w
wyniku. Parametrem selekcji odpowiedniej ramki będzie czas stanu, czyli ile
czasu ten stan już trwa (ile wyświetlana jest animacja). Ramki zmieniają się
jak pamiętamy co 0.1f czyli co 0.1 sek.
W przypadku RUNNING mamy dodatkowo 2 parametr boolean który wskazuje
że animacja się zapętla, następuje od nowa po zakończeniu.
Prawo lewo grafiki
Kolejną ważną sprawą jest określenie kierunku poruszania się
bohatera. Z dostępnych grafik możemy uzyskać jedynie ruch w prawo. Jednak w
łatwy sposób jest uzyskać ruch w lewo stosując lustrzane odbicie dla grafik.
Mamy do tego metodę flip() która stosujemy na odpowiednim regionie.
if((b2dBody.getLinearVelocity().x < 0 || !runRight) && !region.isFlipX()) { region.flip(true, false); runRight = false; }
Tak więc sprawdzamy czy prędkość kieruje postać w lewo oraz (operator
|| zwraca true jeżeli co najmniej 1 z argumentów jest true) sprawdzamy czy wartość
boolean ustawiania odpowiednio w ciałach warunków wskazuje kierunek nie w prawo
(czyli w lewo). Dodatkowo sprawdzamy czy obszar region nie jest już odwrócony (&&
zwraca true jeżeli oba są true). Jeżeli jest odwrócony znaczy że powtórnie nie
ma potrzeby. Analogicznie wygląda dla kierunku w prawo. Można jeszcze dodać że pierwszym
parametram flip() true wybieramy odbicie na x.
else if ((b2dBody.getLinearVelocity().x > 0 || runRight) && region.isFlipX()) { region.flip(true, false); runRight = true; }
Czas stanu
pod koniec ustawiamy czas trwania stanu
stateTimer = currentState == previousState ? stateTimer + dt : 0;
to co widzimy mozna analogicznie uzyskać warunkiem if
if (currentState == previousState) { stateTimer = statTimer + dt; } else { stateTimer = 0; }
Jeżeli stan obecny jest równy poprzedniemu to do czasu stanu dodajemy przyrost wynikający z kolejnego odświeżenia pętli. Dzięki temu nie odgrywamy animacji od początku tylko ją kontynuujemy odwołując się do kolejnej klatki animacji. W innym przypadku, gdy stany są różne czas ten resetujemy, przez co dajemy znać, aby animacja była odgrywana od początku. Ma to znaczenie przy stanach z poruszaniem się.
Aby zachować stan poprzedni do porównania zanim metoda znowu
wykona się od początku powinniśmy przechować ją w zmiennej
previousState = currentState;
na końcu metody getFrame zwracamy
return region;
można teraz w metodzie update bezpośrednio wywołać stworzoną metodę, przekazując dt
setRegion(getFrame(dt));
Wszystko powinno działać jak
należy. Nie w sposób pokazać animację. Poniżej podczas skoku klatka z podwiniętymi
nogami, tak jak oczekiwaliśmy.
Tyle na dziś, udało się w zadowalający sposób to opisać.
Pozdrawiam do następnej części.
https://github.com/KrzysztofPawlak/WordCharger/tree/wpis13
Brak komentarzy:
Prześlij komentarz