Dopo l'introduzione generale di JMonkey nell'articolo precedente, ci interessa adesso concentrare la nostra attenzione sul target Android; il nostro obiettivo è quello di realizzare una applicazione didattica che dimostri l'uso degli eventi touch screen attraverso la costruzione di una semplice scena.
Protagonista della scena è un cubo sul quale possiamo applicare trasformazioni in corrispondenza di eventi come: il singolo tocco, il doppio tocco, oppure il pinch per ingrandimento o riduzione delle dimensioni. Il progetto è evidentemente molto semplice, ma può costituire una ottima base per chi volesse poi approfondire i concetti avanzati legati allo sviluppo di applicazioni e giochi.
Configurazione per Android
Vediamo subito come preparare l'ambiente per lo sviluppo verso Android. Per approfondimenti vi rimandiamo direttamente al sito di JMonkey
Partendo da JMonkeyEngine 3 SDK Beta installato:
- Installare Android SDK 2.2 o superiore, potete scaricare le SDK dal seguente link:
- Dall'ambiente JMonkey possiamo installare il plugin NBAndroid. Dal menu Plugins andare in Available Plugins ed installare l'Android Plugin.
- Dovremo specificare il percorso alla nostra SDK installata. Possiamo verificare il tutto andando dal menu in Tools->Options, opzione Mobile :
- Per quanto riguarda il device, in cui intendiamo installare il file apk del nostro applicativo, i requisiti sono i seguenti:
- Android 2.2 o superiore
- Scheda grafica che supporta OpenGL ES 2.0 o superiore
Per verificare la vostra versione di Android è sufficiente andare nelle impostazioni del vostro telefono. Per verificare il supporto e la corretta versione OpenGL, potete invece scaricare dal market OpenGL 2.0 Tester.
Una volta installato il plugin Android dovreste avere una situazione simile a quella in figura:
L'ambiente a questo punto dovrebbe essere pronto per lo sviluppo su Android, proseguiamo con la creazione del progetto.
Il progetto didattico: TheCube
TheCube è il nome che utilizzeremo per creare la nostra demo. Creiamo un nuovo progetto BasicGame:
Facciamo click su next, nella successiva schermata diamo TheCube come nome, salviamo poi il tutto con un click su finish
. Ci troveremo di fronte alla struttura del progetto.
Apriamo il [Source Packages]
e rinominiamo il package mygame (tasto destro del mouse sul nome del package e scelta su [Refactor]
),costruito di default con la Main class, in it.html.mygame
.
Dobbiamo adesso dare a questo progetto la configurazione Android: facciamo click con il tasto destro sul suo nome,andiamo su [Set Configuration]
e selezioniamo [Customize]
. La schermata che verrà visualizzata consente di rendere il progetto BasicGame
un progetto Android. L'albero Application dovrebbe trovarsi già espanso, se cosi non fosse, espandiamolo e selezioniamo [Mobile]
. Selezioniamo quindi la check [Enable Android Deployment]
, scegliamo la versione Android ( 2.2 o successive) e definiamo il package che conterrà la classe Main di lancio : it.html.mygame
.
All'interno di [Build] -> [Compiling]
abbiate cura di deselezionare l'opzione check [Compile on Save]
.Concludiamo questa operazione con un click su ok. Arrivati a questo punto dovremmo avere la struttura del progetto pronta con la classe Main nel package specificato:
Diamo una rapida spiegazione a questa struttura. [Project Assets] contiene le cartelle che racchiudono le risorse utilizzate dall'applicativo: modelli, materiali, texture ... . All'interno di [Important Files] troviamo tutto ciò che rende possibile la creazione di un file apk da installare su un device Android:
Build File
Avvia la compilazione e generazione del pacchetto
Android Main Activity
E' la classe con parametri di configurazione Android. Se visualizzate questa classe, all'interno del costruttore, trovate i seguenti parametri
// La classe di lancio
appClass = "it.html.mygame.Main";
// Il tipo di configurazione
tentate anche con ConfigType.FASTEST; oppure ConfigType.LEGACY se avete problemi
eglConfigType = ConfigType.BEST;
// La finestra di uscita dell'applicazione: titolo e messaggio
exitDialogTitle = "Exit?";
exitDialogMessage = "Press Yes";
// Abilitare il verbose logging
eglConfigVerboseLogging = false;
// Scegliere l'orientamento dello schermo
screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER;
// Inversione MouseEvents X (default = true)
mouseEventsInvertX = true;
// Inversione MouseEvents Y (default = true)
mouseEventsInvertY = true;
Android Manifest
E' il file di configurazione fondamentale per l'istallazione. Attraverso di esso possiamo inoltre specificare informazioni quali il nome dell'applicazione, la sua icona, la versione ed il package che contiene la classe di lancio. Se apriamo il file noteremo la seguente struttura:
<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="it.html.mygame">
<application android:label="@string/app_name" android:icon="@drawable/icon">
<activity android:label="@string/app_name" android:name="MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
<uses-sdk android:minSdkVersion="8"/>
<supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:smallScreens="true"/>
</manifest>
Vediamo i punti essenziali di questo file. Nel tag manifest possiamo notare come vengono specificate la versione dell'applicativo (android:versionName="1.0")
, e package con la classe di lancio (package="it.html.mygame")
. L'attributo android:label="@string/app_name"
, che possiamo notare nei tag application e activity, denota il nome dell'applicazione. Per vedere dove viene salvato questo valore ,e quindi come dare un determinato nome alla nostra applicazione, passiamo alla visuale Files del progetto ed espandiamo la cartella [Mobile]
, quindi la cartella [res]
al suo interno per editare il file mobile/res/strings.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="app_name">TheCube</string>
</resources>
in questo file possiamo specificare il nome dell'applicazione. L'attributo android:icon="@drawable/icon"
, che non viene aggiunto dal wizard di creazione, è particolarmente utile perchè ci consente di specificare un'icona custom per l'applicazione. Nel nostro caso drawable
rappresenta una cartella all'interno del percorso [Mobile/res]
che contiene il file di nome icon per l'icona rappresentativa dell'applicativo (all'interno dei file allegati all'articolo troverete già un'icona custom in questo percorso).
Android Properties
Questo file contiene alcuni parametri di configuazione tra cui due molto importanti che consentono di applicare una chiave per la segnatura del pacchetto:
key.store=/Android/android-sdk/my-release-key.keystore key.alias=my_alias
Quando realizziamo un'applicazione, vengono generate diverse versioni, tra cui una con una chiave di debug che ci permette di vedere in esecuzione l'applicazione durante la fase di sviluppo. Ma nel momento in cui intendessimo rilasciare una versione sul market di Android, avremo bisogno di generare una nostra chiave ed applicare questa chiave al pacchetto durante la sua generazione.
Il parametro key.store
indica il percorso del file della chiave generata e key.alias
l'alias specificato durante la sua generazione. La generazione di un file keystore avviene attraverso l'utility keytool
delle JDK Java.
Potete trovare istruzioni su come generarla al seguente link:
http://developer.android.com/guide/publishing/app-signing.html
Jmonkey Android, il cuore di un'applicazione: La Scena 3D
La scena grafica è la struttura dati che gestisce tutti gli oggetti del mondo 3D.
Quando creiamo il nostro BasicGame
project estendendo la classe com.jme3.app.SimpleApplication
, automaticamente ereditiamo la scena grafica ed il suo nodo root: rootNode
.
Il rootNode è l'elemento centrale della scena grafica, è sempre presente anche se la scena è vuota: una scena rappresenta essenzialmente un ambiente all'interno del quale il giocatore può muoversi, con visuale in prima o terza persona, interagendo con l'ambiente stesso e con eventuali altre entità.
Spatial : Nodi e Geometrie
Le entità di cui parliamo vengono chiamate Spatial
, Geometry
e Node
. Attraverso esse possiamo definire personaggi ed oggetti dell'ambiente, e loro raggruppamenti. Uno Spatial è una struttura astratta di dati che rappresentano trasformazioni (traslazioni, rotazioni, scalatura) di elementi della scena grafica.
Gli Spatial
possono essere salvati e caricati usando la classe AssetManager
. Nel nostro codice java uno Spatial
, istanza della classe com.jme3.scene.Spatial
, può rappresentare una geometria (com.jme3.scene.Geometry
) oppure un nodo (com.jme3.scene.Node
) a seconda dell'uso che intendiamo farne. Una geometria rappresenta un oggetto visibile della scena: contiene le mesh, materiali, texture.. e può essere collegata ad un nodo. Una mesh è un insieme di poligoni: le mesh
sono essenzialmente costituite da triangoli ed in JMonkey sono rappresentate dalla classe com.jme3.scene.Mesh
.
Quando realizziamo un gioco Android, l'aspetto relativo al numero di poligoni che compongono la mesh di una geometria (che rappresenta ad esempio il personaggio della scena) non è affatto un aspetto trascurabile, anzi deve essere curato con attenzione.
Se sentite parlare di modelli "low poly", è importante capire che ci sta riferendo a modelli adatti ad essere utilizzati su dispositivi mobile. Una mesh
viene utilizzata in combinazione con i material e le texture per darle un particolare aspetto grafico.
Mentre una Geometria ha una sua visibilità nella scena, questo non accade per un nodo: esso non è visibile, ma serve a raggruppare geometrie, spatial ed altri nodi.
Ogni nodo è collegato al suo nodo genitore e può avere zero o più figli a lui collegati. I nodi rivestono particolare importanza in tutti quei casi in cui abbiamo Spatial relazionati e vogliamo che seguano tutti contemporaneamente una determinata trasformazione (rotazione,traslazione,scalatura) oppure nei casi in cui una trasformazione di un oggetto abbia come riferimento un nodo differente dal nodo root (trasformazioni locali). Collegando gli Spatial relazionati ad un nodo, e applicando la trasformazione al nodo, tutti gli Spatial ad esso collegati seguiranno la trasformazione.
Nella prossima parte inizieremo a familiarizzare con gli eventi e con l'implementazione delle animazioni
Il progetto TheCube: gli eventi
Abbiamo completato le configurazioni necessarie per l'ambiente di sviluppo ed introdotto i primi concetti fondamentali, è quindi arrivato il momento di immergerci nel codice. Vogliamo realizzare una demo che illustri l'uso degli eventi touch screen, quindi ci concentreremo sui seguenti eventi:
MOVE
Movimento continuo del dito sullo schermo. Sfrutteremo questo evento Android per la traslazione del cubo che seguirà il movimento del nostro dito.
DOUBLETAP
Doppio tocco sullo schermo. Attraverso questo evento verificheremo se il doppio tocco è avvenuto sul cubo, se si, faremo in modo di ruotare il cubo illustrando il concetto di Quaternion, strumento matematico molto importante per le rotazioni.
LONGPRESSED
Pressione del dito sullo schermo superiore al secondo. Sfruttiamo questo evento per illustrare l'uso delle animazioni. Con una pressione del dito sul cubo, superiore al secondo,verrà eseguita un'animazione.
SCALE_START, SCALE_MOVE, SCALE_END
Movimento delle dita sullo schermo che realizza il pinch Android per lo zoom. Sono tre eventi che si attivano,rispettivamente, all'inizio del ridimensionamento, durante, ed alla fine. Grazie ad essi riusciremo ad ingrandire o ridurre le dimensioni del cubo con il movimento delle nostre dita.
Partiamo dalla classe Main
del package it.html.mygame
. La classe Main
deve implementare l'interfaccia com.jme3.input.controls.TouchListener
. Questa interfaccia ha un metodo onTouch()
, che viene eseguito in risposta alle nostre azioni sullo schermo del dispositivo mobile. Grazie ad essa riusciamo quindi a gestire gli eventi di input. Il cubo è stato realizzato attraverso Blender, convertito in un file j3o per JMonkey, e disponibile nella cartella [cube]
all'interno di [Models]
in [Project Assets]
. Il cubo è il nostro Spatial
che collegheremo al nodo root. Il nostro primo obiettivo è quello di caricarlo e visualizzarlo sullo schermo. Concentriamoci sullo scheletro della Main class:
public class Main extends SimpleApplication implements TouchListener {
public static void main(String[] args) {
Main app = new Main();
app.start();
}
@Override
public void simpleInitApp() { }
@Override
public void simpleUpdate(float tpf) {}
@Override
public void simpleRender(RenderManager rm) {}
@Override
public void onTouch(String name, TouchEvent evt, float tpf) { }
}
Abbiamo diversi metodi, focalizziamo l'attenzione su simpleInitApp()
. Questo metodo viene eseguito all'avvio dell'applicazione, in esso posizioniamo il codice per la costruzione della scena.
Cominciamo con il cubo: dobbiamo ottenere uno Spatial partendo dal modello che lo definisce. Per far ciò, utilizziamo l'assetManager ereditato dalla classe SimpleApplication. Invochiamo il metodo loadModel()
indicando il percorso del file j3o che definisce il modello del cubo, successivamente associamo un nome allo Spatial:
Spatial cube = assetManager.loadModel("Models/cube/Cube.j3o");
cube.setName("cube");
Definiamo una luce per illuminare l'ambiente:
AmbientLight am = new AmbientLight();
am.setColor(ColorRGBA.White.mult(2));
Non desideriamo visualizzare i frame per secondo e le statistiche:
setDisplayFps(false);
setDisplayStatView(false);
Colleghiamo all'oggetto rootNode la luce e lo Spatial del cubo:
rootNode.addLight(am);
rootNode.attachChild(cube);
Disabilitiamo la flyCam per avere la camera fissa nella sua posizione:
flyCam.setEnabled(false);
e dal punto di vista della costruzione la nostra semplice scena è terminata.
Eseguiamo la Main class per avere un'anteprima del risultato, dovremmo avere una schermata simile alla seguente:
se facciamo un doppio click sul file j3o nella cartella models, possiamo aprire il modello del cubo nella SceneExplorer:
nella parte in alto a destra possiamo notare come siano stati dati dei valori iniziali che dispongono il cubo secondo una particolare rotazione in riferimento ai 3 assi x,y,z. La SceneExplorer ci permette di manipolare la scena in una modalità di design.
Il progetto TheCube : L'Animazione
Da questo punto in poi dobbiamo armarci di un pò di pazienza. Purtroppo l'emulatore delle SDK di Android sembra non supportare OpenGL 2, per cui il nostro lavoro sarà visibile installando il file apk generato, su un device Android con i requisiti introdotti ad inizio articolo.
Procediamo con la definizione della classe per la gestione dell'animazione, poi reiamo il package it.html.anim
e definiamo al suo interno la classe CubeAnimation
. Come visibile dal codice completo, questa classe implementa l'interfaccia AnimEventListener
. Grazie a ad essa possiamo gestire gli stati di un'animazione attraverso i metodi onAnimCycleDone()
e onAnimChange()
.
Il primo viene eseguito quando il ciclo dell'animazione termina, il secondo quando cambiamo animazione sullo Spatial: ad uno Spatial è possibile associare diverse animazioni. CubeAnimation
riceve attraverso il costruttore lo Spatial di cui deve gestire l'animazione. La gestione dell'animazione avviene attarverso le 2 variabili di istanza (oltre allo Spatial):
private AnimChannel channel;
private AnimControl control;
Dopo aver caricato il modello animato del cubo, dobbiamo registrarlo in un Animation Controller
. L'oggetto Controller ci consente l'accesso a tutte le sequenze animate disponibili. Il Controller può avere diversi canali, ed ogni canale può eseguire solo una sequenza animata alla volta. L'esecuzione di più sequenze, come si può intuire, è possibile creando più canali ed impostandoli ciascuno sull'animazione che dovranno attivare. Il controllore è definito dalla classe com.jme3.animation.AnimControl
. Per ottenerne uno collegato allo Spatial del cubo, all'interno del costruttore definiamo la sua inizializzazione:
control = model.getControl(AnimControl.class);
Dove model è la variabile di istanza nella quale abbiamo salvato il riferimento allo spatial del cubo. CubeAnimation, implementando l'interfaccia com.jme3.animation.AnimEventListener
, agisce da listener fornendo l'accesso agli eventi che notificano la fine di una sequenza di animazione o il cambio da una sequenza ad un'altra. I metodi onAnimCycleDone()
e onAnimChange()
sono vuoti poichè nel nostro caso non abbiamo necessità di gestire un cambio di animazione. Aggiungiamo immediatamente dopo, la seguente riga di codice, collegando cosi al Controller un ascoltatore di eventi sulle animazioni:
control.addListener(this);
Il controllore è configurato. Ciò che però ci permette di attivare o cambiare un'animazione è il canale creato attraverso il controllore definito. Il canale è definito attraverso la classe com.jme3.animation.AnimChannel
. Il nostro cubo è dotato di una sola animazione, abbiamo quindi bisogno di un singolo canale per attivarla :
channel = control.createChannel();
questa riga di codice completa il costruttore. CubeAnimation offre l'attivazione dell'animazione attraverso il metodo public startAnimation()
.
Vediamo le sue 3 righe di codice:
channel.setAnim("my_animation");
channel.setLoopMode(LoopMode.DontLoop);
channel.setSpeed(1f);
La prima dichiara l'animazione che intendiamo eseguire identificata dalla stringa "my_animation". La seconda specifica che non vogliamo un ciclo di ripetizione dell'animazione. Con l'ultima impostiamo invece la velocità. La classe completa:
public class CubeAnimation implements AnimEventListener{
private AnimChannel channel;
private AnimControl control;
private Spatial model;
public CubeAnimation(Spatial model){
this.model=model;
control = model.getControl(AnimControl.class);
control.addListener(this);
channel = control.createChannel();
}
@Override
public void onAnimCycleDone(...){ //unused }
@Override
public void onAnimChange(...) { //unused }
public Spatial getModel() {return model;}
public void startAnimation(){
channel.setAnim("my_animation");
channel.setLoopMode(LoopMode.DontLoop);
channel.setSpeed(1f);
}
}
Nella prossima parte verrà finalmente realizzato il controllo Touch, implementati i metodi di gestione, ed esportata la demo per i test sul nostro dispositivo!
Il progetto TheCube : Il Touch Handler
La classe TouchHandler, che definiremo nel package it.html.touch
, è sicuramente la classe più complessa del progetto. Possiamo definirla il cuore della nostra applicazione poichè risponde agli aventi touch, attiva le animazioni, usa la camera per recuperare le coordinate del cubo, ed infine attiva le trasformazioni.
Essendo particolarmente importante, analizziamola con attenzione. L'input di cui necessita è costituito dall'oggetto Camera cam
, l'oggetto Node rootNode
della scena, e il riferimento a CubeAnimation
:
public class TouchHandler {
private Camera cam;
private Node rootNode;
private CubeAnimation cubeAnimation;
public TouchHandler(Camera cam, Node rootNode, CubeAnimation cubeAnimation) {
this.cam = cam;
this.rootNode = rootNode;
this.cubeAnimation = cubeAnimation;
}
// ...
}
Nella classe Main abbiamo visto il metodo onTouch()
invocato in risposta agli eventi touch screen. Vogliamo che all'interno di questo metodo sia utilizzato un oggetto TouchHandler
al quale demandare specificatamente questa gestione. Per raggiungere questo obiettivo aggiungiamo un metodo alla classe TouchHandler
con lo stesso nome e la stessa signature:
public void onTouch(String name, TouchEvent evt, float tpf) { // ... }
gestione delle coordinate
Se adesso, immaginiamo di aver agito in qualche modo sullo schermo del dispositivo mobile (un tocco, doppio tocco o altro), la prima azione da compiere all'interno del metodo onTouch()
, sarà quella di determinare le coordinate dello schermo in cui l'evento si è verificato.
Ogni punto è identificato da un vettore e JMonkey utilizza, così come OpenGL, il sistema di coordinate right-handed. Questo significa in pratica che, considerata l'origine al centro dello schermo, l'asse delle x ha la direzione positiva verso destra, l'asse y verso l'alto e l'asse z in avanti e uscente dallo schermo, e infine il verso positivo di rotazione è quello antiorario.
La camera e l'inquadratura
La camera sarà per noi posizionata sull'asse z e rivolta verso l'interno dello schermo nella direzione dell'origine del sistema di riferimento.
In JMonkey un vettore è definito dalla classe com.jme3.math.Vector3f
per le 3 dimensioni e com.jme3.math.Vector2f
per le due dimensioni.
Non entriamo nei dettagli di come funziona la camera che inquadra la nostra scena, ma cerchiamo di aver presente che quello che vediamo sullo schermo Android è la proiezione 2D della scena che invece vive in un mondo 3D. Quindi da un touch sullo schermo otteniamo le coordinate 2D del punto di contatto attraverso il parametro evt di tipo com.jme3.input.event.TouchEvent
e costruiamo con tali coordinate un vettore 2D:
Vector2f touchPoint = new Vector2f(evt.getX(), evt.getY());
Attraverso la cam otteniamo le coordinate nel mondo 3D del punto di contatto e la direzione in cui guarda la Camera:
Vector3f origin = cam.getWorldCoordinates(touchPoint, 0f);
Vector3f direction = cam.getWorldCoordinates(touchPoint, 1f);
Adesso vogliamo capire se il cubo è stato toccato dal touch del nostro dito sullo schermo. In generale, quando vogliamo determinare quale mesh è stata selezionata attraverso il touch, ricorriamo al concetto matematico di Ray. Per determinare se un oggetto è stato selezionato o no, viene disegnata una linea che parte dalla camera e si verifica se tale linea interseca l'oggetto nella scena 3D, questa linea è chiamata Ray
. Quindi creiamo un oggetto com.jme3.math.Ray
, definiamo una lista per tenere traccia delle eventuali collissioni, ed utilizziamo il rootNode per ottenere i risultati di collisioni con il Ray:
Ray ray = new Ray(origin, direction.subtractLocal(origin).normalizeLocal());
CollisionResults results = new CollisionResults();
rootNode.collideWith(ray, results);
In generale possiamo avere diversi oggetti nella scena con i quali si possono avere collisioni. La nostra situazione è però in questo caso estremamente semplice: possiamo toccare solo il cubo e quindi avere al massimo una sola collisione.
Iteriamo la lista dei risultati delle collisioni, recuperiamo ad ogni step il vettore del punto di contatto, hit
, ed infine controlliamo se il punto di contatto è contenuto all'interno del volume del cubo, e in caso affermativo assegnamo true
al flag collisionDetected
e terminiamo l'iterazione:
Spatial cube = rootNode.getChild("cube");
boolean collisionDetected = false;
Vector3f hit = null;
for (int i = 0; i < results.size(); i++) {
hit = results.getCollision(i).getContactPoint();
if (cube.getWorldBound().contains(hit)) {
collisionDetected = true;
break;
}
}
[...]
// gestione degli eventi
if (collisionDetected) {
switch (evt.getType()) {
case DOUBLETAP:
break;
case MOVE:
break;
...
default:
break;
}
}
evt.setConsumed();
L'animazione e l'implementazione degli eventi
L'implementazione degli eventi
In sintesi vogliamo che il tocco per più di un secondo del cubo (LONGPRESSED
) faccia scattare l'animazione, il doppio tocco (DOUBLETAP
) lo faccia ruotare, il MOVE
ci permetta di trascinarlo sullo schermo, e SCALE_START
, SCALE_MOVE
, SCALE_END
ci permettano il ridimensionamento.
L'animazione
Per l'animazione, la più semplice, usiamo il riferimento alla classe CubeAnimation
, non dobbiamo far altro che invocare il metodo startAnimation()
:
case LONGPRESSED:
cubeAnimation.startAnimation();
break;
La rotazione
Qui introduciamo il concetto di Quaternion e la classe QuernionHelper
. Un Quaternion è uno strumento matematico che consente di effettuare rotazioni in modo efficiente. Nella classe QuaternionHelper
abbiamo definito alcuni metodi utili per il suo utilizzo:
// Rotazione dello Spatial s intorno al vettore axis, di un angolo angle
public static Quaternion rotateFromAngleAxis(float angle,Vector3f axis,Spatial s){
return s.getLocalRotation().fromAngleAxis(angle, axis);
}
// Rotazione dello Spatial s intorno agli assi con gli angoli indicati
public static Quaternion setQuaternionRotation(float xAngle,float yAngle, float zAngle,Spatial s){
Quaternion qRot = new Quaternion(new float[]{xAngle,yAngle,zAngle});
s.setLocalRotation(qRot);
return qRot;
}
// Costruzione di un Quaternion per la rotazione secondo gli angoli specificati per ciascun asse
public static Quaternion getQuaternionRotation(float xAngle,float yAngle, float zAngle){
Quaternion qRot = new Quaternion(new float[]{xAngle,yAngle,zAngle});
return qRot;
}
Vediamone ad esempio l'uso per la rotazione di 1/3 pi-greco intorno all'asse Y sul doppio touch:
case DOUBLETAP:
Quaternion quaternion =
QuaternionHelper.getQuaternionRotation(0, FastMath.ONE_THIRD, 0);
cube.rotate(quaternion);
break;
com.jme3.math.FastMath
è una classe JMonkey per il supporto alle operazioni matematiche. Il cubo ruota grazie al metodo rotate()
della classe Spatial che prende in input il Quaternion con le infromazioni di rotazione.
Il ridimensionamento
Il pinch: vediamo come ingrandire o ridurre le dimensioni del cubo. Vogliamo fare in modo che il cubo modifichi di un 1%, progressivamente, le sue dimensioni, grazie al movimento delle nostre dita sullo schermo. Per far ciò dobbiamo agire sia sui tre eventi auto-esplicativi SCALE_START
, SCALE_MOVE
e SCALE_END
:
case SCALE_START:
float scale = evt.getScaleFactor() >= 1 ? 1.01f : 0.99f;
cube.scale(scale, scale, scale);
break;
case SCALE_MOVE:
scale = evt.getScaleFactor() >= 1 ? 1.01f : 0.99f;
cube.scale(scale, scale, scale);
break;
case SCALE_END:
scale = evt.getScaleFactor() >= 1 ? 1.01f : 0.99f;
cube.scale(scale, scale, scale);
break;
in sostanza recuperiamo il fattore di scala attraverso getScaleFactor()
del parametro evt
e se questo valore è maggiore o uguale ad uno abbiamo un ingrandimento ed assegnamo quindi alla variabile scale il valore 1.01 (aumento di un 1% della dimensione), altrimenti il valore 0.99 (diminuzione di un 1% della dimensione). Infine passiamo il valore al metodo scale()
della classe Spatial
che riduce le dimensioni del cubo lungo i 3 assi x,y,z.
Il trascinamento
Abbiamo trattato tutte le azioni che producono un effetto sul cubo, senza traslazioni.
Vediamolo adesso in "azione": il movimento del cubo che segue il nostro dito sullo schermo.
Abbiamo precedentemente recuperato il punto di contatto sullo schermo tra il nostro dito ed il cubo, e salvato questa informazione nel vettore hit
. Il trascinamento del cubo avviene attraverso due semplici righe di codice:
case MOVE:
hit.z = 0;
cube.setLocalTranslation(hit);
break;
azzeriamo la coordinata z poichè vogliamo che si muova sul piano xy (non desideriamo che si avvicini o allontani da noi) ed utilizziamo poi il metodo setLocalTranslation()
per traslarlo nel punto di contatto, ottenendo cosi un movimento fluido.
Arrivati a questo punto tutte le classi che compongono l'applicazione sono state trattate,torniamo adesso alla main class e completiamola in modo che utilizzi le classi CubeAnimation
e TouchHandler
. Possiamo a uqesto punto completare i metodi visti precedentemente in maniera simile alla seguente (per i dettagli rimandaimo al codice allegato):
public void simpleInitApp(){
//...
private CubeAnimation cubeAnimation;
private TouchHandler touchHandler;
// ...
cubeAnimation = new CubeAnimation(cube);
touchHandler = new TouchHandler(cam, rootNode, cubeAnimation);
}
public void initKeys(){
inputManager.addMapping("Touch", new TouchTrigger(0));
inputManager.addListener(this, new String[]{"Touch"});
}
e prima di flyCam.setEnabled(false)
in simpleInitApp()
aggiungeremo la chiamata ad initKeys()
Generazione del file APK
Abbiamo terminato la demo, e finalmente possiamo generare il file da installare sul dispositivo Android.
Accertiamoci prima di tutto che il progetto TheCube sia impostato come main project (in caso possiamo cambiare questa opzione con il tasto destro). Facciamo click sull'icona verde [Run Main Project]
della toolbar, ed attendiamo la generazione del file. Dovremmo vedere fermarsi il processo di generazione con la seguente riga sulla console di outptut:
Waiting for device to be ready.. Connect your device now if its not connected yet.
questo perchè si rimane in attesa di avere un dispositivo collegato in ascolto.
Purtroppo come detto in precedenza le SDK Android sembrano non supportare OpenGL 2 e quindi non possiamo installare su nessun emulatore la nostra demo: scegliamo perciò la strategia di esportare il file apk generato, per importarlo e testarlo manualmente sul terminale. Possiamo fare click sulla freccia accanto alla barra [running]
in basso a destra e terminare il processo.
Il file generato è presente nel percorso [TheCube/mobile/bin]
all'interno della vostra cartella JMonkeyProjects
dove risiedono i progetti. Qui troverete il file TheCube-debug-unaligned.apk
, privo di firma digitale ma installabile sul dispositivo a patto di abilitare l'installazione di applicazioni di origine sconosciuta (nella sezione [Impostazione applicazioni]
del vostro telefono.
Potete salvare il file attraverso un collegamento USB con il vostro telefono ed avviare l'installazione. Al termine dell'installazione l'applicazione sarà visibile con un'icona verde di un cubo:
Per comodità abbiamo incluso nel file allegato oltre ai sorgenti del progetto anche l'esportaxzione apk, così da facilitare il debug.
Possiamo quindi avviare l'applicazione, attendere la visualizzazione della scena, e con soddisfazione vedere come il cubo risponde agli eventi:
Conclusioni
In questo articolo sono stati trattati davvero molti argomenti. Abbiamo visto come padroneggiare gli strumenti essenziali per dar vita ad una semplice scena: l'importanza del concetto di Ray e Quaternion, l'uso dell'input e le differenze tra Spatial
, Geometry
e Node
e come vedere tutto questo in azione su un dispositivo mobile. La strada per imparare a realizzare un gioco è naturalmente appena iniziata, ci sono ancora molte cose da imparare e studiare sul sito ufficiale di JMonkey, ma per il momento possiamo assaporare con soddisfazione,e forse anche con un pizzico di entusiasmo, i nostri primi risultati.