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:
Necessites tenir instal·lat Android Studio.
Crea el projecte amb l'eina per iniciar projectes libGDX.
Si t'apareix l'error relacionat amb la llibreria ZIP tens 2 opcions:
android/build.gradle
i ajustar les llibreries:buildToolsVersion "33.0.0"
Objectes principals del framework:
Game
: classe principal del joc, contenidor de pantalles.Screen
: hi encapsulem una pantalla (game over, presentació, etc.), nivell, minijoc. Cadascuna és independent de l'altra.OrtographicCamera
: objecte per traduir coordenades. Pot servir per adaptar tamanys de pantalla o per projectar en 2D una imatge 3D.SpriteBatch
: objecte per pintar sprites (imatges, animacions).ShapeRenderer
: objecte per pintar formes geomètriques (cercles, el·lipses, rectangles, triangles, etc.BitmapFont
: objecte per pintar lletres.Texture
: imatge.TextureRegion
: fragment d'una imatge.Animation
: objecte per gestionar les animacions i spritesheets.Actor
: objecte tipus widget, que pot ser alhora un element gràfic i una entrada de dades. Exemples: Button, Dialog, TextInputListener, etc.Stage
: objecte per renderitzar Actors
.Skin
: conjunt de imatges i fonts per renderitzar Actors
. Imprescindible si es vol emprar Actor
i Stage
.
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.
El codi de la teva aplicació està al mòdul CORE.
Pots compilar en:
RUN -> Edit configurations -> Add (+) -> Application
myapp.desktop.main
DesktopLauncher
ios.iml
cada cop que executem. Aquí hi ha una solució automatitzada.
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:
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.
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ó.
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:
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é.
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.
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.
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);
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:
angleDeg()
i angleRad()
.intersects(Rectangle)
. Té altres mètodes útils com contains
per saber si un objecte està dins d'un altre.intersect
i contains
.I a la llibreria Graphics destaquem:
draw
(sobrecarregat amb diverses versions, mira la documentació).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.).
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.
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
Els controls d'entrada al joc poden ser molts:
Convé llegir la documentació dels controls en libGDX.
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; }
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.
Vés a l'article libGDX Comunicacions on podràs aprendre a utilitzar:
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
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 //...
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.