środa, 29 marca 2017

Początki fizyki Box2D

Powoli nadchodzi czas na bohatera który wkroczy na scenę i będzie się poruszał po poprzednio stworzonej mapie. Bohater jak i wszystkie elementy powinny podlegać określonym zasadom w momencie gdy następuje interakcja, spotkanie kilku elementów czy oddziaływanie między nimi. Znane jest pod hasłem fizyka gry. Mówiąc o tym ma się na myśli wszelkie grawitacje, kolizje, kształty obiektów czy charakterystyczne zachowania obiektów (np. odbicia, obroty). Cały taki mechanizm do wykorzystania możemy znaleźć w bibliotece pod nazwą Box2D. Jest to najbardziej popularną tego typu biblioteka i jest dostępna na wiele języków programowania. Działanie można porównać do pudełka do którego wrzucamy obiekty i od tego momentu wykonywane są na nich kalkulacje, przeliczenia do momentu aż zostaną uśpione bądź zniszczone.

Najpierw deklarujemy obiekt World. Jest to klasa z wyżej wspomnianej biblioteki box2d. Pozwala zarządzać wszystkimi fizycznymi istotami, dynamiczna symulacją i asynchronicznymi zapytaniami (query). Asynchroniczne znaczy dziejące się poza głównym wątkiem, czyli toczącą się gra. Czasami może się pojawiać sytuacja że coś trwa zbyt długo, a program wykonuje się sekwencyjnie. Spowodowałoby to zatrzymanie akcji, co jest niedopuszczalne. Asynchroniczny wątek jest niezależny dziejący się poza wątkiem głównym, nie zabiera mocy obliczeniowej przeznaczonej na akcje gry. Gdy się zakończy informuje o wyniku wątek główny. Drugim elementem koniecznym do zadeklarowania jest Box2DDebugRenderer który jest graficzną reprezentacją tektur i ciał w użytym pudełku box2d.

private World world;
private Box2DDebugRenderer b2dr;

Teraz tworzymy nowy obiekt dla world używając konstruktora w którym podajemy wektor grawitacji i stan uśpienia. Wektor grawitacji składa się z współrzędnych x i y. Wektor jest obiektem składającym się z modułu (długości), punktu zaczepienia oraz kierunku wraz z zwrotem. Dobrze obrazuje to po raz kolejny wikipedia ;d. U nas wektor będzie wskazywać w którą stronę działa grawitacja.


W konstruktorze PlayScreen dopisujemy

world = new World(new Vector2(0, 0), true);
b2dr = new Box2DDebugRenderer();

Podając w parametrach wektora (0, 0) spowodujemy tymczasowy brak grawitacji. Stan uśpienia world ustawiamy na true, dzięki czemu nie będą wykonywane na nim obliczenia. Obiekt będzie statyczny przez co obliczenia są zbędne oraz nie są wymagane by były uwzględniane przy fizyce. Tworzymy również nowy obiekt Box2DDebugRenderer. Dzięki czemu możemy wyświetlać obiekty Box2D wraz z ramką debugowania (pomocnicza zielona otoczka wokół obiektu).

Osadzanie w obiektach fizyki

Wszystkie obiekty które chcemy aby podlegały fizyce gry, potrzeba osadzić w odpowiednich przygotowanych do tego obiektach. To one umożliwią odpowiednie ich przetwarzanie. Teraz konkretnie do naszego zastosowania obiekty które chcemy objąć fizyką to m.in.: platformy, postacie, przeszkody i inne elementy które w przyszłości dołożymy.

Teraz wykonujemy w tym kierunku wymagane czynności. Tworzymy nowy obiekt BodyDef, który zawiera wszystkie dane do stworzenia stałego korpusu. Później do utworzonego ciała dodaje się kształt. Kolejnym krokiem jest utworzenie FixtureDef który jest pewnym znanym stanem obiektu, który może być kontynuowany (kształt, gęstość, tarcie itp.). Stan taki powinien być możliwy do powtórnego odtworzenia. Później przyjrzymy się bardziej tym pojęciom w praktyce, aby zdobyć trochę wyczucia czym one dokładnie są. Na końcu tworzymy również body, które są ciałami posiadającymi wiele fixtures (stanów), mogącymi być z różną orientacją i zmienną pozycją wewnątrz tego ciała.

W konstruktorze PlayScreen

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

Pętla po warstwach

Podstawową pętle zakładam że znasz. Tu jednak skorzystamy z petli for each. Sam zaznaczam że mniej ją stosuję głownie z przyzwyczajenia, dlatego jest mniej intuicyjna dla mnie. Pętla for służy przede wszystkim do sekwencyjnego przeglądania zbiorów. Konstrukcja z grubsza wygląda tak

for(Typ nazwa_obiekt : nazwa_tablica){
     // mięcho ;d
}

Teraz analogia do obiektów z mapy

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

Jako typ wraz z nazwą obiektu używamy MapObject, poszczególne elementy z całego obiektu jakim jest mapa właśnie taką formę przyjmą. Po dwukropku potrzebna nam jest kolekcja z jakiej będziemy te elementy wyciągać. Chcemy różnym warstwom ustawić różna logikę, jak pamiętamy każda warstwa była czymś innym (platformy, background). Jako kolekcje do przejścia po jej wszystkich elementach wyciągniemy tylko jedną warstwę. I dla każdej warstwy osobno zdefiniujemy logikę nową pętlą for each. 

map.getLayers().get(2).getObjects().getByType(RectangleMapObject.class)

Rozwińmy co tu właściwie po kolei wyciągamy. Z mapy bierzemy wszystkie warstwy, get(liczba) odnosimy się do konkretnej warstwy, bierzemy wszystkie obiekty które zawiera, oraz zawężamy do typu obiektu RectangleMapObject. Jak pamiętamy w tiled była to warstwa oznaczona kolorem fioletowym z ikoną różnych figur geometrycznych i była obiektową co umożliwia podpięcie logiki do niej.

Skąd wiadomo że jest to akurat warstwa 2?
Jednym z podejść jest spojrzeć w katalog assets naszego projektu w plik mapy .tmx i sprawdzić która z kolei jest warstwa obiektowa którą potrzebujemy.

Wewnątrz pętli

Tworzymy obiekt klasy Rectangle (prostokąt). I tu taki trochę trik. Obiekt object jest typu MapObject, a potrzebujemy później operować na obiekcie klasy Rectangle (wymaga tego libGDX). Trzeba to odpowiednio przekształcić, co nazywa się rzutowaniem. Rzutujemy znaczy, że konstrukcja zaczyna być traktowana jakby była właśnie typu na który ją rzutowaliśmy. Rzutujemy więc na RectangleMapObject i od teraz object jest tak traktowany. Możemy więc skorzystać z metody którą posiada getRectangle(), co w efekcie da nam zgodność z tym co oczekiwaliśmy (Rectangle).

Rectangle rect = ((RectangleMapObject) object).getRectangle();

Ustawiamy obiektowi bodyDef typ static. Cechą ich jest brak możliwości poruszania i działania na nich różnych sił. Jest idealnym wyborem dla platform, podłoża, ścian. Mniej jest przy tym typie wymaganej mocy obliczeniowej. Są jeszcze 2 możliwości: kinematic i dynamic. Obiekt dynamiczny może się poruszać, działają na niego siły oraz inne dynamiczne, statyczne i kinetyczne obiekty. Na kinematyczne ciała nie działają żadne siły ale mogą za to się poruszać.
  
bdef.type = BodyDef.BodyType.StaticBody;

Kolejną rzeczą jest ustawić odpowiednio pozycję. Bierzemy do tego punkty x, y prostokąt oraz dodajemy jeszcze połowę jego szerokości i wysokości aby punkt zaczepienia znajdował się w środku figury.

bdef.position.set(rect.getX() + rect.getWidth() / 2,
                  rect.getY() + rect.getHeight() / 2);

Następna linia odpowiada na utworzenie na podstawie powyższej wykonanej definicji bdef obiektu body w zbiorze obiektów world, którym jak było wcześniej wspomniane można dowolnie sobie zarządzać.
           
body = world.createBody(bdef);

Tutaj ustalamy kształt obiektu shape, w momencie utworzenia wskazaliśmy że jest typem PolygonShape (wielokąt). Metodą setAsBox doprecyzowuje że jest kwadratem. W parametrach podajemy ponownie środek szerokości i wysokości prostokąta kafelka naszej mapy (rect) podobnie jak przy ustalaniu pozycji.
            
shape.setAsBox(rect.getWidth() / 2, rect.getHeight() / 2);

Właściwie dlaczego tylko połowa ? Zastanówmy się. Załóżmy że chcemy sprawdzić czy 2 elementy się nachodzą w efekcie kolizji. Wymagałoby to sprawdzenia wszystkich kombinacji boków które mogą się nachodzić. Trochę liczenia, a przy dużej ilości obiektów to już jest różnica. Znacznie prościej sprawdzić odległości między środkami. I tak właśnie robi się w tego typu grach.

fdef.shape = shape;

Dalej ustalamy jeden z parametrów obiektowi który miał przechowywać stan. Ustalamy konkretnie kształt na ten kryjący się pod obiektem shape. shape jest tym co ustaliliśmy na kwadrat wraz z szerokością i wysokością.
Inaczej tłumacząc mamy większy obiekt z wieloma parametrami i zapewne metodami. I jeden z tych parametrów w tym przypadku shape nie jest w postaci prostych liczb, tylko jest innym obiektem, który gdzieś tam te wartości wewnątrz posiada.

Czy jest to potrzebne?
Domyślam się dlaczego, chodź może jest inny powód. Spójrzmy na to od strony autorów libGDX. Mamy obiekt klasy fdef który mówi tylko o stanach, gdzie jednym z parametrów jest właśnie kształt. Nie mogli przewidzieć o jaki kształt nam dokładnie chodzi, co będziemy używać, mamy zupełną dowolność, może to być nawet gwiazdka. Gdyby tak nawet było to od klasy fdef wymagałoby to całą masę konstruktorów dla różnych kształtów jakie mogą być przyjmowane w parametrach. Zupełny nonsens.
                       
body.createFixture(fdef);

W końcu tworzymy ciału (body) wcześniej zdefiniowane stany (fdef). Na obecny stan będzie to kształt prostokąta.

Podsumowanie

Podsumowując stworzyliśmy obiekt body który będzie podlegał fizyce gry, który jest typem statycznym i ma kształt kwadratu wielkości kafelka. Tyle tego, a efekt można zapisać w jednym zdaniu ;d

W celu wytestowania w render dopisujemy

b2dr.render(world, camera.combined);

wszystkie elementy powyższej linii pojawiły sie już wcześniej przy innych rzeczach. Używamy metody odpowiedzialnej za renderowanie, podając w parametrach zbiór obiektów i typ kamery.

Efekt poniżej. Z tym że mamy tu pewien problem. Mianowicie jest różnica między obszarem wyświetlanej warstwy, a obszarem debugowania czyli tym który jest ujęty w fizykę. To jednak rozwiążemy następnym razem.



Na dziś tyle. Pozdrawiam.

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

niedziela, 26 marca 2017

Wyświetlamy mapę w oknie gry

Aby móc podpiąć logikę do podłoża i platform potrzeba teraz stworzyć dodatkową warstwę tym razem Object Layer. Po wyświetleniu interesującej nas warstwy w okienku warstw zaznaczamy stworzoną nową warstwę obiektową, z paska narzędzi wybieramy Wstaw Prostokąt (bądź wciskając przycisk R) i zaznaczamy wszystkie elementy na mapie które chcemy uwzględnić. Przytrzymując przy zaznaczaniu przycisk Ctrl, automatycznie będzie nam dopasowywać ramkę zaznaczania do wielkości całych kafelek. Efekt powinien wyglądać jak poniżej. Być może nie wygląda jeszcze piorunująco, ale na ten moment chcemy skupić się na podpięciu wszystkich elementów które chcemy użyć, a dopiero później można się zająć estetyką.


Wczytujemy mapę do projektu android

Na początku zadeklarujemy 3 nowe elementy niezbędne do wczytania i wyświetlenia mapy na ekranie naszej gry. Umieszczamy je na początku klasy PlayScreen.

private TmxMapLoader maploader;
private TiledMap map;
private OrthogonalTiledMapRenderer renderer;

Pierwszy element jest klasa która posłuży do wczytania mapy z pliku .tmx. Klasa TiledMap będzie służyć do przechowywania map. Zaś OrthogonalTiledMapRenderer za zadanie ma wyświetlać  mapę, bądź poszczególne warstwy na ekranie.

Następnie w konstruktorze poniżej miejsca gdzie stworzyliśmy nowy HUD umieszczamy

maploader = new TmxMapLoader();
map = maploader.load("untitled.tmx");
renderer = new OrthogonalTiledMapRenderer(map);

To co tu wykonujemy to tworzymy nowy obiekt klasy TmxMapLoader() i przypisujemy w wcześniej zarezerwowane miejsce. Później do zmiennej map wczytujemy mapę z pliku który wcześniej stworzyliśmy. Mapę untitled.tmx należy przenieść do folderu assets w drzewku projektu android. W tym samym miejscu gdzie znajduje się badlogic.jpg. Wykonuje się to w prosty sposób w miejscu na dysku gdzie znajduje się plik .tmx klikamy go ppm (prawy przycisk myszy) i wybieramy kopiuj, a następnie w drzewku projektu w android studio na folderze assets ppm i wybieramy paste. Zmiennej renderer  przypisujemy nowy obiekt OrthogonalTiledMapRenderer przekazując również wczytaną wcześniej mapę, aby obiekt w późniejszej metodzie wyświetlania wiedział o którą mapę chodzi.

W metodzie render musimy dopisać aby renderowało wczytaną mapę, a następnie ustawiamy projekcje kamery na mapę.

renderer.render();
renderer.setView(camera);

Przesunięcie kamery

Domyślnie kamera którą stworzyliśmy skupia środek w punkcie współrzędnych 0 na x oraz 0 na y. Przez to widzimy tylko fragment mapy (czerwona ramka) który wczytaliśmy i czarne tło znajdujące się poza. Rozwiązaniem jest ustawienia środka kamery dokładnie w połowie świata który widzimy. Robimy to następującą linią, czego efektem ma być widok ten zaznaczony ramką żółtą. Trzecim parametrem jest współrzędna „z” której nie używamy, dlatego w jej miejsce wstawiamy 0.

camera.position.set(view.getScreenWidth() / 2, view.getScreenHeight() / 2, 0);

  
Skalowanie mapy

Następną rzeczą którą warto zrobić w tym miejscu to trochę pomniejszyć wielkość mapy, poskutkuje to większym obszarem mapy który jest widoczny. Poza tym wygląda to o wiele przyzwoiciej i lepiej jak na platformówkę. Zmieniamy poprzedni kod dopisując po przecinku 1 / 3f

renderer.setView(camera, 1 / 3f);

Może tu paść pytanie co robi ta literka f przy trójce. Już tłumaczę. Jest to jednoznaczne wskazanie że jest to liczba float. Dzięki temu przy podzieleniu nie zostanie ucięta końcówka liczby po przecinku tak jak ma to miejsce w przypadku typu int. Poniżej 2 sytuacje które to rozjaśnią.

1 / 3 = 3
1 / 3f = 3.3(3)

Na obecnym etapie to co mamy wygląda tak


Przesuwanie po mapie

Póki co widzimy jedynie fragment całej mapy, chcemy natomiast zobaczyć całość. Normalnie w takich typach grach następuje podążanie kamery za postacią. Postaci jeszcze jednak nie mamy, co możemy zrobić to przesuwać środek kamery na inne pozycje x i y dzięki czemu uzyskamy bardzo podobny efekt. Aby to zrobić potrzeba w metodzie render aktualizować kamerę. Tworzymy więc metodę którą nazywamy update.

public void update(float dt) {
        handleInput(dt);

        camera.update();
    }

Metoda będzie wymagać jeszcze zmiennej delta (dt). Delta jest to czas jaki zajęło wyrenderowanie ostatniej klatki. Renderowanie wykonuje się ciągle, tzn. co chwilę wykonuje się na nowo, tak aby obraz był widziany stale. Inaczej gdyby wykonało się to tylko jednokrotnie zobaczylibyśmy go lub nie przez ułamek sekundy. My chcemy aby pozycja naszej kamery zmieniała się również ciągle, gdy pojawi się do tego bodziec np. ruch postaci. Daje to efekt płynnej przejścia i nie jest to zmiana skokowa.

Zanim wykonamy update na kamerze potrzebujemy wyłapywać sygnały wejściowe sterowania (początkowo zrobimy strzałkami na klawiaturze). Tworzymy więc nową metodę którą zwiemy handleInput i tam również przesyłamy delta. Wewnątrz uwzględniamy cztery sytuacje jakie mogą nastąpić (prawo, lewo, góra, dół) korzystamy do tego z metody libgdx isKeyPressed która wykonuje się gdy klawisz został wciśnięty. Zmieniamy pozycję kamery odpowiednio dla lewo i prawo na osi x oraz góra, dół na osi y. Jako że renderowanie wykonuje się bardzo szybko możemy przemnożyć przez 100, co daje nam akceptowalną prędkość przesuwania kamery.

public void handleInput(float dt) {
        if(Gdx.input.isKeyPressed(Input.Keys.RIGHT)) {
            camera.position.x += 100 * dt;
        }
        if(Gdx.input.isKeyPressed(Input.Keys.LEFT)) {
            camera.position.x -= 100 * dt;
        }
        if(Gdx.input.isKeyPressed(Input.Keys.UP)) {
            camera.position.y += 100 * dt;
        }
        if(Gdx.input.isKeyPressed(Input.Keys.DOWN)) {
            camera.position.y -= 100 * dt;
        }
    }

Pozostaje jeszcze wywołać tą metodę w innej metodzie update, co widać we wcześniejszej kodzie tamtej metody. Od tego momentu możemy dowolnie przesuwać się po całej mapie.

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

Na dziś to tyle

Do zobaczenia

środa, 22 marca 2017

Edytor Map

W dzisiejszym wpisie postaramy się dodać edytor map do projektu. Poprzednio podczas planowania jednym z założeń jakie wynotowaliśmy było aby gra posiadała wiele poziomów, map, w celu uczynienia rozrywki bardziej różnorodnej i atrakcyjniejszej. Mapy są niczym innym jak elementami porozmieszczanymi po całym świecie, którego zaś fragment widzimy w naszym widoku. Teraz zastanówmy się jakby możliwe byłoby dodawanie wszystkich tych  wymienionych elementów takich jak: platformy, teleporty, przeszkody poprzednio tak „twórczo” wymyślone.

Podejście A

Pierwszym z pomysłów które może przyjść to aby przedmioty umieszczać po wysokości i szerokości wielkości naszego ekranu świata gry, gdzieś tam zaznaczając to w kodzie. Ale pytanie czy byłoby to wygodne? Akurat to pytanie nie jest z kategorii do zastanowienia, wydaje się dla nas oczywiste, żyjemy w święcie gier od młodych lat. Opiszmy to pierwsze podejście. Każdy z elementów mógłby być zahardkodowany, czyli wpisany na sztywno bezpośrednio w kodzie wysokość i szerokość. Można tworzyć zmienne typu Integer czyli przechowujące wartość liczbową i te zmienne później używać w metodach, które odpowiedzialne są za rysowanie tych obiektów. Do rysowania bloków stosowane byłyby różne pętle które jak wiemy wykonują się w ściśle określonych regułach, warunkach np. liczba powtórzeń, czy konkretne zakresy współrzędnych. To podejście jak najbardziej zadziała, ale jest zbyt uciążliwe i czasochłonne w realizacji, co chcemy oczywiście uniknąć.

Podejście B

Wyobraźmy sobie mamy ekran naszej planszy i myszką zaznaczamy miejsca w których chcemy umieścić dany obiekt, bądź obiekty. Dodatkowo od razu widzimy jak będzie to wyglądać w praktyce. Podobne obiekty np. platformy jednego typu możemy wrzucać w kontenery, czyli coś co przechowuje wiele wartości. I następnie wyświetlać całość kontenera jako jedną warstwę do której możemy podpiąć logikę, np. chcemy aby po najściu na tą warstwę postać straciła punkt życia w wyniku wejścia w płomienie.

Kontenery to taka struktura danych która umożliwia przechowywanie obiektów w sposób zorganizowany. Umożliwia ponadto operacje na tych zbiorach, takie jak przeszukiwanie, usuwanie i inne.

Rozwiązanie takie możliwe jest właśnie dzięki edytorom map. Są to narzędzia które znacząco przyspieszają cały proces tworzenia map. Całość zapisana jest do formatu wyjściowego który przy użyciu odpowiednich bibliotek można wczytać do aplikacji w przeróżnych aplikacjach i różnych językach programowania. Jeden z takich popularnych edytorów nosi nazwę Tiled i użyjemy go w projekcie. Jest on edytorem do grafiki w dwóch wymiarach, czyli posiadających wysokość i szerokość.



Rzuty

Elementy na mapie mogą występować w postaci prostokątów, elips, krzywych,  czy obrazków. Tiled wspomaga tworzenie zarówno ortogonalnych, izometrycznych i hexagonalnych map.

Przy okazji wyjaśnijmy co mówią te rzuty po kolei. O ortogonalności mówi się w miejscach gdzie występuje prostopadłość, może wystąpić zarówno w rzucie 2d jak i 3d. Projekcje ortogonalną cechuje także że wielkość obiektu nie jest zależna od odległości od kamery. Można to rozwiązanie spotkać w takich grach jak Tibia, bądź Simcity. 


Rzut izometryczny odzwierciedla przestrzeń 3d na płaszczyźnie, rzuty mają zawsze równą miarę kątów 120° między parami osi. Jest również przykładem rzutu równoległego. Świetnie prezentuje to rysunek poniżej



Hexygonalna mapa stworzona z sześciokątów, najczęściej można zobaczyć ją w grach wojennych, przykładem jest znana gra Heroes. Jeżeli nic wam nie mówią wymienione tytuły warto spojrzeć w google, są to klasyki gier.


Pobieramy Tiled

Pobieramy przechodząc na stronę w linku klikamy kolejno Download via itch.io, po przejściu do kolejnej strony klikamy Download Now. W Widocznym oknie mamy możliwość dotować developerów twórców tego narzędzia. Same zaś narzędzie jest co można przeczytać darmowe. Chcąc tylko pobrać bez dotowania klikamy na napis widoczny na obrazku poniżej.



Dalej możemy wybrać wersję pasującą do naszego systemu operacyjnego (windows, linux, mac) klikając przycisk Download przy odpowiedniej wersji. Powinien pojawić się okno pobierania instalatora.
Proces instalacji wygląda jak standardowy każdej aplikacji, nie ma przy tym żadnej konfiguracji. Lokalizacja miejsca instalacji nie ma właściwie znaczenia. Na końcu klikamy Finish i powinno automatycznie otworzyć się okno programu.

Nowa Mapa

W celu stworzenia nowego pliku mapy klikamy w lewym rogu na przycisku menu Plik, z listy wybieramy Nowy… Pojawia się okno tego rodzaju



Po kolei poustawiajmy i przeanalizujmy jakie możliwości mamy.

Na początku wybieramy orientacje ortogonalną, taka właśnie jest używana w platformówkach. Format warstwy kafelków ustawmy na Base64 (kompresowany zlib).

W format warstwy kafelków do wyboru mamy dwa formaty CSV i Bas64. CSV jest formatem gdzie dane przechowywane są w plikach tekstowych, a wartości rozdziela się przecinkami. Base64 jest formatem gdzie bajty koduje się ciągami znaków. Wybiera się 64 znaki którym przypisuje się wartości 0-63, 63 ponieważ 0 również uznaje się za wartość którą bierze się pod uwagę. Znaki zamieniane są na ciągi binarne, i następnie taki utworzony ciąg dzieli się na liczne podciągi. Jest powszechnie stosowany do kodowania poczty email, a jego charakterystyczną cechą jest że, zbiór wyjściowy jest o 33% większy od zbioru wejściowego.

Pojawia się tu słowo kodowanie i bajty, można jeszcze podciągnąć co to znaczy bit.
Kodowanie to przede wszystkim taka transformacja tekstu jawnego aby stał się nie dostępny niepowołanym osobom trzecim. Stosuje się tu zamiennie pojęcia kod i szyfr. Jednak istnieją subtelne różnice które je rozróżniają, a dotyczą głównie stopnia przekształcenia.

Kolejność renderowania kafelków domyślnie ustawiona jest na Prawy Dół i tak możemy zostawić. W rozmiarze mapy ustawiamy szerokość i wysokość w ilościach kafelek mieszczących się na mapie. Początkowo ustawimy 20 kafelek wysokości i 40 szerokości, co w późniejszych etapach może ulec zmianie. Rozmiar kafelka ustawiamy również na 70x70 px, co głównej mierze zależy jakie wielkości tekstury posiadamy bądź stworzymy w przyszłości. Samo 70px mówi ile pikseli mieści się w jednym „kartoniku”. Im jest ich więcej tym tekstury są ładniejsze i bardziej szczegółowe. Obecnie większość gier stosuje większą nowocześniejszą rozdzielczość, mówi się wtedy że są bardziej „mangowe”, a 16 px określa się jako „retro”. Po wybraniu obu wielkości możemy zobaczyć jaką rozdzielczość w pikselach będzie miała cała mapa (2800x1400 pikseli).

Pierwsze co należy zrobić to załadować pliki graficzne z teksturami które będziemy używać do tworzenia map. Można do tego używać pojedyncze grafiki albo całe zestawy zwane tilesety. W Internecie można znaleźć sporo stron na których można je znaleźć. Zawsze jednak należy patrzeć jaką licencje posiada dany plik. Jednym dość popularnym źródłem jest opengameart.org, gdzie większość jest na licencji creative commons, co znaczy tyle że autor przekazał je domeny publicznej i można robić z nimi wszystko bez ograniczeń, bez żadnej wymaganej zgody. Te które wpadły mi akurat w oko to link. Należy rozpakować i w folderze Tiled zestaw nosi nazwę tiles_spritesheet.png.

Dodanie tekstury do Tiled

W aplikacji Tiled klikamy na znaczek dodaj w dolnym prawym rogu


W oknie w miejscu gdzie wybieramy źródło klikamy przeglądaj i wskazujemy rozpakowany wcześniej tiles_spritesheet.png. Szerokość i wysokość kafelka ustawiamy na 70 px, identyczne jakie zawiera plik tile który posiadamy. Margines mówi o odległości od wszystkich zewnętrznych krawędzi obrazka, u nas nie ma więc ustawiamy na 0 px. Konieczne jest natomiast ustawienie Rozstawu na 2 px, takie zawiera nasz przykładowy zbiór. Można kliknąć OK.


„Wyklikujemy” mapę

W prawy rogu mamy okno z warstwami. Warstwy można traktować jako elementy tego samego typu. Przykładem może być podłoże tego samego typu, bądź monety do zebrania. Każdej warstwie można będzie w naszej grze przypisać inną logikę, jaka ma być wykonywana podczas interakcji naszej postaci z daną warstwą, bądź warstw między sobą. Na początku stworzymy tło, podłoże i kilka platform tego samego typu aby później można było naszą postacią po tym chodzić i skakać. Klikamy dwukrotnie na Warstwa Kafelków 1 i zmieniamy nazwę na background.


Teraz z zestawu kafelków wybieramy taki jaki chcemy użyć do tła, pełny niebieski kwadracik z zestawu wygląda na odpowiedni na tymczasowe tło. Z menu klikamy na ikonę wypełniania i klikamy na obszarze całej mapy. Powinno wypełnić nią całość obszaru.


Teraz pora na podłoże. W tym celu klikamy na ikonę białej kartki z żółtym słoneczkiem w górnym okienku z warstwami, tak jak na rysunku powyżej. Po kliknięciu rozwinie się lista z której wybieramy Object Layer i nadajemy nazwę ground. Teraz zaznaczając warstwę i wybierając kafelkę jaką wybraliśmy dla niej i możemy umieszczać je na mapie klikając. Tu zostawiam wam dowolność. Ten który stworzyłem znajdzie się później na githubie przy kolejnej aktualizacji projektu.

Aha to co można zauważyć to że podłoża są typem obiektowym, chyba domyślacie się dlaczego. Potrzebna będzie jeszcze logika do tego Na dziś wystarczy tyle, pokazany był proces tworzenia mapy, następnie trzeba będzie go podpiąć do projektu, to już wkrótce. Nie zapomnijcie zapisać tego co zrobiliście do tego momentu.

Do zobaczenia.

piątek, 17 marca 2017

Kamera, widok, akcja


Często aplikacja może być uruchamiana na różnych urządzeniach z różną wielkością ekranu. Samych rozdzielczości wyświetlania jest cała masa, przy czy można wyróżnić kilka formatów (Aspect Ratio), co widać kolorami na rysunku poniżej. Ma to wpływ bezpośrednio na to jak wygląda nasza gra, tu właśnie w libGDX przychodzą klasy Viewport, które określają sposób radzenia sobie z tym problemem. Dwa Elementy które będą nam potrzebne na dziś to camera i viewport. Spróbujmy jakoś to umówić.



Camera

Pierwszym krokiem będzie zastosowanie kamery. To tak jak w rzeczywistości możemy na obiekty patrzeć albo na wprost albo spod kąta. I w libGDX mamy do wyboru dwie możliwości PerspectiveCamera i OrthographicCamera. Przeszukując Internet udało się znaleźć fajny rysunek który idealnie prezentuje różnicę w obu podejściach.



Jak widać w kamerze ortogonalnej, obiekty rzutowane są równolegle do punktu widzenia, zaś w kamerze perspektywicznej widzenie rozchodzi się z jednego punku i pod różnym kontem rzutowane są zarysy obiektów na ekranie który je wyświetla. W naszym przypadku platformówki oczywiste jest zastosowanie kamery ortogonalnej, jako że obiekty widziane są bezpośrednio tak jakby z boku.

Poniższe linie umieszczamy w naszym projekcie. Najpierw w naszym ekranie gry czyli PlayScreen inicjalizujemy obiekt OrtographicCamera który nazywamy przykładowo camera.

    private OrthographicCamera camera;

Póki co jest to na razie tylko miejsce zarezerwowane. To co potrzeba teraz zrobić to w konstruktorze powiedzieć/przypisać że jest to nowy obiekt klasy OrthographicCamera następującą linią kodu:

    camera = new OrthographicCamera();

Właśnie tak w Javie wygląda tworzenie nowych obiektów wcześniej już zdefiniowanych klas. Dodatkowo można pomiędzy nawiasami przesyłać różne parametry, jeżeli dana klasa ma zdefiniowane różne konstruktory. O konstruktorach było już wcześniej, dlatego jeżeli istnieje potrzeba to wróć do wcześniejszego wpisu.

Viewport

Viewporty jest to pojęcie z teorii grafiki komputerowej, ale spokojnie to nie uczelnia ;d. Patrząc przez kamerkę jaką znamy np. z aparatu naszego telefonu widzimy tylko wycinek tego co dzieje się wokoło. Viewport to właśnie nic innego jak obszar w postaci wieloboku, inaczej obszar zainteresowania, to co widzimy. A co w przypadku gdy obszar wyświetlania się zmienia np. wysokość się zwęża, jak powinny wyglądać wtedy wyświetlane elementy? libGDX dostarcza różne możliwe na to strategie. Porównamy dla wyczucia różnic dwie z nich FitViewport oraz StretchViewport. Więc dopisujemy stosowne linie kodu, później sprawdzimy różnice i wybierzemy ten odpowiadający naszym potrzebom.

W PlayScreen dopisujemy:

    private Viewport view;

natomiast w konstruktorze

    view = new StrechViewport (800, 480, camera);

Sam StrechViewport ma zdefiniowane dwa konstruktory: dwu i trzy parametrowy. Wykorzystujemy jednak trzy parametry, czyli szerokość, wysokość okna, oraz z jakiej kamery mamy patrzeć na te obiekty. Patrzymy na wprost czyli konieczna jest użycie wcześniej stworzonej OrthographicCamery. StrechViewport przy zmianie wielkości ekranu rozciąga zawsze tak aby obiekt zajmował tą samą część ekranu, czyli nie trzyma aspect ratio. Co czyni niestety przy np. wąskich ekranach, że obiekt staje się słabo widoczny. Takiego efekt raczej nie będzie wyglądał ciekawie. Fitport dla odmiany utrzymuje aspect ratio przy zmianach szerokości i wysokości. A miejscach pozostałych dodaje czarne paski. Z reszta na rysunku wyraźnie to widać. W naszym projekcie zastosujemy Fitport.


HUD

Mamy zrobiony już widok oraz kamere. Jak w większości gier wyświetlają się w którym miejscu jakieś punkty życia, magii, zebrane punkty czy czas. Jest dobry moment aby się za to się zabrać i wyświetlić to na widocznym ekranie. Jednak nie można tego bezpośrednio tak po prostu wyświetlić w viewport, z prostej przyczyny że nasz świat będzie się poruszał. W tym celu stosuje się osobny viewport i kamerę, dzięki czemu będzie mogło być statyczne niezależnie od reszty. Określa się to HUD (Head-Up Display), jest to przezroczysta szybka która wyświetla informacje. Sam termin zaczerpnięty z zastosowań militarnych, gdzie służył do prezentacji danych w samolotach bojowych.

Tworzymy nową klasę Hud, umieszczając ją w nowym pakiecie Scenes.

Pierwsze co to inicjalizujemy obiekty jakie będziemy potrzebować. W języku Java który jest językiem obiektowym, każda element jest obiektem. Przykładowo zwykły tekst

    String nazwa = „przykładowy string”

jest również obiektem klasy String. Wracając jak było wcześniej wspomniane potrzeba stworzyć viewport i kamerę. Ponadto zakładamy że chcemy wyświetlić napis pokazujące zdobyte punkty i poziom gry.

    public Stage stage;
    private Viewport viewport;
    private Integer score;

Stage jest sceną na której występują aktorzy. Stage jest jakby kontenerem wszystkich obecnych obiektów. Aktorzy to graficzne elementy z konkretnymi parametrami, które mogą dodatkowo się zmieniać. Wszystko co widzimy na scenie jest jakby w trakcie podłączenia do prądu, występuje akcja, mogą na tym występować różne animacje, czynności np. zderzenia, czy nachodzenie na siebie. Wszystkie obiekty są nasłuchiwane. Dodatkowo mogą się poruszać i być animowane.  Oczekują także na interakcje z użytkownikiem, który może mieć wpływ na przebieg zdarzenia przez np. sterowanie klawiaturą

Sama zmienna Integer przechowuje jedynie wartość. Aby możliwe było wyświetlenie tego na ekranie potrzeba skorzystać z klasy Label.

    Label scoreLabel;
    Label levelLabel;

Pora na konstruktor, to co trzeba jeszcze wiedzieć to rysowanie wymaga skorzystania z klasy SpriteBatch. SpriteBatch jest jakby kontenerem elementów do rysowania, gdzie wskazuje się indeksy położenia. Przykładowo jeżeli chcemy wyświetlić jakiś obrazek potrzeba go w takim kontenerze umieścić. Wykonuje się to pomiędzy batch.begin() i batch.end(). Aby nasz Hud był widoczny potrzeba go umieścić w batch. Robi się to po przez przesłanie go przez konstruktor. Celem jest podanie tego przy tworzeniu Stage, tak aby mógł zostać uwzględniony w kontenerze do wyświetlania, drugie co podajemy to do Stage to jaką strategie wyświetlania wybraliśmy przez viewport.

        public Hud(SpriteBatch sb) {
        score = 0;

        viewport = new FitViewport(800, 480, new OrthographicCamera());
        stage = new Stage(viewport, sb);


Teraz potrzebne będzie jakaś organizacja wyświetlania naszych labeli na ekranie. W libGDX służy do tego klasa Table. To tak jak dla gości wystawiasz wszystko na stół, gdzie wszystko jest osobno poukładane. Pozwala nam ustawianie nam elementów w jakąś siatkę kolumnami, bądź wierszami, w orientacji jakiej chcemy czy to poziomej czy pionowej. Metodą setFillParent możemy określić aby zajmowała cała powierzchnię, natomiast top() aby były układane od góry. W tym miejscu można się domyśleć że wszystko co znajduje się kropce to korzystanie z metod. I tak bierzemy stworzony obiekt table i po kropce używamy jego metody top(). Co w zależności jak jest zaimplementowane wewnątrz można spodziewać się różnych rezultatów.

        Table table = new Table();
        table.top();
        table.setFillParent(true);

Dalej nic wielkiego się nie dzieje obiektom ustawiamy szereg właściwości. Taki jak format wyświetlania z przewidywaną ilością znaków, rodzaj domyślnego fontu, czy kolor biały

    scoreLabel = new Label(String.format("%06d", score), 
        new Label.LabelStyle(new BitmapFont(), Color.WHITE));
    levelLabel = new Label(String.format("1", score), 
        new Label.LabelStyle(new BitmapFont(), Color.WHITE));

W tym miejscu pora dodać je do naszego stołu metodą add. Metodą expandX() ustawiamy na pełną szerokość. Jednak gdy wiele przedmiotów znajduje się obok siebie to przyjmują one po równo przestrzeni w zależności ile jest tych przedmiotów. PadTop(10) ustala wielkość komórek.

    table.add(scoreLabel).expandX().padTop(10);
    table.add(levelLabel).expandX().padTop(10);

na końcu dodajemy cały table do naszej sceny. Hud jest już gotowy, pora go użyć.

    stage.addActor(table);

Wracamy do PlayScren inicjalizujemy Hud

    Private Hud hud;

W konstruktorze tworzymy ten obiekt przesyłając obiekt game.batch.

    Hud = new Hud(game.batch)

W metodzie render usuwamy te linie z przykładu gdzie był game.batch. Chcemy teraz widzieć Hud zamiast przykładowego rysunku.

Musimy wskazać spritebatch gdzie obecnie jesteśmy, tzn. gdzie patrzy kamera. To co na ekranie powinno być widoczne to właśnie to na co patrzy kamera. Combined jest połączeniem projekcji i widoku. Robimy to linią

    game.batch.setProjectionMatrix(hud.stage.getCamera().combined);

W końcu zlecamy wyrysowanie całości

    hud.stage.draw();

Efekt na obecny moment prezentuje się następująco


Przy okazji powstał pomysł na inny wpis więcej o kamerach i perspektywach w grach. Tyle na dziś do odbioru.

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