Tworzenie gier w Javie – Analiza pierwszej aplikacji

LibGDX dostarcza nam bogaty zestaw klas i funkcjonalności, które znacznie ułatwiają tworzenie gier w języku Java. W tym kursie zaprezentuję wiele z tych klas i pokażę Ci, jak wykorzystać je do pisania gier. W tym rozdziale omówimy sobie kod programu wygenerowanego w poprzednim rozdziale.

Nasz pierwszy projekt korzystający z LibGDX posiada klasę o nazwie FirstGdxApp. Jest ona bardzo krótka, a zawarty w niej, domyślnie wygenerowany kod, wczytuje i wyświetla obrazek na czerwonym tle.

Przyjrzymy się teraz poszczególnym elementom tej klasy, poznając przy okazji podstawy korzystania z LibGDX.

rozdzial-02/pierwszy-projekt-libgdx/desktop/src/com/kursjava/gamedev/FirstGdxApp.java
public class FirstGdxApp extends ApplicationAdapter {
  SpriteBatch batch;
  Texture img;

  @Override
  public void create () {
    batch = new SpriteBatch();
    img = new Texture("badlogic.jpg");
  }

  @Override
  public void render () {
    Gdx.gl.glClearColor(1, 0, 0, 1);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    batch.begin();
    batch.draw(img, 0, 0);
    batch.end();
  }

  @Override
  public void dispose () {
    batch.dispose();
    img.dispose();
  }
}
Ten rozdział nie ma osobnego przykładu w repozytorium z przykładami – analizujemy program wygenerowany w rozdziale drugim.

Klasa bazowa – ApplicationAdapter

Każda klasa główna aplikacji korzystającej z LibGDX powinna implementować interfejs ApplicationListener lub rozszerzać klasę, która go implementuje. Interfejs ten definiuje zestaw metod, które LibGDX wywołuje na rzecz naszego programu w różnych momentach jego wykonywania. Są to m. in. widoczne powyżej metody:

  • create – wywoływana jednorazowo podczas uruchamiania naszego programu – jest to dobre miejsce na inicjalizację naszej gry,
  • render – wywoływana kilkadziesiąt razy na sekundę metoda, w której powinniśmy aktualizować stan i rysować świat gry,
  • dispose – wywoływana, gdy program ma zakończyć działanie, a my powinniśmy zwolnić zasoby, np. wczytane tekstury (obrazki).

Nasza klasa główna FirstGdxApp dziedziczy po klasie ApplicationAdapter, która z kolei implementuje wszystkie metody z interfejsu ApplicationListener. Te zaimplementowane metody mają puste ciała, dzięki czemu w klasach rozszerzających ApplicationAdapter możemy zawrzeć nasze implementacje tylko tych metod, które są dla nas istotne.

Metoda create i tworzone w niej obiekty

Metoda create wywoływana jest jednorazowo na rzecz naszej głównej klasy. W przypadku powyższego programu, tworzone są w niej dwa obiekty:

@Override
public void create () {
  batch = new SpriteBatch();
  img = new Texture("badlogic.jpg");
}

Obiekty te zdefiniowane są w następujący sposób:

SpriteBatch batch;
Texture img;

Texture to klasa, która przechowuje dane obrazków, czyli tekstur, które będziemy wyświetlać na ekranie. Jak widzisz, wczytanie tekstury z pliku jest bardzo proste – wystarczy w konstruktorze przekazać ścieżkę do pliku. Domyślnie LibGDX szuka zasobów, takich jak tekstury, w katalogu core/assets.

Od tej pory będę stosował pojęcie tekstura, gdy będę omawiał rysowanie na ekranie obrazów graficznych.

Drugi z tworzonych obiektów to SpriteBatch. Obiekt tego typu stosujemy do wyświetlania tekstur na ekranie. Optymalizuje on proces rysowania, gdy chcemy wyświetlić wiele fragmentów tej samej tekstury. Przydaje się to w przypadku, gdy używamy jednego dużego pliku zawierającego wiele mniejszych obrazków – zobaczymy takie przypadki w dalszej części kursu. Zaletą takiego podejścia jest mniejsza liczba przełączeń pomiędzy teksturami, która jest czasochłonna, oraz mniejsza liczba zasobów, które musimy wczytywać, przechowywać, oraz po których zwalniamy pamięć.

Metoda render

W metodzie render rysujemy świat naszej gry, a także aktualizujemy jego stan:

@Override
public void render () {
  Gdx.gl.glClearColor(1, 0, 0, 1);
  Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
  batch.begin();
  batch.draw(img, 0, 0);
  batch.end();
}

W przypadku naszego pierwszego wygenerowanego programu nie ma żadnej logiki, więc jedynym zadaniem metody render jest na razie rysowanie. Powyższy kod ma za zadanie:

  • zamalować cały ekran na czerwono:
    Gdx.gl.glClearColor(1, 0, 0, 1);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
  • powiadomić obiekt batch, że chcemy rozpocząć rysowanie:
    batch.begin();
    
  • przekazać teksturę do wyświetlenia i lokalizację w oknie programu, w którym ma ona zostać narysowana:
    batch.draw(img, 0, 0);
    
  • powiadomić obiekt batch, że zakończyliśmy rysowanie:
    batch.end();
    

Rysowanie świata gry odbywa się zazwyczaj w ten sposób, że najpierw zamalowujemy ekran na pewien kolor, a następnie rysujemy elementy gry, które są w tej chwili widoczne na ekranie, z uwzględnieniem, że mogły się one przesunąć od poprzedniego rysowania.

Aby zamalować ekran na pewien kolor, ustawiamy go za pomocą metody glGlearColor obiektu gl, do którego dostęp mamy za pomocą klasy Gdx. Obiekt gl tworzony jest dla naszej wygody przez bibliotekę LibGDX, podobnie jak kilka innych obiektów, np. obiekt klasy służącej do obsługi klawiatury i myszki, którego wkrótce zaczniemy używać.

Kolor podajemy jako cztery znormalizowane wartości czerwonej, zielonej, oraz niebieskiej składowej koloru. Często kolory opisane w ten sposób nazywamy kolorami RGB, czyli posiadającymi składową Red, Green, oraz Blue. Czwarta wartość to kanał alpha, oznaczająca przezroczystość. Znormalizowane wartości to wartości z zakresu od 0.0 do 1.0.

Podanie wartości 0.0 oznacza, że danej składowej koloru ma nie być w ogóle, a wartość 1.0 oznacza maksymalną wartość natężenia tego koloru. W ten sposób mamy do dyspozycji bardzo szeroką paletę kolorów. Ustawiając odpowiednio trzy pierwsze wartości możesz stworzyć kolor, który Cię interesuje.

Mogłeś się wcześniej spotkać z ustawianiem wartości koloru przez podanie trzech wartości z zakresu od 0 do 255. Ponieważ LibGDX pod spodem korzysta z biblioteki graficznej OpenGL, a OpenGL często używa właśnie znormalizowanych wartości z zakresu od 0.0 do 1.0, LibGDX także przyjął taką formę ustawiania wartości.

OpenGL to darmowa, wieloplatformowa biblioteka służąca do obsługi grafiki. To ona jest "pod spodem" odpowiedzialna za wyświetlanie grafiki.

Jeżeli masz kolor opisany za pomocą trzech wartości z zakresu od 0 do 255, to łatwo możesz je znormalizować dzieląc je przez 255. Dla przykładu, kolor opisany za pomocą 255, 128, 20 po znormalizowaniu w ten sposób będzie miał wartości 1.0, 0.50, 0.078.

Przekazując ułamkowe wartości kolorów do metody glClearColor musisz zastosować literę f na końcu każdej liczby. Dzięki temu, Java będzie traktować wpisaną przez Ciebie liczbę jako liczbę typu float. Domyślnie, liczby zmiennoprzecinkowe w języku Java są typu double, który ma większy zakres, niż typ float. Metoda glClearColor oczekuje argumentów typu float, dlatego musisz po każdej liczbie dodać f (o ile ma ona część ułamkową) Przykład:

Gdx.gl.glClearColor(1.0f, 0.50f, 0.78f, 1);

Poniżej znajdziesz kilka przykładowych kolorów i wartości RGB, które im odpowiadają:

Znormalizowane RGB RGB Kolor
1.0, 1.0, 1.0 255, 255, 255
1.0, 0.0, 0.0 255, 0, 0
0.0, 1.0, 0.0 0, 255, 0
0.0, 0.0, 1.0 0, 0, 255
0.0, 0.0, 0.0 0, 0, 0
0.698, 0.698, 0.698 178, 178, 178
0, 1.0, 1.0 0, 255, 255
0.8, 0.6, 1 204, 153, 255
0.6, 0.4, 0.2 153, 102, 51

Po ustawieniu koloru czyścimy ekran za pomocą metody glClear. Następnie, za pomocą obiektu batch przystępujemy do rysowania.

Obiekt batch musi być powiadomiony, kiedy chcesz zacząć i zakończyć rysowanie na ekranie. W tym celu wywołujesz metodę begin, a po zakończeniu rysowania, metodę end.

Między wywołaniem obu tych metod, za pomocą metody draw, informujemy obiekt batch co i gdzie na ekranie chcemy narysować:

batch.draw(img, 0, 0);

Obiekt img to tekstura z obrazkiem wczytanym w omawianej wcześniej metodzie create. Kolejne argumenty oznaczają, gdzie ma się rozpocząć rysowanie tekstury, a dokładniej – gdzie przypada jej lewy, dolny wierzchołek. Pozycja 0, 0 oznacza dolny, lewy róg okna naszego programu. Jest to bardzo ważna cecha aplikacji pisanych z wykorzystaniem LibGDX – układ współrzędnych zgodny jest z tym, który znamy z lekcji matematyki – wartości X zwiększają się w „prawą” stronę patrząc od lewej krawędzi okna, a wartości Y – "w górę" okna, zaczynając od jego dolnej krawędzi:

Początek układu współrzędnych w aplikacjach LibGDX
Początek układu współrzędnych w aplikacjach LibGDX

(zmieniłem w powyższym oknie tło na białe, aby nie raziło w oczy)

Musimy pamiętać, że początek układu współrzędnych jest w lewym, dolnym rogu okna, bo to względem tego punktu będziemy ustalać położenie obiektów w naszych grach. Istotne jest jeszcze to, że pozycje obiektów, które będziemy rysować w oknie gry, będą także oznaczały ich lewy, dolny wierzchołek. Zaraz zobaczymy kilka przykładów rysowania tekstur w różnych miejscach na ekranie.

Powyższe informacje są dla nas bardzo istotne, ponieważ pozycja kursora myszy na ekranie określana jest przy innym założeniu – wartości Y rosną nie z dołu ekranu na górę, lecz z góry na dół. Twórcy LibGDX postanowili, aby współrzędne ekranu były spójne z OpenGL oraz z układem współrzędnych kartezjańskich (czyli tych z lekcji matematyki). W związku z tym, jak zobaczymy w jednej z przykładowych gier tworzonych w ramach tego kursu, będziemy musieli wziąć tę rozbieżność pod uwagę, gdy będziemy uzależniali działanie naszej gry od położenia kursora myszy.

Pozycja Y kursora myszy ustalana jest inaczej (rośnie w dół ekranu), ponieważ takie założenie przyjęli twórcy m. in. systemu Windows.

Metoda dispose

Zasoby wykorzystywane w grach mogą zajmować dużo pamięci – wczytanie wielu tekstur i dźwięków może szybko zapełnić pamięć, którą mają do dyspozycji nasze gry. Wiele z obiektów tworzonych w ramach wykorzystania LibGDX posiada metodę dispose, którą należy wywołać na rzecz tych obiektów, gdy nie są one już potrzebne. Wspomoże to proces zwalniania pamięci przez naszą aplikację.

Klasa główna naszej aplikacji może dostarczyć własną implementację metody dispose, którą LibGDX wywoła zaraz przed zakończeniem działania naszego programu. Jest to dobre miejsce, aby zwolnić zasoby naszej aplikacji, wywołując na nich ich własne metody dispose. Nasz pierwszy program posiada dwa takie zasoby: batch typu SpriteBatch, oraz img typu Texture:

@Override
public void dispose () {
 batch.dispose();
 img.dispose();
}

Ustawienia okna gry

Rozmiar okna oraz jego tytuł i inne parametry związane z wyświetlaniem grafiki będziemy ustawiać w klasie DesktopLauncher.

Klasa ta wygenerowana została w katalogu desktop\src\com\kursjava\gamedev\desktop i wygląda następująco:

rozdzial-02/pierwszy-projekt-libgdx/desktop/src/com/kursjava/gamedev/desktop/DesktopLauncher.java
package com.kursjava.gamedev.desktop;

import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import com.kursjava.gamedev.FirstGdxApp;

public class DesktopLauncher {
 public static void main (String[] arg) {
  LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
  new LwjglApplication(new FirstGdxApp(), config);
 }
}

Jak wspominałem w rozdziale, w którym generowaliśmy pierwszy projekt, klasa ta odpowiedzialna jest za uruchomienie naszej gry i konfigurację biblioteki LibGDX.

Za działanie desktopowej aplikacji LibGDX odpowiedzialny jest obiekt klasy LwjglApplication, który oczekuje argumentów: klasy głównej naszej gry oraz konfiguracji będącej obiektem klasy LwjglApplicationConfiguration, która na razie jeszcze nic nie konfiguruje.

Utworzenie obiektu klasy LwjglApplication powoduje rozpoczęcie działania naszego programu.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Nie musisz podawać swojego imienia, e-mailu, ani strony www, aby opublikować komentarz. Komentarze muszą zostać zatwierdzone, aby były widoczne na stronie.