
Dopo le ultime due lezioni (1 e 2), è giunta l’ora di chiederci: come visualizziamo tutto ciò che abbiamo fatto con libgdx sul display? Semplice, abbiamo bisogno di creare uno screen per la nostra applicazione e, per farlo, utilizzeremo la classe Game di libgdx, molto comoda per questo genere di operazioni. Prima di procedere, tuttavia, è d’obbligo aprire una piccola parentesi riguardo questi screen. In fin dei conti, un gioco altro non è che un’applicazione composta da più schermate, dunque diversi screen. Un semplice esempio è costituito da un gioco dotato di tre schermate: Play Screen, Start Screen e Game Over Screen.
Come potete facilmente intuire, la prima schermata che abbiamo menzionato conterrà i classici menù per iniziare a giocare o uscire dall’applicazione. Lo screen in questione, dunque, si concentrerà proprio sui possibili eventi che possono accadere in esso. La stessa cosa accade per i restanti due, uno dedicato all’azione di gioco vera e propria, l’altro allo stato di game over.
Detto questo, possiamo passare alla pratica e creare la classe GameScreen.java:
package it.androidblog.starassault.screens; import com.badlogic.gdx.Screen; public class GameScreen implements Screen { @Override public void render(float delta) { // TODO Auto-generated method stub } @Override public void resize(int width, int height) { // TODO Auto-generated method stub } @Override public void show() { // TODO Auto-generated method stub } @Override public void hide() { // TODO Auto-generated method stub } @Override public void pause() { // TODO Auto-generated method stub } @Override public void resume() { // TODO Auto-generated method stub } @Override public void dispose() { // TODO Auto-generated method stub } }
La prima classe che abbiamo creato, StarAssault.java, subirà delle piccole modifiche e la parte iniziale si presenterà in questo modo:
package it.androidblog.starassault; import it.androidblog.starassault.screens.GameScreen; import com.badlogic.gdx.Game; public class StarAssault extends Game{ @Override public void create() { setScreen(new GameScreen()); }
GameScreen è molto simile ad ApplicationListener, tuttavia noterete due metodi aggiuntivi molto importanti: show() e hide(), il primo viene chiamato quando una schermata viene visualizzata, mentre il secondo nel caso quest’ultima venga sostituita da un’altra.
Quello che abbiamo fatto noi è molto semplice, infatti al momento StarAssault ha un solo metodo implementato, ossia create(). Esso non fa altro che attivare il nuovo GameScreen. In parole semplici lo crea, dopodiché viene chiamato show() e, successivamente, render(). GameScreen è una delle classi più importanti sulle quali ci concentreremo, in essa infatti vivrà il nostro gioco, mentre tutto ciò che concerne il ciclo di gioco sarà contenuto nel metodo render().
Tuttavia, abbiamo bisogno prima di elementi da renderizzare, ossia il nostro mondo, il quale potrà essere creato anche in show(). A questo punto, continuiamo apportando alcune modifiche alla classe GameScreen.java:
private World world; private WorldRenderer renderer; /** Rest of methods ommited **/ @Override public void render(float delta) { Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); renderer.render(); }
Come potete vedere, abbiamo un’istanza della classe World.java, creata nella precedente lezione e una di WorldRenderer, la classe che andremo a creare ora:
package it.androidblog.starassault.view; import it.androidblog.starassault.model.Bob; import it.androidblog.starassault.model.World; import it.androidblog.starassault.model.Block; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.math.Rectangle; public class WorldRenderer { private World world; private OrthographicCamera cam; /** for debug rendering **/ ShapeRenderer debugRenderer = new ShapeRenderer(); public WorldRenderer(World world) { this.world = world; this.cam = new OrthographicCamera(10, 7); this.cam.position.set(5, 3.5f, 0); this.cam.update(); } public void render() { // render blocks debugRenderer.setProjectionMatrix(cam.combined); debugRenderer.begin(ShapeType.Filled); for (Block block : world.getBlocks()) { Rectangle rect = block.getBounds(); float x1 = block.getPosition().x + rect.x; float y1 = block.getPosition().y + rect.y; debugRenderer.setColor(new Color(1, 0, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); } // render Bob Bob bob = world.getBob(); Rectangle rect = bob.getBounds(); float x1 = bob.getPosition().x + rect.x; float y1 = bob.getPosition().y + rect.y; debugRenderer.setColor(new Color(0, 1, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); debugRenderer.end(); } }
Lo scopo di WorldRenderer è esclusivamente quello di renderizzare il mondo sul display. Esso ha un solo metodo pubblico, render(), il quale viene chiamato nel ciclo di gioco di GameScreen.java. La classe, naturalmente, ha bisogno dell’accesso a World.java e, per questo, passiamo un’istanza di World durante l’istanziazione del renderer.
Un’altra considerazione da fare riguarda i metodi getters e setters di Bob.java e Block.java. Implementarli è molto semplice ma, nel caso non ci riusciste, nella prossima lezione avrete a disposizione il codice completo di StarAssault per il download.
Per quanto riguarda il codice, invece, abbiamo dichiarato innanzitutto una variabile per il mondo e una per la telecamera. Si tratta di una telecamera ortogonale, ne avremo bisogno specialmente quando Bob percorrerà livelli più grandi e la fotocamera dovrà seguirlo passo passo. Successivamente dichiariamo l’oggetto di tipo ShapeRenderer. Esso ci servirà per disegnare comodamente primitive come linee, rettangoli e cerchi.
A questo punto, settiamo la telecamera in modo che riprenda 10 unità in larghezza 7 in altezza (ricordate il mock up visto precedentemente?) e, successivamente, la posizioniamo affinché riprenda il centro della stanza. Di default, la posizione della fotocamera sarà (0,0). Inoltre, è necessario aggiornare la fotocamera e le relative matrici (metodo update()) e, come potete constatare voi stessi, i processi interni di OpenGL sono nascosti perfettamente. Riguardo le impostazioni di settaggio della telecamera, l’immagine di seguito è abbastanza esplicativa.
Successivamente applichiamo la matrice dalla fotocamera al renderer ed informiamo quest’ultimo che vogliamo disegnare rettangoli grazie a ShapeType. Infine, con un’interazione disegniamo i rettangoli facendo riferimento alle coordinate del rettangolo di delimitazione di ogni blocco (abbiamo già visto che OpenGL lavora con i vertici e la larghezza). La stessa operazione la facciamo con Bob, con la differenza che i rettangoli per i blocchi saranno in rosso, mentre per Bob il colore scelto sarà il verde.
Prima di concludere, è necessario affrontare un’altra questione importante: la risoluzione del display. I blocchi che verranno visualizzati, infatti, saranno 10 in larghezza e 7 in altezza (per il settaggio della telecamera), tuttavia viene spontaneo chiedersi: cosa succede nel caso la risoluzione dovesse aumentare? La risposta è molto semplice: i blocchi saranno sempre gli stessi in numero e godranno di una risoluzione maggiore.
Esempio: se la risoluzione dello schermo è di 480 × 320 pixel, significa che 480 pixel rappresentano 10 unità, quindi un blocco sarà costituito da 48 pixel di larghezza. Nel contempo, 320 pixel rappresentano 7 unità e, di conseguenza, il singolo blocco avrà 45,7 pixel di altezza.
Tutto ciò si traduce in un quadrato che non è perfetto, ma ciò è dovuto al rapporto di aspetto (aspect ratio). Nella prossima lezione vedremo altri meccanismi basilari di libgdx e sarà disponibile il download di StarAssault. Per qualunque dubbio o domanda, non esitate a commentare l’articolo.