środa, 5 kwietnia 2017

Bubel do ziemi

Mając już naszego bohatera (będę go nazywał bublem) oraz platformy, można powoli już coś z tym zrobić. Bubel zawieszony został trochę w powietrzu, nie bez powodu. Spowodujemy aby zaczęła przyciągać go siła (grawitacja) ku podłożu. Mamy w konstruktorze PlayScreen napisana odpowiednią do tego metodę. Wystarczy tylko zmienić parametry z (0, 0) na przykładowe (0, -10)

world = new World(new Vector2(0, -10), true);

A graficznie reprezentuje się to tak


Efektem będzie przyciąganie ku podłożu bo tak określony jest wektor, będzie on skierowany od punktu początkowego (0, 0). Im większy parametr ustalimy tym siła odpowiednio większa.

Można odpalić aplikację. Okazuje się ze na obecny moment nic się nie zmieniło. Bubel lewituje tam gdzie poprzednio. To czego nam brakuje to ustawienie odśnieżania. Robimy to ze względu, że zaczynają pojawiać się u nas elementy które się poruszają przez działanie różnych sił, co wymaga koniecznych obliczeń. Chcemy także postać mogła być sterowalna, to także wymaga kalkulacji. Ustalamy to parametrem step linią kodu w metodzie update

World.step(1 / 60f, 6, 2);

Parametry które pojawiają się przy określaniu kroku World to kolejno (time step, potem velocity iteration i position iteration). Po kolei wyjaśnijmy. Time step określa czas do upłynięcia do następnej kalkulacji. Ma ono ogromny wpływ na działające siły między obiektami, czy grawitacją. Po prostu wszystko dzieje się szybciej. Velocity, position iteration w głównej mierze określają jak dokładnie mają być przeprowadzane symulacje. W przypadku kolizji 2 elementów muszą być przeprowadzone pewne obliczenia, aby określić w jakie miejsce mają się przemieścić, albo w jaki sposób obrócić aby „wyjść” z momentu kolizji. Im większa ustalona ich wartość tym dokładniejsze są obliczenia (np. większy obszar obiektu jest brany pod uwagę), jednak nie ma nic oczywiście za darmo – robimy to kosztem utracenia na wydajności. Aha, obecne wartości wzięte są z dokumentacji także nic nie uwzględniają specjalnego.

PPM

Na ten moment można zobaczyć powolnie opadającego Bubla. W tym momencie poszukując spotkałem się z pojęciem PPM które jest za to odpowiedzialne. Spróbujmy dotrzeć do zrozumienia co to właściwie jest. Pierwsze co to można spojrzeć rozwinięcie skrótu Pixel per meter. Ale ?#& co, że co jaki meter?? Takie mniej więcej pytanie sobie postawiłem. Odpowiedź jest jednak dosyć banalna. Fizyka gry Box2D zakłada, że używamy identycznych metryk jak w świecie rzeczywistym (metry, kilogramy, sekundy). W ten sposób zakłada się, że 1 pikselowi odpowiada 1 metr w rzeczywistości. Patrząc na nasze ekrany to są tam długości rzędu 1000 pikseli (grę mamy ustalona na 800x400). Tak więc z położenia o współrzędnych (0, 0) do (1024, 0) odzwierciedla rzeczywistą odległość 1024km. Więc trwa to odpowiednio długo. Dokładnie o tym w tym artykule. Należy coś z tym zrobić i odpowiednio to przeskalować.

More Universal

Przy okazji poprawimy jedną rzecz. Niby nieznaczna, ale kiedyś możemy chcieć zmieniać rozdzielczości i dzięki temu zyskamy na czasie i mniejszych problemach. Jeżeli spojrzymy zarówno w klasie PlayScreen i Hud przy tworzeniu widoku ustawiamy osobno rozdzielczość na 800x400. Możemy zrobić to znacznie efektywniej przekazując ją jako stałą zdefiniowaną w klasie głównej WordCharger. Tworzymy więc dwie statyczne finalne zmienne V_WIDTH, V_HEIGHT. Statyczna w tym przypadku oznacza że może istnieć tylko jedna instancja tej zmiennej w całości działania programu. Zaś finalna oznacza że nie można jej zmieniać. Dzięki modyfikatorowi public będzie można odwołać się do nich z dowolnego miejsca

public static final int V_WIDTH = 800;
public static final int V_HEIGHT = 480;

Teraz przechodzimy do klasy PlayScreen i zmieniamy aby wyglądało to w taki sposób

view = new FitViewport(WordCharger.V_WIDTH, WordCharger.V_HEIGHT, camera);

Najpierw musimy wskazać klasę, a po kropce odwołujemy się do zmiennych bądź metod w zależności od obecnych tam modyfikatorów dostępności. Analogicznie robimy w klasie Hud.

Teraz zrobimy skalowanie. Definiujemy w klasie głównej gry WordCharger zmienną którą użyjemy w pozostałych elementach gry.

public static final float PPM = 100;

Użyjemy jej chcąc uzyskać rezultat w którym 1 piksel jest równoważny 100 metrom. Czyli około 100 razy więcej niż poprzednio. Piksel to 1 punkcik na wyświetlanym ekranie. Co jeszcze ważne musi być koniecznie typu float, ponieważ ponownie będziemy wykonywać działania dzielenia i nie chcemy tracić liczb po przecinku.

W klasie PlayScreen zmieniamy

view = new FitViewport(WordCharger.V_WIDTH / WordCharger.PPM, WordCharger.V_HEIGHT / WordCharger.PPM, camera);

renderer = new OrthogonalTiledMapRenderer(map, 1 / 1.3f * (1 / WordCharger.PPM))

Ta linia może być trochę niejasna. 1 / 1.3f * (1 / WordCharger.PPM) wynik jaki tu uzyskamy jest drugim parametrem który przyjmuje konstruktor gdy chcemy uzyskać skalowanie. Z tym że uwzględniamy tu zarówno skalowanie jakie wymagało aby obszar debagowania pokrywał się z rzeczywistym oraz skalowanie które wynika z PPM.

Dalej w klasie WordCharger zmieniamy jeszcze

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

shape.setAsBox(rect.getWidth() / 2 / WordCharger.PPM, rect.getHeight() / 2 / WordCharger.PPM);

w klasie BatteryHero zmieniamy

bodyDef.position.set(70 / WordCharger.PPM, 140 / WordCharger.PPM);

shape.setRadius(35 / WordCharger.PPM);

Podsumowując zmieniliśmy wszystkie elementy odpowiedzialne za wyświetlanie.


Sterowanie Bublem

Sterowanie w libGdx wykonuje się na 2 sposoby. Idąc za dokumentacją może być wykonana za pomocą impulsów lub za pomocą siły. Użycie siły jednak jak zauważają, jeżeli nie są skierowane w środek masy tworzą się momenty obrotowe i prędkości kątowe. My jednak tego nie chcemy. Drugim sposobem jest użycie impulsów, które polegają na nadanie prędkości obiektowi w odpowiednim kierunku.

player.b2dBody.applyLinearImpulse(new Vector2(2f, 0), player.b2dBody.getWorldCenter(), true);

applyLinearImpulse potrzebuje kolejno parametrów (wektor impulsu, wektor punktu środka masy, wartość boolean rozbudzania obiektu – aby nie przeprowadzało kalkulacji). Wektor punktu środka masy uzyskamy metodą player.b2dBody.getWorldCenter() co da punkt centralnie w środku obiektu.

Teraz zmieńmy nasze wcześniej utworzone warunki sterowania. Zacznijmy od zmienienia metody inputu na isKeyJustPressed. Różni się od isKeyPressed tym że wykrywa moment kiedy przycisk wciśniemy i puścimy. Samo isKeyPressed wykrywa gdy klawisz jest wciśnięty, co będzie powodowało w momencie przytrzymania ciągły ruch, a to spowoduje wystrzelenie w bok, lub kosmos ;d. Tym drugim sposobem unikamy tego problemu.
Dodatkowo fragmentem player.b2dBody.getLinearVelocity().x <= 2 uzyskujemy maksymalne przesunięcie na osi x przez okres 1 sekundy. Tym sposobem unikamy przypadku ciągłego szybkiego wciskania przycisku, aby postać znów nie wyskoczyła gdzieś w dal.

Tak będzie wyglądać całe sterowanie


if(Gdx.input.isKeyJustPressed(Input.Keys.RIGHT) && player.b2dBody.getLinearVelocity().x <= 2) {
player.b2dBody.applyLinearImpulse(new Vector2(2f, 0), player.b2dBody.getWorldCenter(), true);
}

if(Gdx.input.isKeyJustPressed(Input.Keys.LEFT) && player.b2dBody.getLinearVelocity().x >= -2) {
player.b2dBody.applyLinearImpulse(new Vector2(-2f, 0), player.b2dBody.getWorldCenter(), true);
}

if(Gdx.input.isKeyJustPressed(Input.Keys.UP)) {
player.b2dBody.applyLinearImpulse(new Vector2(0, 5f), player.b2dBody.getWorldCenter(), true);
}


Obecnie można sterować postacią. Na końcu dodamy jeszcze podążanie za nią kamery.

W metodzie update klasy PlayScreen dodajemy

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

Co oznacza tyle że obecne położenie zarówno na osi x i y zostaje ustalane na pozycję x i y bohatera.

Można uruchomić grę. To co widzimy postać się porusza. Są właściwie dwa problemy które trzeba będzie rozwiązać. Pierwsze postać zmienia swoje położenie względem środka kamery, do momentu aż znika poza ekran. Drugie niepożądane zachowanie to możliwe wyjście bohatera poza obszar widocznej mapy. Trzeba będzie zająć się tym w przyszłości.



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


Na dziś tyle,
Pozdrawiam.

niedziela, 2 kwietnia 2017

Jak naprawiać oraz wyświetlanie obszaru debug bohatera

Dzisiaj trochę dla odmiany w zupełnie innej kolejności. Najpierw naprawimy to co ostatnio nabroiliśmy. Tutaj chciałbym wam pokazać jak wyglądał proces takiego szukania rozwiązania. W zasadzie każdy to chyba wie, ale i tak to opiszę.

Pierwszy wniosek jaki można wysnuć to, że problem musi leżeć gdzieś w obiekcie b2dr, dosyć to proste bo jest to obszar odpowiedzialny za renderowanie wraz z debugowaniem (zielona pomocnicza ramka, aby wiadomo było jaki obszar uwzględniamy).

Co należy wcześniej zauważyć w konstruktorze PlayScreen początkowo ustawiliśmy dla obiektu renderer (dla TiledMap) skalowanie 1 / 1.3f. w konstruktorze również zainicjowaliśmy nowy obiekt Box2DDebugRenderer b2dr. Czyli co, mamy 2 rożne obiekty do renderowania. Jeden ma informacje o skali, drugi jej nie ma na ten moment. Spójrzy teraz w metodę render. Tam dla obiektu renderer następuje wywołanie metody wyświetlającej render. Dla b2dr również zostaje wywołana metoda render. Ale zaraz, zaraz, a gdzie jest skalowanie? Dalej patrząc przyjmuje ona 2 parametry (world i camera.combined). World za bardzo nie ma jak się czepić ponieważ jest to tylko kontener z elementami, za to można czepić się kamery, bo to ona jest odpowiedzialna za wyświetlanie. Mamy. No dobra to jest wyjaśnienie, ale skąd wiadomo ze można było czepić się akurat kamery?

3 metody poszukiwania rozwiazania

Wersja Lucky Luke
Najpierw można próbować szczęścia, tak jak było to w moim wypadku. Czasem bezskutecznie. Do obiektu b2dr próbowałem użyć metodę do skalowania dopisałem więc .scale. Genialne co? Nic jednak takiego nie było.
Wersja dla mających czas
Można na danym obiekcie kliknąć trzymając klawisz Ctrl, powinno przejść do całości kodu danej klasy. Można wtedy przejrzeć wszystkie metody i konstruktory czy nie ma tam gdzieś takiej możliwości. Często jest to konieczne.
Wersja ratuj! (najskuteczniejsza)
Wpisujemy w Google słowa kluczowe, bądź błędy które sprawiają nam problem. Im lepiej, dokładniej to określisz tym szybciej odnajdziesz co potrzeba. Gdzieś kiedyś słyszałem i utkwiło to w głowie, ze każdy problem jaki masz już prawdopodobnie ktoś inny rozwiązał. Czasem trzeba stosować pewne analogie, akurat do twojego problemu.

Po wpisaniu w google scale debug libgdx. Wystarczy przejrzeć kilka kilka odnośników i coś się znajdzie. Rozwiązanie czekało pod tytułem „libgdx – Box debug draw not correct”. Jest cała masa stron internetowych gdzie można zadawać pytania i odpowiadać. Jednak w większości przypadków wystarczają już istniejące odpowiedzi. Należy wspomnieć tu o najstarszej i największej takiej stronie stackoverflow. Jak to mówią jest to po prostu MUST TO HAVE.

Adaptacja do naszego problemu

Na stronie znajdowała się akurat taka linia kodu dla innej gry

b2dr.render(world, cam.combined.cpy().scale(PPM, PPM, 1));

coś pięknego, bierzemy. A u nas zmieniamy

b2dr.render(world, camera.combined.scale(1 / 1.3f, 1 / 1.3f, 0));

Przy czym przy skalowaniu należy podać skalowanie na x, y, z. Z wymiaru nie mamy, więc po prostu można dopisać 0. Można to sprawdzić najeżdżając myszą nad metodę scale wraz z przytrzymanym Ctrl. Lub kliknąć wraz z Ctrl przechodząc do treści metody.


 Upragniony efekt. Idealna otoczka wokół platform, skrzyni i podłoża.



Dla tych co dotrwali obiecuje że to jednorazowe tak rozległe wyjaśnienie. Będzie pojawiać się tylko w formie dlaczego.

Chochliki (Sprites)

Stwórzmy teraz dla porządku nowy pod pakiet dla tzw. Sprites (duszki, chochliki). W grafice komputerowej mówi się tak na pojedyncze obiekty animowane które mogą poruszać się po ekranie oraz można nimi sterować. Mogą to być zarówno bohaterowie jak i przeciwnicy. Termin pojawił się już w latach 70, są to czasy gdzie powszechne były komputery 8-bitowe. Dzięki skalowaniu i nakładaniu sprit-ów na siebie uzyskiwano efekty światłocieni, co jak na lata 70 dawało sporo większe możliwości tworzenia bardziej dynamicznych gier. Same techniki korzystające z sprit-ów znacznie poprawiły możliwości graficzne komputerów, gdzie procesory jak na tamte czasy były dziesiątki tysięcy razy wolniejsze niż te obecne.

Wewnątrz pakietu tworzymy nową klasę BatteryHero – będzie to postać naszego bohatera. Po nazwie klasy dopisujemy extends Sprite. O extends było już we wpisie przy zagadnieniu Rozszerzamy klasy.
Wewnątrz na początku inicjujemy dwie zmienne World world i Body b2dBody. World ponownie jako pojemnik na obiekty którymi można zarządzać, a b2dBody jako ciało które posiada wiele stanów (kształty itp.).

public World world;
public Body b2dBody;

Konstruktor Chochlików

Kolejną rzeczą będzie stworzenie konstruktora. Czyli wszystko co ma być utworzone w momencie powołania do życia BatteryHero (klasy). Tu chcielibyśmy zdefiniować wygląd, typ i inne. Można wszystko wpisać w wewnątrz, ale można dla większej przejrzystości przenieść to do osobnej metody, a w konstruktorze zostawić tylko jej wywołanie.

Wewnątrz konstruktora do którego przekazujemy obiekt world

public BatteryHero(World world) {
        this.world = world;
        defineBatteryHero();
}

Pojawia się tutaj słówko this. Co w tym miejscu to nam mówi. Musimy najpierw spojrzeć na 2 elementy tu obecne. Pierwszy jest u góry klasy gdzie inicjujemy World world. Drugi element znajdujemy patrzący na konstruktor ten powyżej, ma on przekazany obiekt (World world). Zastanówmy się nad sensem tej machinacji ;d. Przekazując obiekt z miejsca, w naszym przypadku gdzie będziemy tworzyć bohatera. Na razie ku ścisłości on nie istnieje, jest to tylko jego opis i możliwe zachowania. Tworzyć go będziemy nie w wewnątrz tej klasy tylko z innego miejsca. W miejscu gdzie będziemy tworzyć bohatera (prawdopodobnie PlayScreen) obowiązuje pojemnik world z elementami którymi możemy zarządzać. Chcemy aby nasz bohater był dodany do tego pojemnika. Więc przekazujemy pojemnik world do klasy bohatera BatteryHero. Ta klasa mając ten pojemnik może dodać bohatera do wewnątrz. I o to właśnie chodzi.

Mając w konstruktorze

this.world = world;

analogia
z tej klasy = przekazane przez konstruktor;

należy widzieć to w sposób następujący
this.world jest tym miejscem zarezerwowanym u góry klasy. W to miejsce przypisujemy ten obiekt który został przekazany w konstruktorze (World world). Od tego miejsca to wewnątrz tej klasy pod obiektem world jest to samo co w PlayScreen.   

Definiowanie bohatera

metodę umieszczamy gdzieś poniżej

public void defineBatteryHero() {
        BodyDef bodyDef = new BodyDef();
        bodyDef.position.set(70, 140);
        bodyDef.type = BodyDef.BodyType.DynamicBody;
        b2dBody = world.createBody(bodyDef);

        FixtureDef fixtureDef = new FixtureDef();
        CircleShape shape = new CircleShape();
        shape.setRadius(35);

        fixtureDef.shape = shape;
        b2dBody.createFixture(fixtureDef);
}

Nazwa metody powinna sugerować jakąś czynność którą wykonuje. Idealnie jeżeli zarówno zmienne jak i nazwy metod mówią wszystko co trzeba, a komentarze stają się zbędne. Określa się to wtedy mianem clean code i są to dobre praktyki programistyczne. Metoda jest publiczna i zwraca typ void (czyli nic nie zwraca). Czasem metody zwracają po wykonaniu jakiś wynik, wtedy w miejscu void określa się jakiego typu ma być wynik (przykładowo może to być String, czyli napis), a w ciele całej metody, bądź na końcu powinna być linia kodu z słowem kluczowym return

return zmienna;

Wnętrze metody zróbmy na szybko ponieważ dużo elementów pojawiło się już wcześniej podczas wyświetlania tiledMap. Tworzymy najpierw korpus. Ustawiamy jego pozycję na 70 na x, 140 na y. Co ważne ustalamy w ten sposób punkt środka.


Ustawiamy typ jako dynamiczny, ponieważ chcemy aby bohater się poruszał. Tworzymy ciało b2dBody w pojemniku world o zadanym kształcie.

Kolejno ustalamy stan obiektu, w tym przypadku jego kształt. Tym razem kształt ustalony został na okręg. Powód prosty, po prostu sylwetka bardziej przypomina kształt owalu, oraz biorąc pod uwagę jakiś zakres ruchu postaci (przebieranie nogami i rękoma) przybiera to bardziej kształt okręgu niż kwadratu.

Takie pierwsze ilustrujące to skojarzenie


Żródło: http://www.wiking.edu.pl/upload/jezyk_polski/images/renesans.jpg

Metodą setRadius() ustawiamy promień koła jaki chcemy uzyskać. Jak pamiętamy poprzednio kafelki które wykorzystaliśmy przy mapach miały one rozmiar 70x70 pikseli. Więc chcąc uzyskać taką szerokość i wysokość podajemy połowę jako promień koła.


Przypisujemy stanom fixture kształt koła. W końcu te stany tworzymy dla obiektu postaci b2dBody (ciała do wyświetlenia).

Wyświetlanie bohatera (debug)

Aby wyświetlić bohatera na razie tylko jako obszar debugowania (tekstury dołożymy później) należy zainicjować go w PlayScreen. Inicjalizujemy więc na początku klasy obiekt klasy BatteryHero (to co dzisiaj zrobiliśmy).

Private BatteryHero player;

Następnie w konstruktorze i tu kluczowe, poniżej miejsca gdzie utworzyliśmy nowy obiekt World piszemy

player = new BatteryHero(world);

Koniecznie poniżej, ponieważ przy tworzeniu playera chcemy przekazać mu obiekt World. Obiekt musi więc zatem istnieć już wcześniej. W innym przypadku odwołamy się tylko do zarezerwowanego obszaru, co robimy na początku klasy. Inaczej mówiąc jest to obszar pusty. Spowoduje to błąd NullPointerException, co znaczy tyle że próbujemy odwołać się do pustego miejsca. Kod wykonuje się linia po linii dlatego kolejność jest taka ważna.

Efekt po uruchomieniu


Udało się dzisiaj przygotować już pewne podstawy pod bohatera. Nie wygląda może jeszcze zbyt przyjaźnie, ale to już tylko kwestia czasu. W następnym wpisie będzie trzeba trochę dostosować odpowiednio do tego wyświetlanie, może udać się wykonać sterowanie tą kuleczką ;d 

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

Tyle na dziś,

Do zobaczenia wkrótce