====== 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. {{ libgdx.png?250 }} {{tag> #FpInfor #Dam #DamMp08 #DamMp08Uf3 #DamMp08Uf03 jocs games}} Enllaços: - [[libGDX Comunicacions]] en aquesta wiki. - [[https://github.com/libgdx/libgdx/wiki|Documentació libGDX]]. - [[https://libgdx.com/dev/project_generation/|Descàrrega eina per iniciar projectes]]. - [[https://libgdx.com/dev/simple_game/#the-game|Tutorial joc Drop]]. \\ ===== Instal·lació ===== Necessites tenir instal·lat Android Studio. Crea el projecte amb l'[[https://libgdx.com/dev/project_generation/|eina per iniciar projectes libGDX]]. ==== Troubleshooting ==== Si t'apareix l'[[https://stackoverflow.com/questions/70340427/unable-to-find-method-void-org-apache-commons-compress-archivers-zip-zipfile|error relacionat amb la llibreria ZIP]] tens 2 opcions: - Eliminar la compilació de la plataforma iOS. - Editar ''android/build.gradle'' i ajustar les llibreries: buildToolsVersion "33.0.0" \\ ===== Definicions ===== 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''. \\ ===== Primer joc: Drops ===== {{ drop-game.jpeg }} - Crea un [[https://libgdx.com/dev/project_generation/|nou projecte amb aquesta eina]]. Puja'l a Github (i al Moodle). - Segueix el [[https://libgdx.com/dev/simple_game/|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). - Mostra el joc al professor quan tinguis el cubell funcionant. Fes un commit i push al repo. - Realitza la [[https://libgdx.com/dev/simple_game_extended/|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. - Recorda fer un nou commit i push. - Mostra el joc al professor quan el tinguis acabat. - Afegeix les següents ampliacions: - Implementa un comptador de les gotes que es capturen amb el cubell. Mostra el resultat en una cantonada de la pantalla. - Quan una gota arriba a terra el joc s'acaba. Afegeix un so adequat. - 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. - Afegeix una imatge de fons adequada (que les gotes ressaltin i no dificulti el joc). - Quan s'acaba la partida, la imatge de fons és diferent. - 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). - Fes un comptador que indiqui el nombre de //frames// per segon (consultar llibreria Gdx.graphics). - Afegeix alguna funcionalitat de collita pròpia. - Recorda fer commit i push a Github. \\ ===== libGDX és multiplataforma ===== El codi de la teva aplicació està al mòdul CORE. Pots compilar en: - **Android**: si obres el projecte amb Android Studio per defecte podràs executar en Android. - **Desktop** (java app, Windows o Linux): afegeix una nova configuracióRUN -> Edit configurations -> Add (+) -> Application - Selecciona Module: ''myapp.desktop.main'' - Selecciona Main Class: ''DesktopLauncher'' - [[https://medium.com/@bschulte19e/deploying-your-libgdx-game-to-ios-in-2019-8d3796410d82|iOS: segueix aquestes instruccions]]. Requereix tenir un Mac, XCode i Android Studio amb RovoVM plugin - [[https://stackoverflow.com/questions/55670168/unable-to-select-ios-simulator-via-robovm-intellij-idea-plugin-on-android-studio|Segons diu aquí]], el simulador no s'activa, i cal descarregar la darrera versió de RoboVM d'[[http://robovm.mobidevelop.com/downloads/snapshots/idea/|aquí]] - El darrer punt del tutorial diu q cal modificar ''ios.iml'' cada cop que executem. [[https://github.com/MobiVM/robovm/issues/242#issuecomment-519321280|Aquí]] hi ha una solució automatitzada. \\ ===== Sprites i animacions ===== Utilitzarem l'objecte [[https://libgdx.com/wiki/graphics/2d/2d-animation|2D-Animation]] de libGDX. L'[[https://libgdx.com/wiki/graphics/2d/2d-animation|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 [[https://libgdx.com/wiki/graphics/2d/spritebatch-textureregions-and-sprites|objectes de la llibreria]]: - **Texture**: ens ajuda a gestionar una imatge. - **TextureRegion**: ens facilita retallar imatges grans i mostrar només una part (ideal per a agafar un fotograma d'un //spritesheet//). - **Sprite**: descriu una TextureRegion amb la geometria i rotació per a ser renderitzat. - **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ó. - Imatges i //sprites//: - https://www.kindpng.com - https://www.gameart2d.com - Música i sons: - https://freesound.org ==== 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: - [[https://libgdx.com/wiki/tools/texture-packer|TexturePacker doc]]. Hi ha unes apps suggerides pels creadors de libGDX: - https://github.com/crashinvaders/gdx-texture-packer-gui - https://github.com/raeleus/skin-composer - [[http://libdgxtutorials.blogspot.com/2013/09/libgdx-tutorial-8-using-texture-packer.html|Tutorial TexturePacker]]. - [[https://www.codeandweb.com/texturepacker|Texture Packer (sw propietari)]]. Té funcions interessants però algunes son només en la versió Pro (de pagament). - He trobat interessant [[https://stackoverflow.com/questions/28522757/libgdx-textureregion-drawing-weird-when-scaled|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: * Trossejar totes les imatges i fer servir TexturePacker. Obtindrem una spritesheet nova amb el seu atlas. * Fragmentar la //spritesheet// en un array de TextureRegion. Necessitarem saber la posició (x,y) d'origen i dimensions (amplada i alçada) de cada //frame// o //sprite//. **Eines com GIMP poden facilitar-ho seleccionant cada //sprite// i copiant les seves coordenades i dimensons**. {{ android:yeti-spritesheet2.png?400 }} 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 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(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'[[https://libgdx.com/wiki/graphics/2d/2d-animation|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'' ([[https://libgdx.badlogicgames.com/ci/nightlies/docs/api/com/badlogic/gdx/graphics/g2d/TextureRegion.html|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(); {{ietiwalk.gif}} 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 [[https://libgdx.badlogicgames.com/ci/nightlies/docs/api/|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: * [[https://libgdx.badlogicgames.com/ci/nightlies/docs/api/com/badlogic/gdx/math/Vector2.html|Vector2]] ens permetrà representar un vector, que té una component X i una component Y. Val la pena parar atenció als mètodes per calcular l'angle ''angleDeg()'' i ''angleRad()''. * [[https://libgdx.badlogicgames.com/ci/nightlies/docs/api/com/badlogic/gdx/math/Rectangle.html|Rectangle]] compost per posició (X i Y) i amplada i alçada. Al tutorial Drop s'utilitza també per veure les col·lisions (entre el cubell i les gotes) amb el mètode ''intersects(Rectangle)''. Té altres mètodes útils com ''contains'' per saber si un objecte està dins d'un altre. * [[https://libgdx.badlogicgames.com/ci/nightlies/docs/api/com/badlogic/gdx/math/Circle.html|Circle]] ens anirà molt bé per a les pilotes, ja que té posició (X i Y) i radi. També ens permetrà calcular col·lisions amb altres objectes amb els mètodes ''intersect'' i ''contains''. I a la **llibreria Graphics** destaquem: * [[https://libgdx.badlogicgames.com/ci/nightlies/docs/api/com/badlogic/gdx/graphics/g2d/Batch.html|Batch]] és l'objecte que pinta imatges (Textures) amb el mètode ''draw'' (sobrecarregat amb diverses versions, mira la documentació). * El //batch// només pinta [[https://libgdx.badlogicgames.com/ci/nightlies/docs/api/com/badlogic/gdx/graphics/Texture.html|Textures]] (imatges) sobre la pantalla. Si volem pintar formes de colors necessitarem l'objecte Pixmap. * [[https://libgdx.badlogicgames.com/ci/nightlies/docs/api/com/badlogic/gdx/graphics/Pixmap.html|Pixmap]] a mode de //canvas// o "lienzo", ens permet crear imatges a partir de formes geomètriques (línia, cercle, rectangle, píxel, etc.) ==== Dibuixant formes ==== Podem dibuixar formes geomètriques utilitzant Batch i l'objecte [[https://libgdx.badlogicgames.com/ci/nightlies/docs/api/com/badlogic/gdx/graphics/Pixmap.html|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. {{Pilota-rebots.png}} 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 - Pilota (busca una icona que t'agradi) que reboti a tots els límits de la pantalla. - 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. - 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: * Teclat (només per versions Desktop) * Pantalla * Acceleròmetre * Brúixola/Giròscop Convé llegir la [[https://libgdx.com/wiki/input/polling|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: {{ android:game-controls.png }} 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: * Crides HTTP/S estàndard * Ús WebSockets per comunicacions a temps real. \\ ===== 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 800x480 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: * [[https://stackoverflow.com/questions/33062574/how-to-properly-implement-a-dialog-box-using-libgdx|Com fer un Dialog]]. * [[https://github.com/BlueBoxWare/LibGDXPlugin|libGDX plugin per IntelliJ (Android Studio)]] * [[https://github.com/libgdx/libgdx-skins/tree/master/skins|Skins bàsics]] * [[https://github.com/czyzby/gdx-skins|Mes skins]] IMPORTANT: perquè funcioni el skin cal descarregar tots els arxius a la carepta assets. \\