wtorek, 25 kwietnia 2017

Sterowanie na Androidzie

Dzisiejszy wpis miał być kolejnym planowaniem. Jednak odwleczemy to jeszcze chwilę. Przed snem chciałem trochę pomyśleć co można fajnego dalej zrobić. Akurat wcześniej gdzieś tam przypadkiem wrzucił się fragment tego co zostało zrobione. Same patrzenie w ekran który nie można poruszyć pod dłuższym czasie dosyć szybko się nudzi. Więc czas zmienić ten stan rzeczy i stworzyć sterowanie na telefon.

Poszukiwania kontrolera

Od razu do głowy przyszło gdzieś tam stare wspomnienie że w grze rayman-ie na androidzie było to zrobione w dość fajny sposób. Tu co prawda akurat zdjęcie z innej platformy ale wygląda identycznie.


Zdjęcie stąd: http://www.imore.com/rayman-2-the-great-escape-now-in-app-store

Mamy tu sterowanie za pomocą analoga, coś na zasadzie określania pozycji dotyku palca (pole siwe) względem środka koła czerwonego. Trzeba więc tu przeliczać trochę współrzędnych, wychylenie. Zbyt czasochłonne aby to zrobić, więc rezygnujemy.


Szukając dalej w Internecie, w serwisie youtube w oko wpadła jedna gra. Filmik prezentuje po rosyjsku jakiś ranking gier mobilnych w 2015 roku, albo coś podobnego. Gra znajdziemy na google play pod link. Co szczególnego w tej grze to prostota, która wygląda dość fajnie. A i sterowanie idealne do naszych warunków polowych ;d



Link do tego rosyjskiego rankingu: https://www.youtube.com/watch?v=eoAG9lOPqTk

Klasa kontrolera

Więc zrobimy to w taki sam sposób jak w grze powyżej, przy czym później po prawej stronie nad przyciskiem skoku obsadzimy dodatkowe przyciski/tabliczki z słówkami. Plan więc jest, to do dzieła.

Na początku umieśmy grafiki odpowiedzialne z sterowanie w katalogu assets projektu androidowego.





Teraz stwórzmy klasę która będzie odpowiedzialna za rysowanie strzałek i wychwytywanie zdarzeń po ich kliknięciu. Tworzymy więc w pod pakiecie scenes nową klasę Controller. Klasa ta będzie miała dużo cech wspólnych z klasą Hud, gdzie wyświetlamy pola tekstowe u góry ekranu. Dodajemy elementy które będą potrzebne


Viewport viewport;
Stage stage;
boolean upPressed, leftPressed, rightPressed;
OrthographicCamera cam;

Viewport będzie obszarem zainteresowania który widzimy, stage jest sceną na której występują aktorzy, a w naszym przypadku to figury geometryczne które nasłuchujemy i które są odpowiedzialne za sterowanie. W zmiennych boolean będziemy przechowywać informacje czy akurat trzymamy palce nad danym przyciskiem, zmienna ta będzie mówiła o wysłaniu impulsu kierunku w odpowiednia stronę. Oraz ostatnią rzeczą będzie kamera mówiąca jak mamy patrzeć na ekran. Więcej informacji o tym w znajdziecie w wpisie.

Następnie tworzymy konstruktor wewnątrz którego umieszczamy


public Controller() {
    cam = new OrthographicCamera();
    viewport = new FitViewport(800, 480, cam);
    stage = new Stage(viewport, WordCharger.batch);

Zwykła inicjalizacja. Tu dosyć ważne przy tworzeniu sceny ustalamy gdzie mamy patrzeć oraz w drugim parametrze powinno się umieszczać batch (pojemnik z elementami do wyświetlania), jednak nie nowy, a ten z głównej klasy gry. Musimy więc poczynić małą zmianę, aby móc się do niej odwołać. W klasie WordCharger dodajemy zmiennej batch modyfikator static, dzięki temu określamy że od tego momentu może być tylko jeden taki egzemplarz tej zmiennej oraz istniejący przez cały czas działania programu.


public static SpriteBatch batch;

InputProcessor z nasłuchem

Dalej w konstruktorze ustalamy elementowi stage Input Processor, który jest odpowiedzialny za wyłapywanie wszelkich sygnałów wejściowych (touch screen, myszki, czy ekranu).


Gdx.input.setInputProcessor(stage); 

Kolejnym krokiem jest dodanie grafik zgodnie z kierunkiem sterowania. Zrobimy to dla jednego kierunku. Reszta wygląda analogicznie. Tworzymy więc obiekt Image nadając jakąś nazwę związana z kierunkiem np. controlsRightImage. Dalej w parametrze obiektu tworzymy nowy Texture wraz z nazwą pliku graficznego umieszczonego w folderze assets controlRight.png.


Image controlsRightImage = new Image(new Texture("controlRight.png"));

Ustalamy rozmiar jaki ma przyjąć obrazek


controlsRightImage.setSize(96, 96);

I teraz ważne dodajemy Listener przez stworzenie nowego listenera, otwieramy klamry wewnątrz których chcemy zaimplementować użyteczne nam metody. Listener jest obiektem który nasłuchuje, tzn. sprawdza czy nie pojawia się jakiś sygnał wejściowy.
 

controlsRightImage.addListener(new InputListener() {

}


Metody listenera

Klikając wewnątrz otwartego listenera prawym przyciskiem myszy wybieramy Generate… a następnie Override Methods… Z pośród wszystkich dostępnych wybieramy touchDown i touchUp. Te są akurat odpowiedzialne za dotknięcia, co będziemy wykonywać na ekranie naszego telefonu. Poza tym można zobaczyć że są inne odpowiedzialne za scroolowanie (przewijanie), obsługa myszy, czy klawiatury.

@Override

@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
     rightPressed = true;
     return true;
}

@Override
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
     rightPressed = false;
}

Metoda down jak nazwa mówi wykona się jak paluch będzie nad tym elementem strzałki. Ustawiamy więc wartość boolean rightPressed na true. Metoda jest typu boolean więc taką wartość trzeba zwrócić przez return true. Zresztą środowisko w którym pracujemy nas o tym poinformuje przez podkreślenie błędu. Metoda touchUp jest przeciwieństwem więc trzeba ustawić wartość boolean na false. Sama metoda jest typu void więc nic nie zwraca. Krótko podsumowując tworzymy obiekty obrazków które będą nasłuchiwane/sprawdzane czy nie następuje ich dotknięcie. W zależności od tego ustawiamy odpowiednią zmienną boolean. Ta w późniejszych etapach będzie przekazana do głównej pętli gry i w zależności od wartości tych zmiennych będzie wykonywany impuls w odpowiednia stronę.

Wyświetlanie w table

Teraz czas aby wyświetlić wszystkie strzałki kierunku. Aby zachować pewną organizacją i łatwość ustalania położenia skorzystamy z table (stołu). Już to wcześniej robiliśmy. Obszerna dokumentacja o wszystkich możliwych położeniach jest tutaj.

Więc tak generalnie umieszczamy przycisk lewo i prawo na dole ekranu po lewej stronie. Tworzymy stół ustalając jego pozycję i metodą setFillParent mówimy aby dopasował się wielkości stage.


Table tableLeftRight = new Table();
tableLeftRight.left().bottom();
tableLeftRight.setFillParent(true);

Table tableJump = new Table();
tableJump.right().bottom();
tableJump.setFillParent(true);

Tworzymy 2 stoły już tłumacze dlaczego. Chcemy stworzyć przyciski lewa/prawa po lewej stronie i góra (up) po prawej.
Metodą left() można od razu określić pozycję stołu po lewej stronie. I to by było ok na ten moment. Dodatkowo chcemy lekko odsunąć od lewej krawędzi ekranu, co przy różnych ekranach zawsze było by uwzględniane. Problem pojawia się przy dodaniu przycisku po prawej stronie. Ustalając raz pozycję stołu, zmieniając teraz pozycję na prawo metodą right() przeniosło by to wszystkie elementy na prawo. To nas nie urządza.

Drugą opcją byłoby dodaniu przestrzeni ileś pikseli zaczynając od lewej strony np. przy korzystaniu z metody pad. Tylko to zupełnie nie zdało by testu przy innych ekranach.

Co nam potrzeba to złapać prawą stronę i to od niej odliczyć odległość jaką chcemy od prawej krawędzi ekranu. Te rozwiązanie zda test przy każdych rozdzielczościach, dlatego więc tworzymy 2 niezależne stoły.

Teraz poniżej image które stworzyliśmy do istniejących stołów dodajemy właśnie te obiekty, a następnie całość dodajemy jako aktor do stage. Stage jest sceną z obiektami które będą mogły być wyrysowane.

Dla przycisku lewa i prawa


tableLeftRight.padBottom(10);
tableLeftRight.add().pad(10);
tableLeftRight.add(controlsLeftImage).size(controlsLeftImage.getWidth(), controlsLeftImage.getHeight());
tableLeftRight.add().pad(20);
tableLeftRight.add(controlsRightImage).size(controlsRightImage.getWidth(), controlsRightImage.getHeight());
stage.addActor(tableLeftRight);

dla przycisku skoku


tableJump.padBottom(10);
tableJump.add(controlsUpImage).size(controlsUpImage.getWidth(), controlsUpImage.getHeight());
tableJump.add().pad(10);
stage.addActor(tableJump); 

Warto zwrócić uwagę że przy dodawaniu do stołu dodaliśmy obiekt obrazku wraz z  jego rozmiarem który, określiliśmy przy tworzeniu obiektu.

odległości które poustawialiśmy wyżej


Obsługa z zewnątrz

Potrzebujemy teraz móc wywołać metodę rysowania na obiekcie tego stage z klasy głównej gry, tworzymy więc do tego odpowiednią metodę.


public void draw() {
    stage.draw();
}

Pora użyć zbudowanego kontrolera w grze. W klasie PlayScreen deklarujemy


Controller controller;

W konstruktorze PlayScreen zaś inicjujemy tworząc nowy obiekt Controller


controller = new Controller();

W metodzie render czas na wyświetlenie. Robimy to na końcu, dzięki czemu będzie ostatnie rysowane i będzie przykrywało wszystkie inne obiekty (będzie na wierzchu)


controller.draw();

Warto przy okazji przenieść tutaj na koniec kiedyś dokonane rysowanie hud, co dotychczas było niepoprawnie, ponieważ było niewidoczne zakryte gdzieś pod spodem


hud.stage.draw();

Efekt już widać


Touch, Touch, Touch

Jak można sprawdzić jest wszystko widoczne, ale brak reakcji na jakiekolwiek dotknięcie. Trzeba sobie przypomnieć jak to działało wcześniej dla klawiatury. W metodzie update która wykonywana jest cyklicznie co określony przyrost czasu była wywoływana metoda handleInput(). A w wewnątrz niej sprawdzaliśmy czy jakiś klawisz nie jest wciśnięty. Teraz aby dokonać tego samego dla stworzonego kontrolera trzeba dodać kolejne warunki w handleInput()


if (contoller.isRightPressed() &&  player.b2dBody.getLinearVelocity().x <= 2) {
    player.b2dBody.applyLinearImpulse(new Vector2(2f, 0), player.b2dBody.getWorldCenter(), true);
}
if (contoller.isLeftPressed() && player.b2dBody.getLinearVelocity().x >= -2) {
    player.b2dBody.applyLinearImpulse(new Vector2(-2f, 0), player.b2dBody.getWorldCenter(), true);
}
if (contoller.isUpPressed()) {
    player.b2dBody.applyLinearImpulse(new Vector2(0, 1f), player.b2dBody.getWorldCenter(), true);
}

Nie będzie to omawiane. Najważniejszym punktem jest zmiana pierwszej części warunku z input na sprawdzenie wartości boolean w klasie controller. Musimy więc wydobyć te wartości z klasy kontrolera. Przechodzimy i tworzymy wewnątrz Controllera gettery, czyli metody odpowiedzialne za zwracanie wartości. Getter działa dosyć prosto zwraca wartości zmiennej która jest widoczna w danej klasie. Ponieważ do zmiennych w klasie nie powinno się mieć dostęp w sposób bezpośredni, stosuje się metody które  służą tylko do wyciągania. W tym przypadku daje to pewne zabezpieczenie, ponieważ getter oferuje tylko zwracanie wartości która jest ustawiona, nie może jednak tej wartości z innej klasy zmienić. Chyba że istnieją jeszcze settery. Na ten moment te wyjaśnienie z pewnością wystarczy. Zarówno gettery jak i settery można wygenerować automatyczne, z racji że jest to często używany mechanizm, środowiska programistyczne nam to udogadniają.

Klikamy prawym przyciskiem gdzieś poniżej ciała konstruktora i wybieramy Generate… i następnie Getter. Wybieramy leftPressed, rightPressed i upPressed.


public boolean isLeftPressed() {
    return leftPressed;
}

public boolean isRightPressed() {
    return rightPressed;
}

public boolean isUpPressed() {
    return upPressed;
}

Mamy więc gettery z których możemy skorzystać jako metody klasy Controller. Przechodzimy z powrotem do warunku odpowiedzialnego w metodzie handleInput()
I wywołujemy na obiekcie kontroler właśnie te metody contoller.isRightPressed(). Metoda zwróci nam odpowiednią wartość boolean zmianianą przez listener obrazka strzałki sterowania  i jeżeli któraś z nich będzie będzie true to zostanie wykonany impuls.

Po przetestowaniu dodatkowo został zmieniony wektor siły przy wciśnięciu UpPressed na newVector (0, 1f), jest bardziej realistyczny i nie wywalający od razu w górę.

Tyle, Efekt jak wyżej na rysunku, tylko można dodatkowo sterować.

Pozdrawiam

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

piątek, 21 kwietnia 2017

Zbijamy notki

Po tym jak mamy już możliwość wykrywania konkretnych kolizji, czas dodać pierwsze zachowania jakie będą po tym następowały. Gdzieś tam na początku pisałem o planie, aby po zebraniu tabliczki z słowem pojawił się jakiś dialog/komunikat z możliwością sprawdzenia, zapamiętania bądź odgadnięcia słówek. Pytaniem jest czy nie zabija to w jakiś sposób dynamiki rozgrywki. Jest to ten moment gdzie trzeba zatrzymać się, trochę pokminić i sprecyzować logikę gry. Następny wpis właśnie temu będzie poświęcony. Póki co zrobimy aby obiekt kolizji znikał, z resztą i tak będzie to elementem pośrednim tego łańcucha zdarzeń.

Wizja

Najpierw może odgórna wizja jak to powinno wyglądać. Ponieważ jest wiele obiektów z którymi kolidujemy powinniśmy, jakoś rozróżniać czy obiekt ma podlegać kolizji czy nie. Osiągniemy to tworząc kategorię które mogą między sobą kolidować w zależności jak sobie to zdefiniujemy. Po co ? Weźmy sytuację gdzie nastąpiła kolizja i jakiś element powinien zniknąć, a co za tym idzie nie powinno więcej tych kolizji już występować. Wczytując mapę mamy całą warstwę obiektów, co więcej ustawiamy że bohater się z nią zderza. Obiekty z którymi się zderzyliśmy powinniśmy jakoś naznaczać, aby nie było powtórnie z nimi kolizji. To co się robi w praktyce w tym momencie to zmienia kategorię danego obiektu. Np. na taką z którą nie możemy kolidować. A teraz wszystko powoli.

Kategorie filtra

Tworzymy najpierw zmienne finalne które będą dzieliły na kategorie. Tworzymy więc w klasie głównej gry WordCharger


public static final short DEFAULT_BIT = 1;
public static final short BATTERY_HERO_BIT = 2;
public static final short WORDNOTE_BIT = 4;
public static final short DESTROYED_BIT = 8

Kategorie w swojej budowie wymaga podania wartości bitowej, typu zmiennego short, którego zakres pozwala nam osiągnąć przedział [-32768, 32767]. Wielkość typu short jest ustalona na 2 bajty. Przeliczając wielkość bajta na bity, jednemu bajtu przypada 8 bitów, więc z 2 bajtów robi się tym sposobem 16 możliwych bitów. Każdy taki bit to odrębna kategoria którą możemy stworzyć. Ponieważ każdy bit jest przesunięty w ciągu w lewo, zamieniając to na wartość dziesiętną uzyskujemy właśnie te wartości widziane powyżej.

0000000000000001
0000000000000010
0000000000000100
0000000000001000

uzyskanie kolejnych wartości dziesiętnej jest możliwe dzięki potęgowaniu dwójki do kolejnych potęg z zakresu (1-16). Przy czym pierwsza wartość będzie równa 1, a ostatnia 65536. Daje nam to pokrycie całego zakresu zaczynając od przedziału ujemnego.

Ustawianie kategorii i masek

Każdemu obiektowi teraz powinniśmy poustawiać kategorie. Potrzebujemy więc na początku ustawiać filtry dla fixture bohatera. Najpierw przypiszemy fixture bohaterowi kategorię wcześniej zdefiniowaną. Ważna kolejność, dlatego koniecznie filter należy umieścić po definicji fixture. Więc w metodzie defineBatteryHero dopisujemy


fixtureDef.filter.categoryBits = WordCharger.BATTERY_HERO_BIT;

Drugą sprawą jest ustawić maski. Maski służą ustalaniu z jakimi kategoriami może kolidować dany fixture.


fixtureDef.filter.maskBits = WordCharger.DEFAULT_BIT | WordCharger.WORDNOTE_BIT;

w ten sposób określamy że nasz bohater, a dokładnie jego fixture może kolidować z kategorią normalną oraz kategorią oznaczoną WORDNOTE_BIT.
 
Teraz w klasie InteractiveTileObject tworzymy metodę ustawiającą kategorię filtra. Robimy to w tym miejscu ponieważ będzie to metoda uniwersalna dla klas dziedziczącej po niej. Metoda w parametrze powinna przyjmować wybrana kategorie którą chcemy przypisać.


public void setCategoryFilter(short filterBit) { // TODO
    Filter filter = new Filter();
    filter.categoryBits = filterBit;
    fixture.setFilterData(filter);
}

Wewnątrz tworzymy nowy filter, oraz od razu temu obiektowi przypisujemy to co zostało przesłane (filterBit) przy wywołaniu tej metody. Bierzemy w następnym kroku fixture, do którego mamy dostęp jako że jest to pole obowiązujące w całej klasie i ustawiamy stworzony filter metodą setFilterData. setFilterData jest jedną z zdefiniowanych odgórnie metod którą posiada ten „klocek libGDX” Filter.

Zmienne nastroje notki

Pora przejść do klasy WordNote i tu osobno już sprecyzować jakiej dokładnie kategorii chcemy ten obiekt przypisać.


setCategoryFilter(WordCharger.WORDNOTE_BIT);

od tego momentu od utworzenia WordNote będzie właśnie tej kategorii. My chcemy zmienić tą kategorię podczas kolizji. Dlatego w metodzie onHit() dopisujemy


setCategoryFilter(WordCharger.DESTROYED_BIT);

odpalając w tym momencie uzyskamy następujący rezultat
 


Co widzimy? Nastąpiła kolizja z WordNote. Liczy się tylko jeden z komunikatów, drugi dla testów był akurat wynikiem kolizji z tą notką wyżej.  Jak widać postać może wejść w pole gdzie znajduje się obiekt. Brak tu kolizji. Jest to właśnie efekt działania filtra. Po kolizji zmieniliśmy kategorię filtra WordNote na DESTROYED_BIT, a wracając wcześniej do kodu gdzie ustalaliśmy maski dla fixture bohatera określiliśmy aby kolizja mogła występować tylko dla kategorii z etykietką DEFAULT_BIT oraz WORDNOTE_BIT. Dla przypomnienia powtórnie odpowiedzialna ta sama linia kodu, aby nie szukać


fixtureDef.filter.maskBits = WordCharger.DEFAULT_BIT | WordCharger.WORDNOTE_BIT;

Jesteśmy już w stanie wyłączać kolizję pomiędzy obiektami. Jednak co dalej będziemy chcieli to aby po kolizji obiekt zniknął.

Wyciąganie pojedynczej komórki

Przechodząc do samej tabliczki wiemy że jest ona częścią całej zdefiniowanej jednej warstwy z pliku mapy TiledMap. My jednak nie chcemy niszczyć całej warstwy, a poszczególne elementy. Należy powiedzieć w tym miejscu, że cała mapa jest złożona z pojedynczych komórek, tak przecież tworzyliśmy ją w edytorze stawiając „stemple” kafelek. Wczytując mapę do obiektu TiledMap nie utraciliśmy możliwości odwoływania się do poszczególnych kafelek. Podczas kontaktu w trakcie kolizji wiemy właściwie jaka jest dokładna pozycja body elementów biorących w nich udział. Dzięki czemu możemy odpowiednio przeliczyć i określić w ten sposób, która to kafelka. Tworzymy więc metodę zwracającą konkretną kafelke


public TiledMapTileLayer.Cell getCell() {
        
}
       

Wewnątrz na początku odwołujemy się do warstwy, w naszym przypadku jest to warstwa 4, licząc od 0 będzie 3;


TiledMapTileLayer layer = (TiledMapTileLayer) map.getLayers().get(3);

Biorąc pozycję x i dzieląc ją na długość kafelki uzyskamy, która to jest kafelka. Dodatkowo należy jeszcze uwzględnić skalowanie które kiedyś zrobiliśmy. Należy cały ten proces odwrócić. Jest to nic innego jak odwrócenie ułamka 70 / WordCharger.PPM. Dodatkowo rzutujemy na typ (int), kafelki są całymi liczbami, a nie typem zmiennoprzecinkowym float. Całość zwracamy jako efekt wywołania całej metody przy pomocy słowa kluczowego return.


return layer.getCell((int) (body.getPosition().x * WordCharger.PPM / 70),
                (int)(body.getPosition().y * WordCharger.PPM / 70));

Wstawianie pustego kafla

Czas skorzystać z dobrodziejstwa które udało się stworzyć. Przechodząc do klasy WordNote w metodzie onHit dopisujemy


getCell().setTile(null); 

Odpalając grę i kolidując z notką efekt następujący



Z notatki pozostał tylko no cóż … obszar debugownia. Co mówi że obiekt istnieje ale brak kolizji i ustawiliśmy tile na null. Muszę się przyznać że już fajnie popykać pozbijając trochę notek ;d 

Tyle na dziś

Pozdrawiam

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

środa, 19 kwietnia 2017

Na sygnale wraz z kolizją

Jak pamiętamy nad głową bohatera powinna być umieszczona bateria wraz ze wskaźnikiem naładowania. Mimo że koncepcja może ulec zmianie, najpierw ją wykonajmy i oceńmy czy ma sens istnienia w takiej formie. Zadanie dość łatwe ponieważ większość będzie podobnie jak wcześniej, wiec do dzieła.

Na początku tworzymy w pakiecie sprites nową klasę Battery


public class Battery extends Sprite {

wewnątrz potrzebne będą 2 zmiennie: regionu do wyświetlenia, oraz zmiennej w której przechowamy atlas


private TextureRegion batterylevel;
private TextureAtlas atlas;

w konstruktorze przypiszemy obiektowi klasy ten przekazany, oraz ustawimy rozmiar tekstury, który będzie niezmienny dlatego robimy to od razu tutaj.


public Battery(TextureAtlas at) {
    this.atlas = at;
    setBounds(0, 0, 100 / WordCharger.PPM, 50 / WordCharger.PPM);
}

Wyświetlanie baterii

Następnie robimy resztę czynności które potrzebne są do wyświetlenia elementu: są to odnalezienie odpowiedniego rejonu, oraz ustawienie go jako ten do wyświetlenia. Każdy kolor baterii będziemy ustawiać za pomocą jednej z 4 metod. Robimy więc analogiczne kroki dla 4 pozostałych kolorów wskazując odpowiednie miejsca. Współrzędne można sprawdzić w pliku battery_enemy.pack w katalogu assets projektu androidowego.


public void setOrange() {
    batterylevel = atlas.findRegion("red");
    batterylevel = new TextureRegion(batterylevel.getTexture(), 0, 0, 128, 64);
    setRegion(batterylevel);
}

Aktualizacja pozycji baterii

Do szczęścia potrzebujemy jeszcze aktualizować pozycję baterii w miejscu aby znajdowała się nad jego głową. W tym celu w metodzie przekażemy cały obiekt bohatera z którego następnie wyciągniemy potrzebne nam informacje. Współrzędne wyciągamy kolejno z b2dBody bohatera metodą getPosition(). Obiekt umieścimy wyżej o całą wysokość sprita getHeight() od miejsca górnego krańca miejsca y bohatera. Akurat taka wysokość jest odpowiadająca


public void update(BatteryHero hero) {
    setPosition(hero.b2dBody.getPosition().x / 0.7f - getWidth() / 2, hero.b2dBody.getPosition().y / 0.7f + getHeight());
}

Teraz przechodząc do PlayScreen najpierw inicjalizujemy obiekt


private Battery battery;

W konstruktorze tworzymy obiekt i przekazujemy mu wymagany atlas, oraz wstępnie ustalamy przykładowy kolor na pomarańczowy


battery = new Battery(atlas);
battery.setOrange();

Na razie zmiana kolorów ustalona jest na sztywno, w późniejszych etapach gdy będziemy mogli już cos zbierać stworzymy odpowiednie warunki do zmiany koloru.

Obecnie bateria będzie pozostawała jeszcze w miejscu, należy w metodzie update wywołać metodę aktualizującą, wraz z obiektem player z którego wyciągniemy aktualne współrzędne


battery.update(player);

Osiągnięty efekt podążająca nad głową bateria


Kolizja

Obecnie wiemy że następuje jakaś interakcja między obiektami (platformy, notatki) a bohaterem, jest to oczywiste ponieważ nie możemy w tych miejscach stanąć. Teraz kolejnym elementem który chcemy zrealizować jest zbieranie karteczek. Do tego wymagana będzie informacja, kiedy jeden obiekt spotyka na swojej drodze drugi tzw. następuje kolizja. Po identyfikacji co z czym koliduje, będziemy mogli przypisać tym obiektom zachowania jakie chcemy osiągnąć np. spowodujemy aby karteczka znikła. Elementy które dodaliśmy do world mogą zacząć być sprawdzane czy występuje między nimi kolizja. Działa się to na prostej zasadzie sprawdzania czy tzw. Fixture tych obiektów się dotykają.

Najpierw na fixture obu elementów tzn. bohaterowi oraz Wordnote używamy metod dzięki którym będziemy mogli później do tych obiektów odwołać się przy identyfikacji.

W konstruktorze Wordnote wskazując na ten obiekt


fixture.setUserData(this); 

Fixture ale dla klasy dziedziczącej

jest tu pewna poważna rzecz, którą potrzeba uwzględnić

ponieważ obiekt WordNote dziedziczy InteractiveTileObject, a chcemy ustawić metodę setUserData konkretnym klasom, musimy dać możliwość odwołania się do Fixture klasie nadrzędnej. Więc najpierw musimy stworzyć konkretny obiekt i przypisać mu tak stworzone fixture. Modyfikator proctected umożliwi odwołanie się do tego obiektu klasom dziedziczącym. Więc w klasie InteractiveTileObject tworzymy


protected Fixture fixture;

a w konstruktorze przypisujemy wcześniej stworzony już obiekt do fixture


fixture = body.createFixture(fdef);

Jak pamiętamy fixture tworzymy w klasie InteractiveTileObject, co jest powodem że jeżeli w tym miejscu ustawilibyśmy etykietę setUserData, to brakowało by rozróżniania z jakim obiektem mamy do czynienia.

Ustalając w klasie wyżej WordNote tą metodą spowodujemy, że będziemy mogli identyfikować konkretne obiekty.

Teraz w BatteryHero przy tworzeniu Fixture dopisujemy metodę setUserData wraz z etykietką „hero". Hero będzie podstawą naszego warunku, ponieważ to go właśnie chcemy identyfikować czy koliduje z innymi obiektami na mapie.


b2dBody.createFixture(fixtureDef).setUserData("hero");

ustawiamy również parametr isSensor dzięki czemu zbieramy informacje o kontakcie, ale nie generujemy odpowiedzi kolizji.


fixtureDef.isSensor = true;

kolejną ważną rzeczą jest stworzenie abstrakcyjnej metody w InteractiveTileObject. Będzie to metoda która będzie wywoływana przy kontakcie bohatera z warstwą interaktywną (platformy, notatki). Przy czy jest abstrakcyjna, ponieważ definicje jej zrobimy osobno każdemu obiektowi, odpowiednio do zachowania jakie chcemy uzyskać.


public abstract void onHit();

W klasie WordNote natomiast nadpisujemy tą metodę w treści logując o następującej kolizji


@Override
    public void onHit() {
        Gdx.app.log("WordNote", "Collision");
    }

ContactListener

Teraz pora na identyfikacje kontaktów między obiektami. Tworzymy w pakiecie Tools nową klasę WorldContactListener implementującą interfejs ContactListener. ContactListener zawiera metody które są realizowane w momencie kontaktu.


public class WorldContactListener implements ContactListener {

Interfejs ContactListener będzie od nas wymagał, aby zaimplementować wewnątrz 4 metody. Są to odpowiednio:
beginContact – kiedy rozpoczyna się dotyk dwóch fixture
endContact – kiedy kończą
postSolve – zachowania przy rozwiązywaniu wyjścia z kolizji
preSolve - zachowania przy rozwiązywaniu wyjścia z kolizji

Zajmiemy się w głównej mierze beginContact

Najpierw wyciągamy oba fixture które biorą udział w kolizji


Fixture fixA = contact.getFixtureA();
Fixture fixB = contact.getFixtureB();

Identyfikacja obiektów

Sprawdzamy czy chociaż jeden z nich to “hero”


if(fixA.getUserData() == "hero" || fixB.getUserData() == "hero") {

Sprawdzamy które Fixture jest dokładnie czym. Fixture hero przypisujemy fixA lub fixB w zależności czy fixA.getUserData() == "hero". Wystarczy sprawdzenie jednego warunku, ponieważ są tylko 2 opcje.


Fixture hero = fixA.getUserData() == "hero" ? fixA : fixB;

Jeżeli hero == fixA, to obiektem jest fixB, jeżeli nie to automatycznie obiektem jest fixA


Fixture object = hero == fixA ? fixB : fixA;

Wywołanie metody klasy interaktywnej

Teraz pora na sprawdzenie czy obiekt jest klasy interaktywnej i wywołanie na nim metody.

Sprawdzamy czy obiekt nie jest pusty i czy jest klasy InteractiveTileObject. Dokładniej to sprawdzamy metodą isAssignableFrom() czy rozróżniony obiekt drugi poza bohaterem object.getUserData().getClass()) jest oznaczony InteractiveTileObject.class. Może to wystąpić jedynie w przypadku gdy dziedziczy właśnie z tej klasy


if(object.getUserData() != null && InteractiveTileObject.class.isAssignableFrom(object.getUserData().getClass())) {

    }
}

Wewnątrz warunku rzutując object na InteractiveTileObject możemy wywołać abstrakcyjną metodę onHit(). A ta w zależności od implementacji wykona metodę zdefiniowaną bezpośrednio w klasie WordNote.


((InteractiveTileObject) object.getUserData()).onHit();

Wszystko co dzisiaj zrobiliśmy było tylko dla jednego widocznego komunikatu w konsoli. Ale co tam, było warto ;d


Tyle na dziś,
Pozdrawiam

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

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