Taula de continguts

Desenvolupament de jocs amb libGDX

A l'article jocs_android pot veure's com treballar jocs utilitzant el framework estàndard d'Android (bàsicament modificant els valors del layout i els elements que hi conté).

En aquest article utilitzarem una llibreria específica, libGDX per realitzar el joc. Aquesta té molts avantatges, sobretot que permet compilar en diverses plataformes (Android, Desktop, iOS, HTML). A més, ens permetrà utilitzar recursos gràfics específics que ens facilitaran operacions complexes en 2D i 3D amb acceleració OpenGL.

, , , , , ,

Enllaços:

  1. libGDX Comunicacions en aquesta wiki.


Instal·lació

Necessites tenir instal·lat Android Studio.

Crea el projecte amb l'eina per iniciar projectes libGDX.

Troubleshooting

Si t'apareix l'error relacionat amb la llibreria ZIP tens 2 opcions:

  1. Eliminar la compilació de la plataforma iOS.
  2. Editar android/build.gradle i ajustar les llibreries:
    android/build.gradle
    buildToolsVersion "33.0.0"


Definicions

Objectes principals del framework:


Primer joc: Drops

drop-game.jpeg

  1. Crea un nou projecte amb aquesta eina. Puja'l a Github (i al Moodle).
  2. Segueix el tutorial per fer el joc Drop. El tutorial conté indicacions per implementar el joc en les 4 plataformes amb el mateix codi Java, però si vols , només cal que segueixis el tutorial per la part de Android i el core (la part comuna).
  3. Mostra el joc al professor quan tinguis el cubell funcionant. Fes un commit i push al repo.
  4. Realitza la segona part del tutorial de libGDX on s'expliquen els objectes Game i Screen. Acaba de transformar el teu projecte a la versió amb objectes tal i com es descriu al tutorial i que et permetrà fer una pantalla de presentació del joc.
  5. Recorda fer un nou commit i push.
  6. Mostra el joc al professor quan el tinguis acabat.
  7. Afegeix les següents ampliacions:
    1. Implementa un comptador de les gotes que es capturen amb el cubell. Mostra el resultat en una cantonada de la pantalla.
    2. Quan una gota arriba a terra el joc s'acaba. Afegeix un so adequat.
    3. Quan s'acaba la partida s'esborra la pantalla i s'atura el so. Es mostra la puntuació i un botó que permeti reiniciar la partida.
    4. Afegeix una imatge de fons adequada (que les gotes ressaltin i no dificulti el joc).
    5. Quan s'acaba la partida, la imatge de fons és diferent.
    6. Ajusta el hit test per tal que la gota només es consideri recollida si entra per la part de sobre del cubell (que no funcioni si venim de costat).
    7. Fes un comptador que indiqui el nombre de frames per segon (consultar llibreria Gdx.graphics).
    8. Afegeix alguna funcionalitat de collita pròpia.
  8. Recorda fer commit i push a Github.


libGDX és multiplataforma

El codi de la teva aplicació està al mòdul CORE.

Pots compilar en:

  1. Android: si obres el projecte amb Android Studio per defecte podràs executar en Android.
  2. Desktop (java app, Windows o Linux): afegeix una nova configuració
    RUN -> Edit configurations -> Add (+) -> Application
    1. Selecciona Module: myapp.desktop.main
    2. Selecciona Main Class: DesktopLauncher
  3. iOS: segueix aquestes instruccions. Requereix tenir un Mac, XCode i Android Studio amb RovoVM plugin
    1. Segons diu aquí, el simulador no s'activa, i cal descarregar la darrera versió de RoboVM d'aquí
    2. El darrer punt del tutorial diu q cal modificar ios.iml cada cop que executem. Aquí hi ha una solució automatitzada.


Sprites i animacions

Utilitzarem l'objecte 2D-Animation de libGDX.

L'objecte Animation ens facilita la gestió dels sprites a partir d'una spritesheet. Fes aquest tutorial per veure com fer l'animació. És relativament fàcil aconseguir-ho amb els objectes de la llibreria:

  1. Texture: ens ajuda a gestionar una imatge.
  2. TextureRegion: ens facilita retallar imatges grans i mostrar només una part (ideal per a agafar un fotograma d'un spritesheet).
  3. Sprite: descriu una TextureRegion amb la geometria i rotació per a ser renderitzat.
  4. Animation: un cop tenim triats els Texture o TextureRegion que ens cal per a l'animació, els podem posar en un array, i Animation ens facilitarà anar avançant els fotogrames un a un, segons el temps transcorregut.

Exercici

Busca una spritesheet que t'agradi en alguna de les següents webs amb recursos lliures de copyright i mira d'animar-la de forma similar a com es fa al tutorial de Animation.

Tingues en compte que és molt més fàcil animar una spritesheet amb els sprites ubicats en forma de matriu regular (com l'exemple de la documentació). Si són irregulars necessitarem un mapeig que es sol fer amb un Texture Atlas. Es recomana que inicialment feu servir un spritesheet simple.

Cada alumne ha de buscar un spritesheet diferent. Mostreu-lo al professor abans de realitzar l'animació.

  1. Imatges i sprites:
  2. Música i sons:

Eines interessants

Una eina interessant és TexturePacker, que ens permet construir una spritesheet a partir d'imatges individuals, i ens generarà també el TextureAtlas que facilitarà trobar després les coordenades de cada fotograma:

  1. TexturePacker doc. Hi ha unes apps suggerides pels creadors de libGDX:
  2. Texture Packer (sw propietari). Té funcions interessants però algunes son només en la versió Pro (de pagament).
  3. He trobat interessant aquest post de Stackoverflow que recomana l'ús d'aquesta eina, TexturePacker.


Animant un spritesheet irregular

Si volem animar un spritesheet irregular necessitarem fer un mapeig de les imatges (sprites) en un Texture Atlas. Per aconseguir-ho tenim diverses opcions:

Per exemple, per aquesta spritesheet podem fer el caminar del Yeti de la següent manera:

A la definició de classe:

TextureRegion frames[] = new TextureRegion[4];
Animation<TextureRegion> yeti;

A create():

// per cada frame cal indicar x,y,amplada,alçada
frames[0] = new TextureRegion(sheet,9,190,107,112);
frames[1] = new TextureRegion(sheet,124,190,113,107);
frames[2] = new TextureRegion(sheet,248,190,101,110);
frames[3] = new TextureRegion(sheet,364,190,122,109);
yeti = new Animation<TextureRegion>(0.25f,frames);

A render():

stateTime += Gdx.graphics.getDeltaTime(); // Accumulate elapsed animation time
TextureRegion frame = yeti.getKeyFrame(stateTime,true);
 
batch.begin();
batch.draw(frame, 200, 100);
// si volem invertir el sentit, ho podem fer amb el paràmetre scaleX=-1
batch.draw(frame, 200, 100, 0, 0,
           frame.getRegionWidth(),frame.getRegionHeight(),-1,1,0);batch.end();

Invertir sprites

En moltes spritesheets els mateixos frames per caminar cap a l'esquerra serveixen per caminar cap a la dreta, ja que les eines de libGDX ens permeten invertir la imatge.

Per invertir un sprite es pot fer a través del mètode TextureRegion.flip, però el resultat (en aquest cas) no és molt bo ja que al haver diferents amplades del frame fa que el moviment faci salts irregulars.

És més pràctic invertir la imatge directament en el mètode SpriteBatch.draw ajustant la sccaleX=-1 , lo qual facilitarà un moviment més fluid (al menys a l'exemple) ja que l'origen es manté bé.


Mahoma o la muntanya

Per aconseguir una sensació de moviment hem fet servir l'objecte 2D-Animation de libGDX, juntament amb Texture i TextureRegion, tal i com explica el tutorial.

Ara, a part de l'animació, haurem de bellugar el personatge… o bé el fons de la pantalla. Tenim aquestes dues estratègies per fer la sensació de que avancem.

Moure el personatge

Per desplaçar el personatge per la pantalla, només caldrà modificar ON el pintem de la pantalla, just a les línies del codi de la funció render():

// al render()
// ...
spriteBatch.begin();
// Draw current frame at (50, 50)
spriteBatch.draw(currentFrame, 50, 50);
spriteBatch.end();

Podem desplaçar el personatge tenint una posx i posy, i utilitzant-les al render:

spriteBatch.draw(currentFrame, posx, posy);

A cada iteració del render només caldrà anar incrementant o decrementant adequadament aquestes posx i posy.

Moure el background

També podem optar per deixar el personatge al mig de la pantalla i bellugar el fons o background. O sigui que si Mahoma no va a la muntanya, serà la muntanya la que vindrà a Mahoma.

La idea serà tenir un mapa de fons, una imatge molt gran, de la qual retallarem només la part que volem mostrar a la pantalla del joc (el background).

L'objecte TextureRegion (veure TextureRegion a la documentació oficial) ens anirà perfecte per gestionar el fons de pantalla ja que està pensat per «retallar» una imatge gran i definir només una part d'ella (la que volem mostrar).

En particular, el mètode TextureRegion.setRegion(…) ens facilitarà que puguem desplaçar l'origen del fragment del background que volem mostrar, provocant un efecte de moviment (i que combinat amb l'animació queda exactament com es vol per a un joc).

Al create():

// bg
background = new Texture(Gdx.files.internal("background12.jpeg"));
background.setWrap(Texture.TextureWrap.MirroredRepeat, Texture.TextureWrap.MirroredRepeat);
bgRegion = new TextureRegion(background);
posx = 0;
posy = 0;

I al render() (acció del main loop del joc):

// (1) CALCULAR
//...calculem posx i posy del personatge...
 
// TextureRegion ens permet retallar un fragment de la Texture
// retallem el fragment de background des de la posició del personatge (posx, posy)
bgRegion.setRegion(posx,posy,game.SCR_WIDTH,game.SCR_HEIGHT);
 
// (2) PINTAR
game.batch.begin();
// primer pintem el background
game.batch.draw(bgRegion,0,0);
// ...després pintem altres coses...
 
// finalitzem main loop
game.batch.end();

A més, pots aconseguir fer un background q es vagi repetint fent efecte mirall quan arribes als límits, com és el cas d'aquest exemple del Ieti. Això ho aconseguim amb Texture.setWrap(…) (consulta la doc de Texture). El cas típic és fer-li wrap vertical i horitzontal:

background.setWrap( Texture.TextureWrap.MirroredRepeat,
                    Texture.TextureWrap.MirroredRepeat);


Objectes libGDX

La llibreria libGDX ens dona una sèrie d'objectes per facilitar els càlculs i l'encapsulament de variables.

De la llibreria Math destaquem:

I a la llibreria Graphics destaquem:

Dibuixant formes

Podem dibuixar formes geomètriques utilitzant Batch i l'objecte Pixmap.

Aquest és un exemple utilitzat per a un tret làser de color vermell (rectangle):

Pixmap tretPixmap = new Pixmap( TRET_WIDTH, TRET_HEIGHT, Pixmap.Format.RGB888);
tretPixmap.setColor( Color.RED );
tretPixmap.fill();
Texture tretTexture = new Texture(tretPixmap);

Per fer una pilota bàsica (cercle) amb transparència = 0.3, (on 0.0==transparent i 1.0==opac) de color blanc ho faríem així:

Pixmap pilotaPixmap = new Pixmap(100,100,Pixmap.Format.RGBA8888);
pilotaPixmap.setColor(new Color(1.0f,1.0f,1.0f,0.3f));
pilotaPixmap.fillCircle(50,50,50);
Texture pilotaTexture = new Texture(pilotaPixmap);

Aquests objectes es crearien al constructor del Game. Després pintaríem el «tret» o la «pilota» en el mètode render segons les coordenades que haguem calculat (utilitzant Vector, Rectangle, etc.).


Pilotes rebotant

Molts jocs es basen en fer rebotar pilotes per la pantalla. Us passo algunes estratègies típiques per aconseguir-ho.

Ull! Segons aquesta imatge l'origen de coordenades està a dalt a l'esquerra. Això sol ser així en la majoria de llibreries gràfiques. libGDX, en canvi, té l'origen a baix a l'esquerra, que resulta més intuïtiu ja que està més d'acord amb els clàssics eixos de coordenades cartesians.

Com pots veure a la imatge, rebotar una pilota és fàcil si tens les coordenades (pos_x i pos_y) de la posició, i la trajectòria la emmagatzemes com una velocitat descomposada en component X i component Y (vel_x i vel_y , per exemple).

Quan la pilota arriba al límit inferior o superior, només cal invertir el signe de la velocitat Y si la trajectòria semblarà exactament un rebot.

Exercici

Fes una aplicació de demostració amb els següents nivells (Screen).

Quan es cliqui sobre la pantalla es passa al següent nivell.

Procura utilitzar Vector2

  1. Pilota (busca una icona que t'agradi) que reboti a tots els límits de la pantalla.
  2. Pilota amb gravetat. Rebota a terra, però no al «sostre».
    • Pista: es tracta de que la velocitat Y varii al llarg del temps, amb un increment relacionat amb g (la gravetat).
    • Vigila el comportament que té la pilota al llarg del temps. Hauria de mantenir-se a la mateixa alçada màxima. Si no és així, revisa perquè passa.
  3. Pilota amb gravetat i fricció. Quan rebota a terra, perd algo d'energia i acaba atenuant-se


Controls Touchscreen

Els controls d'entrada al joc poden ser molts:

Convé llegir la documentació dels controls en libGDX.

Joystick virtual

Una estratègia adequada sol ser definir unes regions de la pantalla que, quan es premin, permetin avançar el personatge. Definirem aquestes regions amb rectangles d'1/3 de la pantalla:

A l'objecte:

Rectangle up, down, left, right, fire;
final int IDLE=0, UP=1, DOWN=2, LEFT=3, RIGHT=4;

A create():

// facilities per calcular el "touch"
up = new Rectangle(0, game.SCR_HEIGHT*2/3, game.SCR_WIDTH, game.SCR_HEIGHT/3);
down = new Rectangle(0, 0, game.SCR_WIDTH, game.SCR_HEIGHT/3);
left = new Rectangle(0, 0, game.SCR_WIDTH/3, game.SCR_HEIGHT);
right = new Rectangle(game.SCR_WIDTH*2/3, 0, game.SCR_WIDTH/3, game.SCR_HEIGHT);

A render() podem cridar la funció virtual_joystick_control() que ens retornarà la direcció que detectem de les regions definides:

    protected int virtual_joystick_control() {
        // iterar per multitouch
        // cada "i" és un possible "touch" d'un dit a la pantalla
        for(int i=0;i<10;i++)
        if (Gdx.input.isTouched(i)) {
            Vector3 touchPos = new Vector3();
            touchPos.set(Gdx.input.getX(i), Gdx.input.getY(i), 0);
            // traducció de coordenades reals (depen del dispositiu) a 800x480
            game.camera.unproject(touchPos);
            if (up.contains(touchPos.x, touchPos.y)) {
                return UP;
            } else if (down.contains(touchPos.x, touchPos.y)) {
                return DOWN;
            } else if (left.contains(touchPos.x, touchPos.y)) {
                return LEFT;
            } else if (right.contains(touchPos.x, touchPos.y)) {
                return RIGHT;
            }
        }
        return IDLE;
    }

Exercici

Implementa el control de moviment mitjançant touch input al teu personatge amb animació. Pots optar moure el personatge per la pantalla, o bé moure el background, tal i com s'explica a l'apartat anterior.


Comunicacions

Vés a l'article libGDX Comunicacions on podràs aprendre a utilitzar:


Ortographic Camera

Tal i com explica el tutorial del Drop Game, la OrtographicCamera ens facilitarà la traducció entre les coordenades que hem definit pel joc (o «coordenades virtuals») i les coordenades reals del dispositiu (device), que poden tenir dimensions diferents. A més, al treballar amb una llibreria multiplataforma, voldrem fer un sol joc a la carpeta core/, i el codi de les carpetes de les plataformes (android/, ios/, html/) haurà de ser fix (no hem de repetir el joc a cada plataforma).

Les operacions que realitzarà la càmera seran:

Coordenades virtuals -> project -> Coordenades reals (//device//)
Coordenades virtuals <- unproject <- Coordenades reals (//device//)

Posant que volem una pantalla de 800×480

DesktopLauncher.java
public class DesktopLauncher {
    public static void main (String[] arg) {
        Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration();
        config.setWindowedMode(480, 800);

A l'arxiu de Game o Screen:

public final int GAME_WIDTH = 800;
public final int GAME_HEIGHT = 480;
 
public void create() {
        camera = new OrthographicCamera();
        camera.setToOrtho(false, GAME_WIDTH, GAME_HEIGHT);
 
        //...
}

Per fer que el SpritBatch tradueixi automàticament en totes les accions de dibuix, el configurarem a l'inici del render():

public void render() {
        camera.update();
        spriteBatch.setProjectionMatrix(camera.combined);
 
        //...
}

Si estem capturant entrades de la pantalla, caldrà la operació contrària: unproject()

protected int virtual_joystick_control() {
    // iterar per multitouch
    // cada "i" és un possible "touch" d'un dit a la pantalla
    for(int i=0;i<10;i++)
    if (Gdx.input.isTouched(i)) {
        Vector3 touchPos = new Vector3();
        touchPos.set(Gdx.input.getX(i), Gdx.input.getY(i), 0);
        // traducció de coordenades reals (depen del dispositiu) a 800x480
        game.camera.unproject(touchPos);
        // les dades convertides s'enregistren a la mateixa variable touchPos
        //...


Actors, Scenes i Skins

Per fer controls avançats com Buttons, Dialogs, etc. hem de tenir en compte que son elements tant de renderització com de entrada de dades, i es tracten de forma especial. En caldrà emprar els objectes Stage i Skin:

Algunes referències:

IMPORTANT: perquè funcioni el skin cal descarregar tots els arxius a la carepta assets.