Kontynuując naukę na temat biblioteki libgdx w niniejszym instruktażu zostaną poruszone podstawowe informacje na temat rysowania tekstur, animacji oraz wykrywania gestów użytkownika.
Zacznijmy więc od stworzenie prostej gierki.
Tworzymy podstawową klasę MyGdxGame która dziedziczy po Game. Będzie to klasa wejściowa za pomocą której będziemy mogli zarządzać ekranami naszej aplikacji. Jej struktura nie różni się w dużym stopniu od każdego ekranu w naszej aplikacji i wygląda następująco:
package com.binaryalchemist.game;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.binaryalchemist.screens.FirstScreen;
public class MyGdxGame extends Game {
@Override
public void create () {
setScreen(new FirstScreen());
}
@Override
public void render () {
super.render();
}
@Override
public void resize(int width, int height) {
super.resize(width, height);
}
@Override
public void pause() {
super.pause();
}
@Override
public void resume() {
super.resume();
}
@Override
public void dispose() {
super.dispose();
}
}
Implementuję ona metody wywoływane w różnym czasie życia aplikacji. Nie wprowadzamy tu wielu zmian jedynie ustawiamy by po stworzeniu został wybrany ekran FirstScreen. W zasadzie moglibyśmy stworzyć jednie ekran dziedziczący po ApplicationListener lecz w takiej formie łatwiej będzie się zarządzało projekt w przyszłości gdy posiadamy większą liczbę ekranów i obiektów miedzy którymi musimy delegować zadania.
Przechodzimy więc do właściwej części instruktarza. Na początku stwórzmy prostą podstawę umożliwiającą wczytanie obrazka wraz z tłem. Tworzymy klasę FirstScreen która dziedziczy po klasie Screen i implementujemy wszystkie niezbędne metody. Następnie tworzymy dwie tekstury reprezentujące tło oraz statek kosmiczny gracza i rysujemy je na ekranie. Kod wygląda następująco:
package com.binaryalchemist.screens;
public class FirstScreen implements Screen{
private Sprite sprite;
private SpriteBatch spriteBatch;
private Texture background;
private Texture spacecraft;
@Override
public void show() {
spriteBatch = new SpriteBatch();
background = new Texture("space.jpg");
spacecraft = new Texture("spacecraft.png");
sprite = new Sprite(background);
sprite.setSize(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
}
@Override
public void render(float delta) {
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
spriteBatch.begin();
sprite.draw(spriteBatch);
spriteBatch.draw(spacecraft,0,0,100f,200f);
spriteBatch.end();
}
@Override
public void resize(int width, int height) {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void hide() {
}
@Override
public void dispose() {
sprite.getTexture().dispose();
spacecraft.dispose();
}
}
Obiekt spriteBatch służy nam do rysowania textur na ekranie. Wszystkie rysowanie obiekty muszą być poprzedzone metodą spriteBatch.begin() oraz zakończone spriteBatch.end(); Obiekty możemy rysować na dwa sposoby. Korzystając z metody draw() obiektu SpriteBatch w której podajemy nazwę zmiennej gdzie zapisana jest tekstura, jej położenie na ekranie oraz wielkość. Innym dogodnym sposobem jest utworzenie obiektu Sprite który w wygodny sposób przechowuje informacje o obiekcie takie jak jego rozmiar czy położenie. Wszystkie tektury muszą na koniec życia aplikacji zostać usunięte aby nie doszło do wycieku pamięci także pozbądźmy się ich w metodzie dispose()
Powinniśmy otrzymać taki oto wynik tego prostego programu:
Zróbmy jednak by było to trochę ciekawsze na początek pozwólmy użytkownikowi na poruszanie naszego statku kosmicznego. Implementujemy więc klasę InputProcessor i poprawiamy kod o następujące elementy:
package com.binaryalchemist.screens;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
public class FirstScreen implements Screen, InputProcessor {
private Sprite sprite;
private SpriteBatch spriteBatch;
private Texture background;
private Texture spacecraft;
public float x=0;
public float y=0;
@Override
public void show() {
spriteBatch = new SpriteBatch();
background = new Texture("space.jpg");
spacecraft = new Texture("spacecraft.png");
sprite = new Sprite(background);
sprite.setSize(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Gdx.input.setInputProcessor(this);
}
@Override
public void render(float delta) {
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
spriteBatch.begin();
sprite.draw(spriteBatch);
spriteBatch.draw(spacecraft,x-(spacecraft.getWidth()/2),y-(spacecraft.getHeight()/2),100f,200f);
spriteBatch.end();
}
@Override
public void resize(int width, int height) {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void hide() {
}
@Override
public void dispose() {
sprite.getTexture().dispose();
spacecraft.dispose();
}
@Override
public boolean keyDown(int keycode) {
return false;
}
@Override
public boolean keyUp(int keycode) {
return false;
}
@Override
public boolean keyTyped(char character) {
return false;
}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
this.x = screenX;
this.y = Gdx.graphics.getHeight()-screenY;
return false;
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
this.x = screenX;
this.y = Gdx.graphics.getHeight()-screenY;
return false;
}
@Override
public boolean touchDragged(int screenX, int screenY, int pointer) {
this.x = screenX;
this.y = Gdx.graphics.getHeight()-screenY;
return false;
}
@Override
public boolean mouseMoved(int screenX, int screenY) {
return false;
}
@Override
public boolean scrolled(int amount) {
return false;
}
}
Klasa InputProcessor służy do nasłuchiwania kliknięć użytkownika czy to pochodzących od klawiatury, myszki czy dotyku w ekranie smartfona. Niniejsze metody uruchamiane są gdy:
public boolean keyDown(int keycode) – gdy użytkownik naciśnie wybrany klawisz na klawiaturze
public boolean keyUp(int keycode) – gdy użytkownik puści wybrany klawisz na klawiaturze
public boolean keyTyped(char character) – gdy wpisany przez użytkownika klawisz zostanie przetworzony
public boolean touchDown(int screenX, int screenY, int pointer, int button) – gdy użytkownik kliknie przycisk na mysce lub dotknie ekranu na smartfonie
public boolean touchUp(int screenX, int screenY, int pointer, int button) – gdy użytkownik puści przycik myszki lub odsunie palec od ekranu
public boolean touchDragged(int screenX, int screenY, int pointer) – gdy użytkownik poruszy myszkę lub przesunie palec na ekranie
public boolean mouseMoved(int screenX, int screenY) – gdy użytkownik poruszy myszkę
public boolean scrolled(int amount) – gdy użytkownik poruszy kółkiem w myszce
W tej aplikacji nadpisaliśmy metody odpowiedzialne jedynie za wykrywanie dotyku na smartfonie. Metody te umożliwiają nam odczytanie położenia palca użytkownika. Wartości te zapisujemy w zmiennych x i y i przekazujemy je do metody draw spriteBatch. Aby umieścić texture w centrum dotyku odejmujemy połowę jej wysokości oraz szerokości.
Zapewne wielu z was zauważyło że przy wyznaczaniu pozycji y odejmujemy wysokość ekranu:
this.y = Gdx.graphics.getHeight()-screenY;
Wynika to z faktu iż koordynaty otrzymywane z dotyku nie są takie same jak koordynaty rysowanych obiektów. (Dla rysowanych obiektów punkt 0,0 znajduję się w lewym dolnym rogu ekranu. Koordynaty z dotyku punkt 0,0 określają jak górny lewy róg ekrany). Można to naprawić w ten sposób bądź skorzystać z klasy camera o której zapewne będzie przyszły instruktaż. Na koncu nie zapomnijmy zarejestrować naszego listenera Gdx.input.setInputProcessor(this);
Powinniśmy być teraz w stanie poruszać naszym statkiem kosmicznym.
W celu uzyskania identycznych rezultatów możliwe że będzie trzeba zmienić w pliku androidmanifest android:screenOrientation na „portrait”
Ostatnią rzeczą jaką chciałem tu poruszyć są animacje. Dzięki którym nasza aplikacja może zyskać wiele życia. Animacje są niczym innym jak pojedynczymi obrazkami wyświetlanymi z odpowiednią prędkością. W celu utworzenia animacji na początek potrzebujemy sprite sheet zawierający wszystkie klatki naszej animacji. Osobiście do tworzenia takich animacji wykorzystuję photoshopa w wersji cs6 która umożliwia tworzenie animacji i zapisanie ich w formie pojedynczych obrazków.
Aby połączyć wszystkie pojedyncze obrazki w całość korzystam z programu ImageMagick (https://www.imagemagick.org/script/index.php). W czasie instalacji zaznaczymy wszystkie dodatki a po niej nie zapomnijmy dodać programu do zmiennych środowiskowych:
MAGICK_HOME="$HOME/ImageMagick-7.0.3"
Teraz możemy wejść do wiersza poleceń i skorzystać z następującego polecenia:
montage -tile 10x7 -mode concatenate -background none "*.png" spacecraftanimation.png
Powyższa komenda scala nam wszystkie obrazki znajdujące się w danym folderze do jednego pliku. Pliki są umieszczane w jednym pliku w 10 kolumnach i 7 wierszach przy zachowaniu przezroczystego tła. Osobiście utworzyłem spritesheet z 64 obrazków, oto otrzymany rezultaty:
Warto przy tym pamiętać ze pojedynczy obrazek png nie powinien przekroczyć rozmiaru 2048×2048 z większymi bowiem wiele urządzeń na androida sobie nie radzi.
Gdy to zrobimy pozostaje nam zaimplementować rozwiązanie w aplikacji:
package com.binaryalchemist.screens;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
public class FirstScreen implements Screen, InputProcessor {
private Sprite sprite;
private SpriteBatch spriteBatch;
private Texture background;
private Texture spacecraft;
private Animation playerAnimation;
private TextureRegion[] playerTextureRegion;
private Texture playerAnimationTexture;
private TextureRegion currentFrame;
float stateTime = 0;
public float x=0;
public float y=0;
@Override
public void show() {
spriteBatch = new SpriteBatch();
background = new Texture("space.jpg");
spacecraft = new Texture("spacecraft.png");
sprite = new Sprite(background);
sprite.setSize(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Gdx.input.setInputProcessor(this);
playerAnimationTexture = new Texture("spacecraftanimation.png");
playerTextureRegion = createRegion(playerTextureRegion, playerAnimationTexture, 100, 200, 3, 10);
playerAnimation = new Animation(0.1f, playerTextureRegion);
currentFrame = new TextureRegion();
}
@Override
public void render(float delta) {
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
stateTime += Gdx.graphics.getDeltaTime();
currentFrame = playerAnimation.getKeyFrame(stateTime, true);
spriteBatch.begin();
sprite.draw(spriteBatch);
spriteBatch.draw(currentFrame,x-(spacecraft.getWidth()/2),y-(spacecraft.getHeight()/2),100f,200f);
spriteBatch.end();
}
@Override
public void resize(int width, int height) {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void hide() {
}
@Override
public void dispose() {
sprite.getTexture().dispose();
spacecraft.dispose();
playerAnimationTexture.dispose();
}
@Override
public boolean keyDown(int keycode) {
return false;
}
@Override
public boolean keyUp(int keycode) {
return false;
}
@Override
public boolean keyTyped(char character) {
return false;
}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
this.x = screenX;
this.y = Gdx.graphics.getHeight()-screenY;
return false;
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
this.x = screenX;
this.y = Gdx.graphics.getHeight()-screenY;
return false;
}
@Override
public boolean touchDragged(int screenX, int screenY, int pointer) {
this.x = screenX;
this.y = Gdx.graphics.getHeight()-screenY;
return false;
}
@Override
public boolean mouseMoved(int screenX, int screenY) {
return false;
}
@Override
public boolean scrolled(int amount) {
return false;
}
public static TextureRegion[] createRegion(TextureRegion[] outputRegion, Texture texture, int sizeX, int sizeY, int row, int column){
TextureRegion[][] tmp = TextureRegion.split(texture, sizeX, sizeY);
outputRegion = new TextureRegion[row*column];
int index = 0;
for (int i = 0; i < row; i++) {
for (int j = 0; j < column; j++) {
outputRegion[index++] = tmp[i][j];
}
}
return outputRegion;
}
}
Tworzymy cztery obiekty są nimi:
private Animation playerAnimation – obiekt odpowiedzialny za wyświetlanie animacji
private TextureRegion[] playerTextureRegion – tablica przechowująca wszystkie pojedyncze obszary (obrazki) z spritesheata;
private Texture playerAnimationTexture – Textura przechowująca cały spritesheet (tutaj plik png);
private TextureRegion currentFrame – aktualnie wyświetlany obszar;
Aby podzielić plik png na pojedyncze obszary można stworzyć taką metodę jak createRegion() Przyjmuję ona kilka parametrów.
outputRegion – zmienna do której zapisujemy wyniki
texture – textura którą dzielimy na pojedyncze obszary
sizeX – szerokość pojedynczego obszaru
sizeY – wysokość pojedynczego obszaru
int row – liczba wierszy w teksturze
int column – liczba kolumn w teksturze
Na koniec pozostaje wyświetlenie samej animacji w tym celu została utworzona zmienna stateTime. Zmienna ta monitoruję czas jak upłyną w aplikacji. Dzięki niej wiemy iż jeśli minęły już np 5 sekund z 30 sekundowej animacji to musimy wybrać 5 obszar z tablicy. Co też robimy w zmiennej
currentFrame = playerAnimation.getKeyFrame(stateTime, true);
Wartość true mówi nam iż animacja powinna zostać zapętlona. Aktualnie wybrany obszar rysujemy poprzez spriteBatch.draw(currentFrame,x-(spacecraft.getWidth()/2),y-(spacecraft.getHeight()/2),100f,200f);
Po wszystkich krokach powinniśmy otrzymać taki oto rezultat.