sobota, 15 kwietnia 2017

Ożywianie bohatera – animacja

Dzisiaj skupimy się na dodaniu animacji do postaci. Animację tworzy się poprzez szybkie zmienianie kolejno następujących po sobie obrazków. Grafika którą posiadamy składa się z 7 mniejszych obrazków. Ludzkie oko łapie około 25 obrazków na sek. Znaczy tyle potrzeba aby cos wydawało się płynne, co daje 0,04 sek na 1 obrazek. Tu jest fajny test który pokaże różnice. My tyle nie mamy aby było to wybitnie płynne, ale to nie szkodzi.
 
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