Teeme läbi mõned lihtsad näited. See materjal on loodud inglise keelsete juhendite baasil.

Loome uue projekti libGDX setup tööriistaga. Kui soovid järgnevaid koodijuppe muutmata kopeerida, siis veendu, et paketi nimi ja mänguklass oleksid samasugused nagu pildil:

Projekti loomine

Intellij’s projekti avamiseks:

File -> Open -> leia kataloog, mille seadsid just destination’iks -> vali seest build.gradle fail -> OK -> Open as project.

Gradle asukoht

Mänguaknale kuvamine

Ava projektis \core\src\ee.taltech.iti0301.libgdxdemo\libgdxDemo.java klass.

Kopeeri sellesse järgnev kood ja käivita klassis desktop\src\ee\taltech\iti0301\libgdxdemo\DesktopLauncher.java asuv main meetod, et joonistada ring:

package ee.taltech.iti0301.libgdxdemo;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;

public class libgdxDemo extends ApplicationAdapter {
	ShapeRenderer shapeRenderer;

	@Override
	public void create () {
		shapeRenderer = new ShapeRenderer();
	}

	@Override
	public void render () {
		Gdx.gl.glClearColor(.25f, .25f, .25f, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

		shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
		shapeRenderer.setColor(0, 1, 0, 1);
		shapeRenderer.circle(200, 100, 75);
		shapeRenderer.end();
	}

	@Override
	public void dispose () {
		shapeRenderer.dispose();
	}
}

Create() kutsutakse välja vaid ühe korra, kui mäng käima pannakse.

Render() kutsutakse välja mitu korda sekundis, et muuta, mida mänguaknale kuvatakse.

Dispose() kutsutakse välja, kui mängust lahkutakse.

ShapeRenderer klassi kasutame, et saada ligipääs lihtsatele joonistamisfunktsioonidele.

Nähtavasti määrame tausta värvuse, kujundi tüübi (seest täidetud ehk Filled), kujundi värvuse, kujundi liigi (ring) koos selle koordinaatide ja raadiusega.

Pane tähele, et (0; 0) koordinaat asub libGDX puhul ekraani (mänguakna) alumises vasakus nurgas!

Joonistatud ring

Animatsioonid

  1. Hoiustame stseeni kirjeldavaid olekuid muutujates
  2. Kasutame neid iga kaadri puhul stseeni uuesti joonistamiseks
  3. Uuendame muutujate väärtusi ajas, et viia stseenil läbi muudatusi

Siinpuhul circleX ja circleY hoiavad endas ringi koordinaate ning xSpeed ja ySpeed hoiavad ringi horisontaalset/vertikaalset kiirust. Kui ringi keskpunkt jõuab mänguakna serva, siis muudetakse vastav kiirus vastassuunaliseks, et ring alati mänguakna sisse jääks.

Täiendatud kood:

package ee.taltech.iti0301.libgdxdemo;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;

public class libgdxDemo extends ApplicationAdapter {
	ShapeRenderer shapeRenderer;

	float circleX = 200;
	float circleY = 100;

	float xSpeed = 2;
	float ySpeed = 1;

	@Override
	public void create () {
		shapeRenderer = new ShapeRenderer();
	}

	@Override
	public void render () {

		circleX += xSpeed;
		circleY += ySpeed;

		if (circleX < 0 || circleX > Gdx.graphics.getWidth()) {
			xSpeed *= -1;
		}

		if (circleY < 0 || circleY > Gdx.graphics.getHeight()) {
			ySpeed *= -1;
		}

		Gdx.gl.glClearColor(.25f, .25f, .25f, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

		shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
		shapeRenderer.setColor(0, 1, 0, 1);
		shapeRenderer.circle(circleX, circleY, 75);
		shapeRenderer.end();
	}

	@Override
	public void dispose () {
		shapeRenderer.dispose();
	}
}

Liikuv ring

Delta Time

Koodil on üks puudus - me ei saa määrata, kui tihti render() meetodit välja kutsutakse. Nii töötaks mäng aeglasemalt või kiiremini olenevalt seadmest, mille peal seda jooksutatakse, ehk täpsemalt selle seadme kaadrisagedusest (frame rate). Mäng peaks aga töötama samal kiirusel olenemata seadmest.

Selleks peab ringi liikumine sõltuma ajast, mis on möödunud viimasest kaadrist. Kui on möödunud väga vähe aega, peaks ka ring olema liikunud väga vähe. Kui aga on möödunud rohkem aega, peaks ring olema liikunud kaugemale.

LibGDX pakub lahendusena Gdx.graphics.getDeltaTime() meetodit, mis tagastab viimasest kaadrist möödunud aja. Tähistab protsenti ühest sekundist, nt 0.5 on pool sekundit.

Paneme xSpeed ja ySpeed tähistama vahemaad (pikslites), mis liigutakse sekundis:

float xSpeed = 120;
float ySpeed = 60;

Nüüd peaks ring liikuma horisontaalselt 120 pikslit sekundis, vertikaalselt 60 pikslit sekundis.

Korrutame need väärtused aja muuduga, et saada läbitav vahemaa praeguse kaadri jaoks:

circleX += xSpeed * Gdx.graphics.getDeltaTime();
circleY += ySpeed * Gdx.graphics.getDeltaTime();

Nüüd liigub ring sama kiirusega, olenemata seadme kaadrisagedusest.

Pollimine

Kasutaja sisendit saab kõige lihtsamini kätte sisendseadmete pollimisega. Mõned olulisemad meetodid:

  • Gdx.input.isTouched() tagastab true kui kasutaja klõpsab hiirega aknale. Töötab ka puutetundlike ekraanide puhul
  • Gdx.input.getX() tagastab hiireklõpsu või ekraanipuudutuse x-koordinaadi
  • Gdx.input.getY() tagastab hiireklõpsu või ekraanipuudutuse y-koordinaadi. NB! OpenGL puhul on y-koordinaadi väärtus 0 hoopis ekraani alumises servas, mistõttu tuleb korrektseks asukoha määramiseks lahutada hiireklõpsu y-koordinaat akna kõrgusest: Gdx.graphics.getHeight() - Gdx.input.getY()
  • Gdx.input.isKeyPressed() tagastab, kas etteantud klaviatuuri klahvi vajutatakse. Näiteks Gdx.input.isKeyPressed(Input.Keys.W) kontrollib, kas W klahvi hoitakse praegu all.

Rohkem infot Wikis

Asenda kood järgnevaga ja proovi nii hiire klõpsu kui ka WASD klahve:

package ee.taltech.iti0301.libgdxdemo;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;

public class libgdxDemo extends ApplicationAdapter {
	ShapeRenderer shapeRenderer;

	float circleX = 200;
	float circleY = 100;

	@Override
	public void create() {
		shapeRenderer = new ShapeRenderer();
	}

	@Override
	public void render() {

		if (Gdx.input.isTouched()) {
			circleX = Gdx.input.getX();
			circleY = Gdx.graphics.getHeight() - Gdx.input.getY();
		}

		if(Gdx.input.isKeyPressed(Input.Keys.W)){
			circleY++;
		}
		else if(Gdx.input.isKeyPressed(Input.Keys.S)){
			circleY--;
		}

		if(Gdx.input.isKeyPressed(Input.Keys.A)){
			circleX--;
		}
		else if(Gdx.input.isKeyPressed(Input.Keys.D)){
			circleX++;
		}

		Gdx.gl.glClearColor(.25f, .25f, .25f, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

		shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
		shapeRenderer.setColor(0, 1, 0, 1);
		shapeRenderer.circle(circleX, circleY, 75);
		shapeRenderer.end();
	}

	@Override
	public void dispose() {
		shapeRenderer.dispose();
	}
}

Liigutatav ring

Input Events

Pollimine töötab püsiva sisendi puhul hästi (nt hiirekursori asukoha jälitamine), kuid võib olla tülikas kasutaja sisendile reageerimise puhul (klõpsamine, klahvivajutus, ekraanipuudutus). Nendes olukordades tuleks kasutada sisendsündmuste töötlejat (input event handler).

Proovi näidet:

package ee.taltech.iti0301.libgdxdemo;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.MathUtils;

public class libgdxDemo extends ApplicationAdapter {
	ShapeRenderer shapeRenderer;

	float r = MathUtils.random();
	float g = MathUtils.random();
	float b = MathUtils.random();

	@Override
	public void create () {
		shapeRenderer = new ShapeRenderer();

		Gdx.input.setInputProcessor(new InputAdapter() {

			@Override
			public boolean keyTyped (char key) {
				r = MathUtils.random();
				g = MathUtils.random();
				b = MathUtils.random();
				return true;
			}

			@Override
			public boolean touchDown (int x, int y, int pointer, int button) {
				r = MathUtils.random();
				g = MathUtils.random();
				b = MathUtils.random();
				return true;
			}
		});
	}

	@Override
	public void render () {
		Gdx.gl.glClearColor(r, g, b, 1);
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
	}

	@Override
	public void dispose () {
		shapeRenderer.dispose();
	}
}

Lõime InputAdapter‘i, milles keyTyped() ja touchDown() meetodid, mis seavad muutujad r, g ja b suvalisteks väärtusteks. KeyTyped on klaviatuuri klahvide ning touchDown hiirenuppude jaoks. Nii muudab render() meetod nende muutujate põhjal tausta värvust, kui tuvastatakse hiireklõps või klahvivajutus.

NB! Ehk märkasite, et mõned klaviatuuri klahvid (nt nooleklahvid) ei tee midagi. Seda seetõttu, et keyTyped toimib ainult siis, kui see klahv genereerib Unicode sümbolit. Kui asendate koodis keyTyped (char key) ära keyDown (int keycode)-ga, siis avaldavad mõju ka teised klahvid.

Vt Event handling Wikist

Sisendsündmused


Mitme vaatega mäng


Terviklikul mängul on üldjuhul olemas:

  • tiitellehe vaade (nt mängujuhiste ja play nupuga);
  • sätete/valikute vaade (nt heli vaigistamise valikuga);
  • mängu mängimise vaade;
  • mängu lõppemise vaade (informeerib võidust/kaotusest ja laseb uuesti alustada).

Vaateid võib olla rohkemgi.

Proovime esmalt ilma libGDX’ita

Järgnevat kehva struktuuriga lahendust ei tohiks keerukamate mängude puhul kasutada, kuid see annab parema arusaama libGDX abil sama asja tegemisest.

Proovi koodi:

package ee.taltech.iti0301.libgdxdemo;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Vector2;

public class libgdxDemo extends ApplicationAdapter {

	enum Screen{
		TITLE, MAIN_GAME, GAME_OVER
	}

	Screen currentScreen = Screen.TITLE;

	SpriteBatch batch;
	ShapeRenderer shapeRenderer;
	BitmapFont font;

	float circleX = 300;
	float circleY = 150;
	float circleRadius = 50;

	float xSpeed = 4;
	float ySpeed = 3;

	@Override
	public void create () {
		batch = new SpriteBatch();
		shapeRenderer = new ShapeRenderer();
		font = new BitmapFont();

		Gdx.input.setInputProcessor(new InputAdapter() {

			@Override
			public boolean keyDown (int keyCode) {

				if(currentScreen == Screen.TITLE && keyCode == Input.Keys.SPACE){
					currentScreen = Screen.MAIN_GAME;
				}
				else if(currentScreen == Screen.GAME_OVER && keyCode == Input.Keys.ENTER){
					currentScreen = Screen.TITLE;
				}

				return true;
			}

			@Override
			public boolean touchDown (int x, int y, int pointer, int button) {
				if(currentScreen == Screen.MAIN_GAME){
					int renderY = Gdx.graphics.getHeight() - y;
					if(Vector2.dst(circleX, circleY, x, renderY) < circleRadius){
						currentScreen = Screen.GAME_OVER;
					}
				}
				return true;
			}
		});
	}

	@Override
	public void render () {

		if(currentScreen == Screen.TITLE){

			Gdx.gl.glClearColor(0, .25f, 0, 1);
			Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
			batch.begin();
			font.draw(batch, "Title Screen!", Gdx.graphics.getWidth()*.25f, Gdx.graphics.getHeight() * .75f);
			font.draw(batch, "Click the circle to win.", Gdx.graphics.getWidth()*.25f, Gdx.graphics.getHeight() * .5f);
			font.draw(batch, "Press space to play.", Gdx.graphics.getWidth()*.25f, Gdx.graphics.getHeight() * .25f);
			batch.end();
		}
		else if(currentScreen == Screen.MAIN_GAME) {
			circleX += xSpeed;
			circleY += ySpeed;

			if (circleX < 0 || circleX > Gdx.graphics.getWidth()) {
				xSpeed *= -1;
			}

			if (circleY < 0 || circleY > Gdx.graphics.getHeight()) {
				ySpeed *= -1;
			}

			Gdx.gl.glClearColor(0, 0, .25f, 1);
			Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

			shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
			shapeRenderer.setColor(0, 1, 0, 1);
			shapeRenderer.circle(circleX, circleY, 75);
			shapeRenderer.end();
		}
		else if(currentScreen == Screen.GAME_OVER){
			Gdx.gl.glClearColor(.25f, 0, 0, 1);
			Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

			batch.begin();
			font.draw(batch, "You win!", Gdx.graphics.getWidth()*.25f, Gdx.graphics.getHeight() * .75f);
			font.draw(batch, "Press enter to restart.", Gdx.graphics.getWidth()*.25f, Gdx.graphics.getHeight() * .25f);
			batch.end();
		}
	}

	@Override
	public void dispose () {
		shapeRenderer.dispose();
	}
}

Lõime tiitellehe, mängu vaate ja mängu lõpu vaatega mängu.

Kasutame selles enum‘it, et kirjeldada kolme võimalikku vaadet. Kontrollime, missugune vaade on parasjagu aktiivne ja otsustame selle põhjal, mis vaade teha aktiivseks klahvide või hiirenupu vajutuste järel (vt create() meetodit) kui ka seda, mida täpsemalt ekraanile kuvada (vt render() meetodit).

Kolme vaatega mäng

Kuna keerulisema mängu loomiseks pole see adekvaatne lahendus, siis teeme nüüd elu lihtsamaks libGDX abil.

Teeme LibGDX’iga

LibGDX aitab hoida vaateid (screens) eraldi klassides ja ressursse nende vahel jagada Game klassi ja Screen liidese abil.

Üldjuhul sisaldab libGDX’i projekt ühte klassi, mis laiendab Game‘i, ning mitut klassi, mis implementeerivad Screen‘i. Igale mängus eksisteerivale vaatele vastab klass.

Struktuur

Asenda eelmine näide järgnevaga:

package ee.taltech.iti0301.libgdxdemo;

import com.badlogic.gdx.Game;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;

public class libgdxDemo extends Game {

	SpriteBatch batch;
	ShapeRenderer shapeRenderer;
	BitmapFont font;

	@Override
	public void create () {
		batch = new SpriteBatch();
		shapeRenderer = new ShapeRenderer();
		font = new BitmapFont();
		setScreen(new TitleScreen(this));
	}

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

Pane tähele, et nüüd meie libgdxDemo laiendab Game‘i ja ei sisalda mänguloogikat ega render() osa.

Meile huvipakkuv osa koodist on

setScreen(new TitleScreen(this));

kus setScreen() päritakse Game klassist ja võimaldab meil vahetada erinevate vaadete vahel. Luuakse TitleScreen ja edastatakse libgdxDemo‘i viide konstruktorisse this võtmesõna abil.

Loo libgdxDemo‘ga samasse kataloogi TitleScreen klass järgneva sisuga:

package ee.taltech.iti0301.libgdxdemo;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.GL20;

public class TitleScreen extends ScreenAdapter{

    libgdxDemo game;

    public TitleScreen(libgdxDemo game) {
        this.game = game;
    }

    @Override
    public void show() {
        Gdx.input.setInputProcessor(new InputAdapter() {
            @Override
            public boolean keyDown(int keyCode) {
                if (keyCode == Input.Keys.SPACE) {
                    game.setScreen(new GameScreen(game));
                }
                return true;
            }
        });
    }

    @Override
    public void render(float delta) {
        Gdx.gl.glClearColor(0, .25f, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        game.batch.begin();
        game.font.draw(game.batch, "Title Screen!", Gdx.graphics.getWidth() * .25f, Gdx.graphics.getHeight() * .75f);
        game.font.draw(game.batch, "Click the circle to win.", Gdx.graphics.getWidth() * .25f, Gdx.graphics.getHeight() * .5f);
        game.font.draw(game.batch, "Press space to play.", Gdx.graphics.getWidth() * .25f, Gdx.graphics.getHeight() * .25f);
        game.batch.end();
    }

    @Override
    public void hide() {
        Gdx.input.setInputProcessor(null);
    }
}

See klass laiendab ScreenAdapter‘it (ehk implementeerib Screen‘i, ilma et peaks defineerima igat võimalikku meetodit) ja sisaldab konstruktorit, mis hoiustab libgdxDemo objekti, mille äsja edastasime.

Show() meetod kutsutakse automaatselt välja, kui antud vaade ehk tiitelleht (TitleScreen) saab praegu kuvatavaks vaateks. Selles kontrollitakse tühiku ehk space klahvi vajutamist. Seda vajutades saab vaateks GameScreen vaade.

Render() meetodit kutsutakse korduvalt välja (tavaliselt 60 kaadrit sekundis) nii kaua, kuni antud vaade on praegu kuvatav vaade. See kuvabki tiitellehe elemendid. Seejuures kasutame libgdxDemo klassis loodud batch‘i ja font‘i, et need saaks loodud vaid korra ühes klassis ehk ei peaks korduma mitme vaate puhul.

Hide() meetod kutsutakse välja, kui mängus hakatakse kuvama teist vaadet. See eemaldab show() meetodis loodud InputProcessor‘i, et ei võetaks vastu uusi sisendeid selle vaate mõjutamiseks.

Lisa nüüd ka GameScreen klass, mille sisuks:

package ee.taltech.iti0301.libgdxdemo;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Vector2;

public class GameScreen extends ScreenAdapter {

    libgdxDemo game;

    float circleX = 300;
    float circleY = 150;
    float circleRadius = 50;

    float xSpeed = 4;
    float ySpeed = 3;

    public GameScreen(libgdxDemo game) {
        this.game = game;
    }

    @Override
    public void show() {
        Gdx.input.setInputProcessor(new InputAdapter() {
            @Override
            public boolean touchDown(int x, int y, int pointer, int button) {
                int renderY = Gdx.graphics.getHeight() - y;
                if (Vector2.dst(circleX, circleY, x, renderY) < circleRadius) {
                    game.setScreen(new EndScreen(game));
                }
                return true;
            }
        });
    }

    @Override
    public void render(float delta) {
        circleX += xSpeed;
        circleY += ySpeed;

        if (circleX < 0 || circleX > Gdx.graphics.getWidth()) {
            xSpeed *= -1;
        }

        if (circleY < 0 || circleY > Gdx.graphics.getHeight()) {
            ySpeed *= -1;
        }

        Gdx.gl.glClearColor(0, 0, .25f, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        game.shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
        game.shapeRenderer.setColor(0, 1, 0, 1);
        game.shapeRenderer.circle(circleX, circleY, 75);
        game.shapeRenderer.end();
    }

    @Override
    public void hide() {
        Gdx.input.setInputProcessor(null);
    }
}

Siin sisaldub juba eelnevast tuttav mänguloogika, mis paneb ringi mööda mänguakent ringi liikuma. Muu on analoogiline TitleScreen klassiga.

Kui kasutaja vajutab ringile, siis muudame me aktiivseks vaateks EndScreen‘i.

Loo nüüd viimane ehk EndScreen klass järgneva sisuga:

package ee.taltech.iti0301.libgdxdemo;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.GL20;

public class EndScreen extends ScreenAdapter {

    libgdxDemo game;

    public EndScreen(libgdxDemo game) {
        this.game = game;
    }

    @Override
    public void show() {
        Gdx.input.setInputProcessor(new InputAdapter() {

            @Override
            public boolean keyDown(int keyCode) {

                if (keyCode == Input.Keys.ENTER) {
                    game.setScreen(new TitleScreen(game));
                }

                return true;
            }
        });
    }

    @Override
    public void render(float delta) {
        Gdx.gl.glClearColor(.25f, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        game.batch.begin();
        game.font.draw(game.batch, "You win!", Gdx.graphics.getWidth() * .25f, Gdx.graphics.getHeight() * .75f);
        game.font.draw(game.batch, "Press enter to restart.", Gdx.graphics.getWidth() * .25f, Gdx.graphics.getHeight() * .25f);
        game.batch.end();
    }

    @Override
    public void hide() {
        Gdx.input.setInputProcessor(null);
    }
}

Saadud tulemus on identne eelmise näite tulemusega, mille lõime ilma libGDX’ita, kuid pakub mõistlikumat klasside struktuuri ning keerukamate mängude puhul ka lühemat ja lihtsamini mõistetavat koodi.