A questo punto dovreste essere ben preparati sull’architettura di un gioco per Android e, se avete seguito le due lezioni precedenti, sul vostro Eclipse il progetto Droidz ha iniziato già a prendere forma. In questa lezione su come sviluppare un gioco per Android vedremo come dare inizio al nostro primo ciclo di gioco che inizierà dall’input dell’utente: nel nostro caso il touchscreen. L’input dell’utente sarà solo l’inizio di una catena di eventi gestiti dalle attività Android che andremo noi stessi a creare. Il ciclo di gioco che creeremo sarà relativamente semplice, ma costituirà una solida base di partenza per chi vuole espandere le proprie conoscenze.
[divider]
Una visione d’insieme
Come soprascritto, l’input dal touchscreen è solo l’inizio. Ciò che la nostra applicazione dovrà fare, successivamente, riguarderà l’aggiornamento dello stato dei vari oggetti e, infine, effettuare il rendering sul display, accompagnando il tutto con eventuali suoni o vibrazioni. Il diagramma qui di seguito vi chiarirà le idee.
Per quanto riguarda l’aggiornamento degli oggetti interni e la loro resa sul display, si intendono due attività (non confondetevi con le Android Activity) raggruppate logicamente ed eseguite una dietro l’altra. Tutto ciò accadrà in una “Android Activity”, quest’ultima creerà la View dove accadrà il tutto. La View catturerà l’input e sempre essa mostrerà le immagini risultanti. Immaginate l’Activity come un riquadro contenente un foglio di carta (la nostra View) dove possiamo disegnare quel che vogliamo. Nell’immagine di seguito è racchiuso il concetto appena esposto.
[divider]
Primo passo: creazione della View e del Thread principale
A questo punto iniziamo a mettere mano al codice. Aprendo il file MainActivity.java noteremo la seguente riga:
setContentView(R.layout.activity_main);
Questa istruzione non fa altro che assegnare la View predefinita alla nostra Activity quando l’applicazione viene avviata. È d’obbligo, dunque, occuparci personalmente della creazione della View, ossia di una semplice classe Java che ci fornirà tutto il necessario per gestire gli eventi (come onTouch) e non solo, infatti essa ci consentirà anche di disegnare e utilizzare bottoni o forme di qualunque tipo.
La via più semplice è quella di estendere la classe SurfaceView di Android e implementare Callback per accedere ai cambiamenti di superficie (ad esempio quando il device viene ruotato ed entra in gioco l’accelerometro). Poco fa abbiamo parlato di “estendere una classe” e “implementare un metodo“, se non avete idea di cosa significa, come riportato nella prima lezione, date una ripassata alla vostra conoscenza di Java. Prendiamo come esempio iniziale il codice del nostro pannello di gioco principale, qui di seguito riportato:
package it.androidblog.droidz; import android.content.Context; import android.graphics.Canvas; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceHolder.Callback; import android.view.SurfaceView; public class MainGamePanel extends SurfaceView implements CallBack { public MainGamePanel(Context context) { super(context); // Aggiungiamo callback(this) alla superfice per intercettare gli eventi getHolder().addCallback(this); // Rendiamo attivo il nostro pannello di gioco in modo che possa gestire gli eventi setFocusable(true); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { } @Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); } @Override protected void onDraw(Canvas canvas) { } }
Questa classe è molto semplice: essa include tutti i metodi dei quali abbiamo bisogno (li vedremo più giù, al momento lasciamoli vuoti) e, inoltre, compie anche un altro lavoro, molto importante per il nostro gioco. Soffermiamoci, per un attimo, alle righe 16 e 18, entrambe commentate. La prima ha come scopo quella di impostare la classe corrente (MainGamePanel.java) come gestore degli eventi che avvengono sulla superficie, la seconda istruzione invece è necessaria in quanto dobbiamo attivare il nostro pannello per gestire gli eventi.
A questo punto creiamo il thread che costituirà il nostro ciclo di gioco creando una nuova classe che chiameremo MainThread:
package it.androidblog.droidz; public class MainThread extends Thread{ private boolean running; public void setRunning (boolean running) { this.running = running; } @Override public void run() { while (running) { //aggiornamento stato di gioco //rendering dello stato di gioco su display } } }
Finché il valore di running sarà settato su true, il metodo run() effettuerà un ciclo infinito. Al momento non abbiamo istanziato ancora nulla, quindi iniziamo modificando la classe MainGamePanel vista prima per poi arrivare alla resa sul display (che vedremo comunque nelle prossime lezioni con accuratezza).
package it.androidblog.droidz; import android.content.Context; import android.graphics.Canvas; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; public class MainGamePanel extends SurfaceView implements SurfaceHolder.CallBack { private MainThread thread; public MainGamePanel(Context context) { super(context); getHolder().addCallback(this); // Creiamo il thread per il ciclo di gioco thread = new MainThread(); setFocusable(true); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceCreated(SurfaceHolder holder) { thread.setRunning(true); thread.start(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { // Il thread deve arrestarsi e aspettare che finisca boolean retry = true; while (retry) { try { thread.join(); retry = false; } catch (InterruptedException e) { // riproviamo ad arrestare il thread } } } @Override public boolean onTouchEvent(MotionEvent event) { return super.onTouchEvent(event); } @Override protected void onDraw(Canvas canvas) { } }
Le prime modifiche effettuate riguardano le righe 12 e 18, dove abbiamo rispettivamente dichiarato e istanziato il thread. Nel metodo surfaceCreated, invece, abbiamo impostato il flag su true e dato inizio al thread. Quando questo metodo viene richiamato, la superficie dell’applicazione è già creata e il nostro ciclo di gioco può avviarsi in maniera sicura. Ora, diamo un’occhiata più da vicino al metodo surfaceDestroyed.
In realtà, quello che abbiamo fatto non è molto. Non è di certo il posto giusto per impostare il valore di retry su true, ma ciò che abbiamo fatto ci assicura che il thread venga chiuso in maniera corretta. In parole più semplici, lo abbiamo bloccato in attesa che muoia definitivamente. Avviando ora l’emulatore non sarete in grado di vedere molto infatti useremo lo strumento LogCat per testare il gioco, ma procediamo con ordine.
[divider]
Secondo passo: aggiungere l’interazione con il display
Quello che faremo adesso, per interagire con il display, riguarderà la registrazione delle coordinate. Esse verranno registrate solo quando toccheremo la parte alta del display, infatti toccando nella parte inferiore usciremo dall’applicazione. Alla classe MainThread.java, aggiungiamo le seguenti righe di codice:
private SurfaceHolder surfaceHolder; private MainGamePanel gamePanel; public MainThread(SurfaceHolder surfaceHolder, MainGamePanel gamePanel) { super(); this.surfaceHolder = surfaceHolder; this.gamePanel = gamePanel; }
Come potete notare, abbiamo dichiarato le variabili surfaceHolder e gamePanel per poi passarle al costruttore come parametri. È necessario averle entrambe e non solo gamePanel in quanto è necessario bloccare la superficie touch in determinati casi, e surfaceHolder serve proprio a quello. In MainGamePanel, invece, è necessario istanziare il thread:
thread = new MainThread(getHolder(), this);
Al thread istanziato stiamo passando i parametri necessari affinché esso possa accedere al pannello di gioco, difatti il metodo per l’aggiornamento dello stato sarà implementato proprio nel pannello in questione e utilizzato dal thread (ma vedremo tutto questo successivamente). Al momento concentriamoci su un’altra questione di rilevante importanza: la costante TAG.
Per utilizzare il framework di logging Android e testare l’applicazione (cosa che faremo a breve), è necessario dichiarare una costante TAG in ogni classe e il suo nome sarà proprio quello della classe che lo contiene. Il motivo è molto semplice, il tag ci servirà per identificare l’origine del messaggio di log e utilizzare lo stesso nome della classe ci aiuterà non poco (anche nelle operazioni di ricerca).
Completiamo le classi
È giunta l’ora di completare le nostre tre classi e l’interazione con il display. Ecco come dovrebbe apparire la classe MainThread.java:
package it.androidblog.droidz; import android.util.Log; import android.view.SurfaceHolder; public class MainThread extends Thread{ private static final String TAG = MainThread.class.getSimpleName(); private SurfaceHolder surfaceHolder; private MainGamePanel gamePanel; private boolean running; public MainThread(SurfaceHolder surfaceHolder, MainGamePanel gamePanel) { super(); this.surfaceHolder = surfaceHolder; this.gamePanel = gamePanel; } public void setRunning (boolean running) { this.running = running; } @Override public void run() { long tickCount = 0L; Log.d(TAG, "Starting game loop"); while (running) { tickCount++; //aggiornamento stato di gioco //rendering dello stato di gioco su display } Log.d(TAG, "Game loop executed " + tickCount + " times"); } }
Come potete vedere abbiamo istanziato la costante TAG e, nel metodo run(), tickCount che viene incrementato ad ogni ciclo di gioco. Vediamo, ora, le aggiunte da fare alla classe MainGamePanel. Nel nostro caso, per il momento ci basterà implementare il metodo onTouchEvent (prima lasciato vuoto) e aggiungere il consueto TAG.
private static final String TAG = MainGamePanel.class.getSimpleName(); public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { if (event.getY() > getHeight() - 50) { thread.setRunning(false); ((MainActivity)getContext()).finish(); } else { Log.d(TAG, "Coords: x=" + event.getX() + ",y=" + event.getY()); } } return super.onTouchEvent(event); }
Con MotionEvent.ACTION_DOWN, controlliamo se l’input è un tocco prolungato sullo schermo. Se è così, controlliamo le coordinate dell’evento e, se è avvenuto nella parte bassa del display (i 50 pixel più in basso), chiudiamo il thread e terminiamo l’Activity, in caso contrario registriamo le coordinate grazie al logging.
Se avete studiato il codice potreste notare una incongruenza nelle coordinate. In realtà, esso funziona, ma bisogna ricordare che le coordinate dell’area rettangolare che, in questo, è il display, non hanno origine in basso, ma in alto a sinistra. Difatti, le coordinate del punto in basso a destra sono esattamente (getWidth(), getHeight()). Infine, ecco come modificare MainActivity.java:
package it.androidblog.droidz; import android.os.Bundle; import android.util.Log; import android.view.Window; import android.view.WindowManager; import android.app.Activity; public class MainActivity extends Activity { private static final String TAG = MainActivity.class.getSimpleName(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // requesting to turn the title OFF requestWindowFeature(Window.FEATURE_NO_TITLE); // making it full screen getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // set our MainGamePanel as the View setContentView(new MainGamePanel(this)); Log.d(TAG, "View added"); } @Override protected void onDestroy() { Log.d(TAG, "Destroying..."); super.onDestroy(); } @Override protected void onStop() { Log.d(TAG, "Stopping..."); super.onStop(); } }
Abbiamo scritto i metodi onStop() e onDestroy() solo per registrare il ciclo di vita dell’attività, mentre alla riga 19 abbiamo reso full screen l’applicazione. A questo punto, il lavoro di oggi può ritenersi concluso per la parte codice, ma se provate ad avviare l’applicazione vi ritroverete dinanzi un display completamente nero. Tuttavia, se clicchiamo nella parte bassa dello schermo l’applicazione si chiuderà come da noi richiesto, mentre nella parte alta verranno registrate le coordinate grazie ai TAG utilizzati. Vediamo, ora, come testare l’applicazione. [divider]
Uno sguardo al LogCat Android
Per aprire il LogCat Android e capire cosa fa, di preciso, il nostro gioco, basta andare su Windows -> Show View -> LogCat. In basso si aprirà il LogCat che, in termini poveri, non è altro che una console dove potete seguire i log Android ricercando anche per tag, testo e così via. Proviamo ad avviare l’applicazione e cliccare un paio di volte sulla parte alta del display e poi sulla parte più bassa per chiudere il gioco. Vi apparirà nel LogCat qualcosa di molto simile al seguente:
Quelle che ci interessano sono le linee blu e, come potete vedere, non solo vi saranno restituite le coordinate dei punti in cui avete cliccato, ma vi dirà anche quante volte avete eseguito il ciclo di gioco. Il numero è molto alto, ma nelle prossime lezioni vedremo come creare un ciclo di gioco più complesso di questo, considerando anche FPS e UPS, ossia Frames per Second e Updates per Second.
Inoltre considereremo un ciclo di gioco che si occuperà anche di disegnare qualcosa sul display e lo farà un certo numero di volte, come noi stessi specificheremo. Insomma, il lavoro da fare è ancora tanto, ma non vi scoraggiate. Nella prossima lezione, vedremo come disporre le immagini con le librerie Android. Come sempre, per qualunque chiarimento, commentate l’articolo.