niedziela, 9 kwietnia 2017

Interaktywne obiekty - początki


Ostatnio skończyliśmy na etapie poruszania postacią. Powinniśmy wrócić jeszcze do tego zagadnienia ponieważ ruch kamery odbiega od tego jak byśmy sobie życzyli. Środek kamery przemieszcza się niezgodnie z bohaterem w inny obszar ekranu. Potrzeba na początku znaleźć przyczynę takiego zachowania.

Odpowiedzią okazała się różnica pomiędzy obszarem debugowania bohatera, a jego pozycją rzeczywistą. Wniosek przychodzi z prostego faktu że pobieramy współrzędne bohatera i tam ustalamy pozycję kamery. Wystarczy odpowiednio przeskalować pozycje kamery, aby te obszary się zgadzały


camera.position.x = player.b2dBody.getPosition().x / 1.3f;
camera.position.y = player.b2dBody.getPosition().y / 1.3f;

Czas na wiosenne porządki

W klasie PlayScreen obecnie znajduje się mnóstwo kodu który można trochę lepiej zorganizować, aby uzyskać więcej przejrzystości. Co można zrobić to całe tworzenie world przy użyciu tiledMap przenieść do osobnej klasy. Podobnie jak ma to miejsce w przypadku bohatera BatteryHero, całość definicji znajduje się w osobnej klasie, a my tylko tworzymy obiekt tego typu w klasie PlayScreen.

Tworzymy nowy pod pakiet Tools, a w wewnątrz niego nową klasę B2WorldCreator. Wewnątrz tworzymy pusty konstruktor z dwoma przekazanymi parametrami (World world, TiledMap map).

public B2WorldCreator(World world, TiledMap map) {
}

World jest nam potrzebny aby mieć możliwość mieć dostęp do zarządzania obiektami. TiledMap jest oczywiście wczytaną mapą z pliku na której będziemy operować.
Przechodzimy do klasy PlayScreen i wycinamy wszystko począwszy od linii kodu widocznej niżej, aż do końca pętli w której ustawaliśmy właściwości warstwy podłoża mapy



BodyDef bdef = new BodyDef();

for (MapObject object : map.getLayers().get(2).getObjects().getByType(RectangleMapObject.class)) {

}

Wklejamy to w konstruktor B2WorldCreator, a w klasie PlayScreen w miejscu z którego wycięliśmy kod tworzymy obiekt

new B2WorldCreator(world, map);

Całe te manewry powoduje że cała zawartość klasy B2WorldCreator jest w tym momencie wykonywana. Jednak działanie pozostaje takie same jak przed zmianą z małą różnicą innego uporządkowania kodu. Przy większej ilości kodu lepszy komfort pracy jeżeli to dobrze zorganizujemy będzie jeszcze bardziej odczuwalny.

Zwalnianie zasobów

Ponieważ gry używają całą masę zasobów (obrazki, dzwięki itp.) należy pomyślec nad ich zwalnianiem w momencie gdy nie są potrzebne. Przykładem może być sytuacja gdy minimalizujemy grę, wtedy raczej nie chcemy aby coś ciężkiego wykonywało się dalej w tle. W przypadku małej ilości pamięci ram może niepotrzebnie spowalniać telefon czy inne urządzenie. Pozbywamy się obiektów manualnie w metodzie dispose wskazując kolejno na obiekty do odwołania. W linku można poczytać jakie zasoby wymagają zwalniania. Więc kolejno


@Override
public void dispose() {
    map.dispose();
    renderer.dispose();
    world.dispose();
    b2dr.dispose();
}

Przy czym zostało nam zwolnienie jeszcze hud, ale nie dokonamy tego tak od razu ponieważ nie ma zaimplementowanej wewnątrz odpowiedniej do tego metody. Wszystkie powyższe elementy jako gotowe klocki libGdx do budowania posiadały zaimplementowane metodę dispose(). Nasz hud który sami utworzyliśmy jednak jej nie ma.

Możemy w klasie Hud wrzucić


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

Co spowoduje zniszczenie obiektu stage wraz z wszystkimi jego zasobami.

Interfejs Disposable

Albo można użyć do tego interfejsu. Interfejs zawiera wewnątrz zdefiniowane metody, ale bez danych. Ponieważ metoda dispose jest używana w wielu miejscach w całości biblioteki, twórcy wrzucili ją właśnie jako interfejs który możemy wielokrotnie użyć. Wewnątrz znajduje się jej definicja.
Najpierw usuwamy więc poprzednio utworzoną metodę dispose()
 W klasie Hud po nazwie dopisujemy implements i nazwę interfejsu. Teraz wygląda to tak

public class Hud implements Disposable {

W tym momencie powinno podkreślić nam linię początku klasy co by wskazywało na błąd. Najeżdżając myszą na kod wyświetli chmurkę z podpowiedzią o konieczności implementacji metody dispose(). Klikamy tym razem na linii, tak aby wyświetliła się czerwona żarówka z sugestiami rozwiązania problemu. Wybieramy Implement methods i zaznaczamy metodę dispose.


Pojawia się nadpisana metoda dispose() którą wypełniamy jak poprzednio


stage.dispose();

Teraz w PlayScreen dopisujemy w miejscu zwalniania zasobów dodatkowo


hud.dispose();

Można przetestować, wszystko działa jak poprzednio.

Można również zadać sobie pytanie jaka idea stoi za stosowaniem interfejsów. Ze znalezionych informacji wynika, że tworzy się je na etapie modelowania projektu. Więc ustalasz sobie jak powinny wyglądać używane później metody w konkretnym programie. Dzięki temu w późniejszych fazach jest o wiele łatwiej się poruszać po całości kodu i nie trzeba pamiętać nazw metod.

Inne obiekty

Kolejnym punktem do osiągnięcia jest zrobienie grupy przedmiotów z którymi gracz może przeprowadzać interakcje. Zaczniemy od stworzenia małych tabliczek z wyrazami obcymi, które można zbierać. Późnij dorobimy do tego okno dialogowe gdzie będzie trzeba coś z tym zrobić.






Pora więc dodać kolejną warstwę do mapy Tiled przechodzimy więc do edytora. Dodajemy warstwę Tile Layer oraz Object Layer, obie można nazwać wordnote. I powtarzamy te same kroki jak wcześniej dla ground, niezbędne czynności opisane w obu tych postach link i link2. Dołączam również obrazek który został wykonany na potrzeby projektu w programie Inkscape



Można używać bez ograniczeń, lub jeżeli wolisz znajdź/stwórz coś własnego.

Efekt


Gdy mamy już to gotowe należy dodać ponownie do folderu assets projektu nowo wyedytowany plik untitled.tmx nadpisując stary. Nie należy zapomnieć o także dodaniu nowego pliku graficznego który użyliśmy na mapie tiled.

Możliwe problemy z plikiem tiled po edycji

Jeżeli w tym miejscu po uruchomieniu pokaże wam się wyjątek GdxRuntimeException oznacza to źle wskazane miejsce pliku obrazka. Otwieramy więc unitiled.tmx i edytujemy


Widzimy że źródło obrazka znajduje się gdzieś poza projektem. Zmieniamy na same “note.png”, w tym momencie będzie odwoływać się do tego samego folderu assets w którym jest plik tiled.


<image source="note.png" trans="ff00ff" width="70" height="70"/>

Kolejną ważną rzeczą jest aby przy zapisywaniu pliku tiled mieć w edytorze zahaczone wszystkie warstwy. Inaczej efekt jak poniżej



Iterujemy po raz drugi

Aby uwzględnić dodanie nowych tabliczek na mapie, należałoby ponownie przeiterować pętlą for po odpowiedniej warstwie. Całość znajduje się w klasie B2WorldCreator. Po skopiowaniu wystarczy podmienić jedynie nr tej warstwy. U mnie patrząc w plik untiled.tmx warstwa obiektów wordnote jest warstwą 5. Należy pamiętać ze liczymy od 0, więc prawidłowo odwoływać powinniśmy się do warstwy 5-1 = 4. Po uruchomieniu działa tak jak powinno, następuje kolizja również z notkami.

Abstrakcyjne interaktywne obiekty

Zakładamy że będziemy tworzyć w przyszłości więcej warstw obiektów o podobnym zachowaniu. Jak pamiętamy pierwsza pętla odpowiadała za podłoża które są raczej statyczne. Reszta warstw chcemy raczej aby dawało coś się z nimi zrobić. Mówi się na takie warstwy że są to warstwy interaktywne. Jako że warstwy interaktywne będą posiadać duża część kodu powtarzalną, można wziąć ten fragment i przenieść do osobnej klasy. Dzięki temu nie trzeba tego pisać za każdym razem, a jedynie stworzyć taki obiekt który będzie fragmentem składowym.
Tworzymy więc nową klase w pakiecie sprites i nazywamy ją InteractiveTileObject.


public abstract class InteractiveTileObject {

    private World world;
    private TiledMap map;
    private TiledMapTile tile;
    private Rectangle bounds;
    private Body body;

    public InteractiveTileObject(World world, TiledMap map, Rectangle bounds) {
        this.world = world;
        this.map = map;
        this.bounds = bounds;
    }
}

Nic to nowego po prostu będą to takie same obiekty podobnie jak to było w klasie B2WorldCreator. Można z resztą się temu przyjrzeć. Część z nich zdefiniujemy tutaj, a część zostanie przekazana przez konstruktor. Całą zawartość pętli gdzie wskazaliśmy warstwę wordnote wycinamy i przenosimy do konstruktora InteractiveTileObject. Dodtkowo musimy ponownie utworzyć nowe obiekty


BodyDef bdef = new BodyDef();
PolygonShape shape = new PolygonShape();
FixtureDef fdef = new FixtureDef();

Miejsca gdzie używamy rect zastępujemy bounds przekazanymi przez konstruktor. Tu po prostu inaczej to nazwaliśmy.

Gotowa klasa wygląda tak

public abstract class InteractiveTileObject {

    private World world;
    private TiledMap map;
    private TiledMapTile tile;
    private Rectangle bounds;
    private Body body;

    public InteractiveTileObject(World world, TiledMap map, Rectangle bounds) {
        this.world = world;
        this.map = map;
        this.bounds = bounds;

        BodyDef bdef = new BodyDef();
        PolygonShape shape = new PolygonShape();
        FixtureDef fdef = new FixtureDef();

        bdef.type = BodyDef.BodyType.StaticBody;
        bdef.position.set((bounds.getX() + bounds.getWidth() / 2) / WordCharger.PPM,
                (bounds.getY() + bounds.getHeight() / 2) / WordCharger.PPM);

        body = world.createBody(bdef);

        shape.setAsBox(bounds.getWidth() / 2 / WordCharger.PPM, bounds.getHeight() / 2 / WordCharger.PPM);
        fdef.shape = shape;
        body.createFixture(fdef);
    }

}

Dodając przy nazwie slowo kluczowe abstract, powoduje to że nie może być ona reprezentowana pod postacią obiektów. Ponieważ obiekt który wyświetlamy zawiera znacznie więcej parametrów, nie powinno dać się go wyświetlać. Klasa abstrakcyjna jest jakby częścią innej klasy która może już przyjmować jakąś postać. Inaczej mówi się że jest uogólnieniem pewnej grupy obiektów, co znaczy tyle że zawiera zestaw parametrów które są wspólne dla innych grup.

Klasa WordNote rozszerzająca klasę abstrakcyjną

Stwórzmy teraz obiekt który będzie zawierał właśnie tą klasę abstrakcyjną. Zrobić to chcemy dla tabliczek z słówkami. W pakiecie sprites tworzymy nową klasę WordNote


public class WordNote extends InteractiveTileObject {
    public WordNote(World world, TiledMap map, Rectangle bounds) {
        super(world, map, bounds);
    }
}

Słowem extends mówimy, że cała ta klasa z części składa się z abstrakcyjnej klasy InteractiveTileObject. W konstruktorze przekazane zostały parametry z których będziemy korzystać. Metodą super przekazujemy te parametry dalej, bo tak naprawdę to klasa InteractiveTileObject ich wymagała. To ona jest odpowiedzialna za tą część. Tylko tyle w tej klasie. Widać teraz jak szybko można tworzyć kolejne takie klasy.

Teraz petla powinna być pusta. Pora stworzyć wewnątrz nowy obiekt


new WordNote(world, map, rect);

To wszystko. Testujemy I działa jak poprzednio. Efekt niby ten sam, ale kolejne tworzenie innych obiektów będzie błyskawiczne.

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

Dzisiaj dodatkowo nauczyłem prezentować czytelniej code snippets przy użyciu fajnej stronki http://hilite.me/ efekt zresztą widać, w porównaniu z wpisami wcześniejszymi. Polecam jak będziecie coś pisać.

Na dziś tyle,
Pozdrawiam

Brak komentarzy:

Prześlij komentarz