Zabawa klockami – tworzymy UI

W tym wpisie zajmiemy się utworzeniem interfejsu użytkownika, tworząc nowy wygląd menu, ekranu kończącego rozgrywkę oraz ekranu z autorem gry. Do tego celu wykorzystamy zaimplementowaną klasę Scene2d.ui, do wykorzystywanej przez nas biblioteki libGDX. Klasa ta, daje możliwość tworzenia UI, w bardzo prosty sposób. Jednak przed rozpoczęciem tworzenia interfejsu, musimy zadbać o prawidłową konfigurację omawianej dziś klasy.

Konfiguracja

Do poprawnego działania Scene2d.ui, potrzebne są pliki uiskin.png, uiskin.atlas, uiskin.json, default.png i default.fnt. Pliki te znajdziemy w narzędziach jakie dostarcza nam libGDX (Przy konfiguracji projektu powinniśmy mieć zaznaczoną opcję Tools). Wystarczy otworzyć (W przypadku Intellji) External Libraries/gdx-tools-1.9.5, przekopiować interesujące nas pliki do naszego projektu /core/assets/… . Ta operacja wystarczy do prawidłowego działania Scene2d.ui.

Dodatkowo narzędzia dostarczone nam wraz z libGDX, zawierają w sobie program do generowanie czcionek w formacie .fnt oraz pliku graficznego .png. Program ten znajdziemy przechodząc odpowiednio przez katalogii External Libraries/gdx-tools-1.9.5/com.badlogic.gdx/tools/hiero/Hiero.java. Uruchamiając ten plik wyskoczy nam okienko z wyborem czcionki.

Aby wyeksportować pliki, należy kliknąć na górze okienka File/Save BMFont Files. Program poza wczytanymi czcionkami z systemu, umożliwia także import z pliku.

Przedstawiona konfiguracja umożliwiła działanie klasie Scene2d.ui, jednak nadal nie wiemy, czym są skopiowane przez nas pliki. Teraz zajmiemy się omówieniem każdego z nich.

default.fnt – to czcionka z jakiej będziemy korzystać przy tworzeniu interfejsu użytkownika.

default.png – czcionka zaprezentowana w postaci pliku graficznego z rozszerzeniem .png (nie jest konieczna do działania UI).

uiskin.png – mapa plików graficznych, wykorzystywanych przez elementy interfejsu np. przez Button.

uiskin.atlas – plik odpowiedzialny za opis mapy graficznej uiskin.png.

uiskin.json – główny plik konfiguracyjny, odpowiada za połączenie pozostałych plików w jedną całość. Zapisany jest w formacie .JSON, znanym dla programistów JS.

Pliki graficzne

Do wykonania tego elementu gry, przygotowałem pliki graficzne, które za chwile wykorzystamy tworząc nowy wygląd gry. Sceny jakie powstaną, są proste pod względem graficznym, ale zostały zaprogramowane w sposób zgodny z biblioteką libGDX.

 

Implementacja

Menu

Przechodząc już do implementacji naszego nowego interfejsu, w klasie Menu.java, tworzymy nowe obiekty.

private Stage stage;
private Skin skin; 
private Texture texture; 
private ImageButton playButton; 
private ImageButton exitButton; 
private ImageButton authorButton; 
private Image image; 
private Drawable drawable;

stage odpowiadał będzie za zbieranie aktorów, czyli kontrolek do jednego zbioru i wyświeltanie ich na ekranie.

skin importuje plik konfiguracyjny naszego UI, zapisany z rozszerzeniem .json.

texture zmienna ta odpowiada za tymczasowe przechowywanie pliku graficznego danego elemetnu.

playButton, exitButton, authorButton są przyciskami jakie wykorzystujemy w naszym menu.

image to obrazek z logiem gry.

drawable – zmienna ta umożliwia przekazanie obrazka do konstruktora klasy ImageButton.

stage = new Stage(); 
skin = new Skin(Gdx.files.internal("uiFile/uiskin.json"));

Następuje inicjalizacja zmiennej stage oraz skin. Jak możemy zauważyć, konstruktor Skin() jako parametr przyjmuje uchwyt pliku konfiguracyjnego uiskin.json.

Teraz, możemy przystąpić do utworzenia pierwszego przycisku.

texture = new Texture("ui/play.png"); 
Drawable drawable = new TextureRegionDrawable(new TextureRegion(texture)); 
playButton = new ImageButton(drawable); 
playButton.setPosition(Gdx.graphics.getWidth() / 2 - playButton.getWidth() / 2, Gdx.graphics.getHeight() * 0.5f);

Inicjalizujemy zmienną texture, podając w argumencie ścieżkę do pliku graficznego. Kolejnym krokiem jest utworzenie zawartości dla zmiennej drawable. Następnie, inicjalizujemy zmienną naszego przycisku playButton, podając w parametrze, wcześniej zainicjalizowaną zmienną drawable. Pozostało nam jeszcze ustalenie pozycji na ekranie naszej nowej kontrolki, wykorzystujemy do tego metodę setPosition(), która jako parametry, przyjmuje połowę szerokości okna gry, pomniejszoną o połowę wielkości grafiki oraz połowę wysokości okna. Dzięki tak podanym parametrom nasz przycisk znajduję się na środku ekranu.

Teraz powtarzamy ten sam kod dla exitButton i authorButton.

texture = new Texture("ui/author.png"); 
drawable = new TextureRegionDrawable(new TextureRegion(texture)); 
authorButton = new ImageButton(drawable); 
authorButton.setPosition(Gdx.graphics.getWidth() / 2 - authorButton.getWidth() / 2, Gdx.graphics.getHeight() * 0.4f);
    texture = new Texture("ui/exit.png");
    drawable = new TextureRegionDrawable(new TextureRegion(texture));
    exitButton = new ImageButton(drawable);
    exitButton.setPosition(Gdx.graphics.getWidth() / 2 - exitButton.getWidth() / 2, Gdx.graphics.getHeight() * 0.3f);

Pozostała nam jeszcze zmienna image, która będzie wyświetlać logo gry na górze menu.

texture = new Texture("ui/UWar.png"); 
image = new Image(texture); 
image.setPosition(Gdx.graphics.getWidth() / 2 - image.getWidth() / 2, Gdx.graphics.getHeight() * 0.8f);

Przekazujemy plik graficzny do zmiennej texture. Następnie przy pomocy wczytanej grafiki inicjalizujemy zmienną image. Ustawiamy pozycję w podobny sposób jak miało to miejsce w przypadku przycisku Play.

Jak wiemy przyciski muszą wykonywać określoną akcję po naciśnięciu. Teraz zajmiemy się oprogramowaniem każdego z przycisków.

        playButton.addListener(new ChangeListener(){
            public void changed(ChangeEvent event, Actor actor){
                dispose();
                game.setScreen(new Game(game));
            }
        });

Dla playButton korzystamy z metody nasłuchującej addListener(), która w parametrze przyjmuje nowy obiekt odpowiedzialny za zmianę stanu przycisku. Publiczna metoda changed(), wykonuje czyszczenie pamięci za pomocą metody dispose() oraz wczytanie nowego okna, w tym wypadku okna z grą.

W podobny sposób oprogramowujemy każdy z przycisków, wczytując odpowiednie screeny.

        authorButton.addListener(new ChangeListener(){
            public void changed(ChangeEvent event, Actor actor){
                    dispose();
                game.setScreen(new Author(game));
            }
        });
    exitButton.addListener(new ChangeListener(){
        public void changed(ChangeEvent event, Actor actor){
            dispose();
            Gdx.app.exit();
        }
    });

Abyśmy mogli zobaczyć zmiany na ekranie musimy, każdy z elementów dodać do zmiennej stage.

stage.addActor(playButton); 
stage.addActor(exitButton); 
stage.addActor(authorButton); 
stage.addActor(image);
Gdx.input.setInputProcessor(stage);

Ostatnią rzeczą jaką mamy zrobić to rysowanie stage. Zatem w metodzie render() dodajemy linijkę odpowiedzialną za wyświetlanie wszystkich zgromadzonych elementów (kontrolek) w stage.

stage.draw();

Autor

Na chwilę obecną scena Autor, będzie zawierała dwa napisy oraz przycisk powracający do menu.

Zatem utwórzmy nową klasę Author.java i dodajmy w niej pola.

private Stage stage;
private Skin skin;
private Texture texture;
private Label author;
private Label me;
private Drawable drawable;
private ImageButton backButton;

Sytuacja ma się bardzo podobnie do utworzonego wcześniej menu, dlatego nie będę już tak dokładnie omawiał linijki po linijce. Jak widzimy mamy dwa pola klasy Label, będą to nasze napisy.

Następnie zajmujemy się inicjalizacją nowych zmiennych.

    stage = new Stage(); skin = new Skin(Gdx.files.internal("uiFile/uiskin.json"));
    author = new Label("Author", skin);
    author.setPosition(Gdx.graphics.getWidth() / 2 - author.getWidth() / 2, Gdx.graphics.getHeight() * 0.7f);
    me = new Label("Mariusz Bugajski", skin);
    me.setPosition(Gdx.graphics.getWidth() / 2 - me.getWidth() / 2, Gdx.graphics.getHeight() * 0.5f);

Jak możemy zobaczyć w powyższym kodzie, inicjalizujemy w taki sam sposób pola, stage oraz skin. Po czym zajmujemy się utworzeniem pierwszego napisu. Konstruktor Label przyjmuje dwa argumenty, pierwszy to napis jak ma zawierać, natomiast drugi to zmienna klasy Skin, odpowiedzialna za przechowywanie informacji o wyglądzie naszego UI. Musimy jeszcze ustawić pozycję na ekranie naszych napisów, domyślnie znajduję się w lewym dolnym rogu (punkt 0,0).

Przycisk tworzymy w ten sam sposób jak robiliśmy to tworząc menu.

texture = new Texture("ui/back.png"); 
drawable = new TextureRegionDrawable(new TextureRegion(texture)); 
backButton = new ImageButton(drawable); 
backButton.setPosition(Gdx.graphics.getWidth() / 2 - backButton.getWidth() / 2, Gdx.graphics.getHeight() * 0.3f);
backButton.addListener(new ClickListener(){
        @Override
        public void clicked(InputEvent event, float x, float y){
            dispose();
            game.setScreen(new Menu(game));
        }
});

Pozostało dodać kontrolki do zmiennej stage oraz wyświetlić pole na ekranie w metodzie render().

stage.addActor(author); 
stage.addActor(me); 
stage.addActor(backButton); 
Gdx.input.setInputProcessor(stage);

Metoda render()

stage.draw();

Ekran końca rozgrywki

Ostatnim elementem UI jakim się teraz zajmiemy jest ekran kończący rozgrywkę. Jego zadaniem jest pokazanie wyniku jaki został osiągnięty przez gracza oraz danie możliwość wyjścia i lub ponownej gry.

Zatem tym razem nasz screen będzie miał dwa przyciski, obrazek oraz napis. Obrazek z logiem gry, zaś napis będzie przedstawiał ilość punktów zdobytych przez gracza.

Przechodzimy zatem do pliku End.java, dodając na początek nowe pola. private Stage stage; private Skin skin; private Texture texture; private Label label; private Image image; private Drawable drawable; private ImageButton againButton; private ImageButton exitButton;

Inicjalizujemy zmienne w taki sam sposób jak wcześniej.

stage = new Stage(); 
skin = new Skin(Gdx.files.internal("uiFile/uiskin.json"));
    texture = new Texture("ui/UWar.png");
    image = new Image(texture);
    image.setPosition(Gdx.graphics.getWidth() / 2 - image.getWidth() / 2, Gdx.graphics.getHeight() * 0.8f);

    label = new Label(player.getScore() + "", skin);
    label.setPosition(Gdx.graphics.getWidth() / 2, Gdx.graphics.getHeight() / 2);

    texture = new Texture("ui/play.png");
    drawable = new TextureRegionDrawable(new TextureRegion(texture));
    againButton = new ImageButton(drawable);
    againButton.setPosition(Gdx.graphics.getWidth() * 0.2f, Gdx.graphics.getHeight() * 0.3f);

    texture = new Texture("ui/exit.png");
    drawable = new TextureRegionDrawable(new TextureRegion(texture));
    exitButton = new ImageButton(drawable);
    exitButton.setPosition(Gdx.graphics.getWidth() * 0.65f, Gdx.graphics.getHeight() * 0.3f);

    againButton.addListener(new ChangeListener(){
        public void changed(ChangeEvent event, Actor actor){
            dispose();
            game.setScreen(new Game(game));
        }
    });

    exitButton.addListener(new ChangeListener(){
        public void changed(ChangeEvent event, Actor actor){
            dispose();
            Gdx.app.exit();
        }
    });

Jak widzimy konstruktor Label przyjmuje wynik, przekazany w konstruktorze klasy End, przez zmienną typu Player. Cała reszta wygląda podobnie do omawianych wcześniej scen.

Dodajemy elementy wyglądu do stage.

stage.addActor(label); 
stage.addActor(image); 
stage.addActor(againButton); 
stage.addActor(exitButton); 
Gdx.input.setInputProcessor(stage);

Wyświetlamy w metodzie render()

stage.draw();

Efekty pracy

Podsumowanie

Stworzyliśmy trzy ekrany, które umożliwiają nam poruszanie się po grze, bez konieczności znania skrótów klawiszowych, jak to było dotychczas. Do utworzenia scen wykorzystaliśmy dostarczony z biblioteką libGDX moduł odpowiedzialny za tworzenie UI. Dowiedzieliśmy się jak go skonfigurować w projekcie oraz użyliśmy gotowych komponentów takich jak przyciski, napisy, obrazki. Te elementy interfejsu pozwoliły nam na stworzenie całego menu.

Pozdrawiam,

sirmarbug

Podziel się ze znajomymi