Ottimizzare un gioco HTML5 per i dispositivi mobili
Poco meno di un annetto fa, introdussi in questo articolo un puzzle in HTML5 realizzato con Adobe Animate. Pochi mesi dopo, blog con annesso autore sparirono in un wormhole caratterizzato da rotture di scatole varie impegni lavorativi. Per cui, nonostante le promesse fatte all’epoca, non ho ancora spiegato come ottimizzare il gioco per il mobile. Visto che le soluzioni tecniche in oggetto non sono invecchiate, e che l’articolo giace nelle bozze di wordpress a prendere la polvere, ho deciso di sfoggiare la mia faccia di bronzo brevettata © e chiudere il discorso come se niente fosse nonostante 10 mesi di ritardo.
Come ho già scritto negli articoli precedenti, se ottimizzare un gioco per i diversi browser richiede una certa attenzione, il discorso si complica ulteriormente quando testiamo il nostro lavoro per il mobile. Per fare un esempio, il comportamento dell’ultima versione di Firefox per Windows, potrebbe non coincidere con l’ultima versione dello stesso browser su android. E a prescindere dai simulatori online free e a pagamento, l’unico modo per testare in modo efficace il nostro gioco consiste nell’installare più browser su più dispositivi e armarsi di pazienza. Ma a prescindere dall’inevitabile testing, ci sono una serie accorgimenti che è necessario prendere in fase progettuale, vediamoli assieme.
Rendere il gioco fullscreen responsive
Un modo efficace per migliorare la user experience del giocatore su smartphone è quello di sfruttare al massimo tutto lo spazio disponibile. Per raggiungere lo scopo possiamo forzare il gioco ad occupare tutto lo spazio e allo stesso tempo nascondere tutte le barre del browser attivando la modalità fullscreen. Nel nostro esempio useremo il codice descritto in questo articolo, che illustra appunto come mettere in atto questo effetto visivo.
Chiaramente, la tecnica in questione potrebbe distorcere gli elementi grafici pensati per essere visti su schermi più piccoli. Per cui, mentre va tutto bene sul telefonino, la resa potrebbe lasciare a desiderare su un monitor. Una possibile soluzione al problema consiste nello sviluppare due versioni della stesso gioco: una versione responsive “forte” pensata per il mobile e una tipologia “light” pensata per i computer. Poi, una volta pubblicate le due versioni, usare uno sniffer per verificare il tipo di dispositivo usato dall’utente e stabilire dove direzionarlo. Vedremo un esempio completo di sniffer alla fine dell’articolo.
Come dovrebbe essere un puzzle game completo
Come ho già avuto modo di accennare, il puzzle visto nel nostro esempio è solo un prototipo di gioco. Manca un contatore del tempo disponibile che possa mettere in difficoltà il giocatore, un numero consono di livelli da superare e altri piccoli ritocchi. Tuttavia in questa versione mi sono divertito ad aggiungere meccanismo imprescindibile in tutti i giochi pensati per il mobile ovvero il menu di navigazione: che sia destinato per il mobile o un normale computer, l’utente deve sempre avere la possibilità di uscire o ricominciare il gioco tramite un apposito menu.
Inoltre, per rendere meno complicate le operazioni di trascinamento quando si gioca su uno smartphone, ho cambiato la disposizione dei pezzi dopo la partenza del gioco. Nella prima versione erano collocati in modo del tutto casuale. Adesso, sono disposti in modo altrettanto casuale, ma allineati lungo i bordi dell’area di gioco (vedi immagine in alto).
La struttura del file sorgente
Rispetto alla versione precedente del puzzle abbiamo tre livelli in più: schermo, p_principale ed opzioni. Il primo dei tre contiene un clip filmato denominato msg. Come abbiamo visto nello scorso tutorial, si tratta di un scritta che copre tutti i livelli e che comunica al giocatore se deve o meno ruotare il dispositivo.
Poiché nei livelli sottostanti si trovano degli oggetti sensibili al tocco (i pezzi del puzzle), per evitare che quando appare il messaggio l’utente li muova inavvertitamente toccando lo schermo, è stata attivata l’opzione Pulsante nel menu Comportamento istanza del pannello Proprietà. In altre parole, vale la vecchia regola presente nelle precedenti incarnazioni del programma: un pulsante che copre altri pulsanti annulla tutte le altre interazioni. Quindi quando appare il messaggio, l’utente può toccare lo schermo senza creare interazioni con gli elementi sottostanti (il tutto senza aggiungere una riga di codice).
Ma il vero cambiamento riguarda l’introduzione di un menu a scorrimento. In ogni gioco che si rispetti l’utente deve sempre poter mettere in pausa, ricominciare da zero oppure uscire. Per cui, anche se si tratta solo di un esempio, non potevano mancare i controlli essenziali.
Il livello p_principale contiene il pulsante (denominato p_menu) che attiva lo scorrimento del nostro menu. Una volta avviato il gioco questo pulsante – tranne quando è presente il clip msg – sarà sempre visibile.
Infine nel livello opzioni abbiamo il clip filmato menu_smart che costituisce il menu vero e proprio.
Si tratta di un oggetto a scatole cinesi: un clip filmato denominato menu_smart che ne contiene un altro chiamato mn che a sua volta contiene 4 pulsanti. Il clip mn si muove tramite una classica interpolazione movimento che lo fa scorrere da destra verso sinistra in entrata e nella direzione opposta in uscita (vedi la linea temporale nell’immagine alto). Nella linea temporale interna di mn ci sono quattro pulsanti (pm2, pm3, pm4 e pm5) che servono ad eseguire questi compiti: dare delle informazioni sul gioco tramite una didascalia, riavviare il gioco da zero, chiudere il menu ed infine uscire dal puzzle.
Il codice del gioco
La parte iniziale del codice presente nel fotogramma azioni fonde tra di loro i listati dei due tutorial precedenti: abbiamo il drag and drop basato sull’evento tick e il codice responsive fullscreen. Nel sorgente che è possibile scaricare alla fine di questo articolo trovale il listato suddiviso in varie porzioni delimitate da appositi commenti. Si tratta delle solite istruzioni già viste e commentate, quindi saltiamo le prime 125 righe di codice e passiamo al blocco di codice che ha il compito di gestire il posizionamento dei pezzi in avvio:
// --------------------------------------------------------- // codice posizionamento pezzi // --------------------------------------------------------- function muovi(target,posx,posy,t,tw){ var tween = createjs.Tween.get(target,{loop: false}) .to({x:posx, y:posy,}, t,tw) } this.centro=function(){ for(var cl=0; cl<=23; cl++ ){ muovi(this["pezzo"+cl],cnx,cny,2000,createjs.Ease.elasticOut); } } function rn(minimo,massimo){ return Math.round(Math.random()*(massimo-minimo))+minimo; } // array delle coordiante finali var gruppo=new Array(); function creagruppo(){ gruppo[0]={x:644,y:416}; gruppo[1]={x:124,y:80}; gruppo[2]={x:228,y:64}; gruppo[3]={x:330,y:58}; gruppo[4]={x:430,y:70}; gruppo[5]={x:535,y:53}; gruppo[6]={x:650,y:69}; gruppo[7]={x:750,y:68}; gruppo[8]={x:110,y:152}; gruppo[9]={x:674,y:145}; gruppo[10]={x:80,y:228}; gruppo[11]={x:126,y:308}; gruppo[12]={x:740,y:188}; gruppo[13]={x:55,y:326}; gruppo[14]={x:674,y:245}; gruppo[15]={x:730,y:278}; gruppo[16]={x:678,y:340}; gruppo[17]={x:52,y:420}; gruppo[18]={x:134,y:415}; gruppo[19]={x:223,y:417}; gruppo[20]={x:325,y:410}; gruppo[21]={x:432,y:417}; gruppo[22]={x:543,y:408}; gruppo[23]={x:718,y:380}; } creagruppo(); // funzione che mescola gli elementi dell'array gruppo var nuovoarray= new Array(); function mix(){ while (gruppo.length > 0){ var miorandom = Math.floor(Math.random()*gruppo.length); nuovoarray.push(gruppo[miorandom]); gruppo.splice(miorandom,1); } gruppo=nuovoarray; } // funzione che attribuisce le coordinate this.rand=function(){ mix(); for(var cl=0; cl<=23; cl++ ){ var rx=gruppo[cl].x; var ry=gruppo[cl].y; muovi(this["pezzo"+cl],rx,ry,2000,createjs.Ease.cubicOut); this["pezzo"+cl].on("mousedown", startdrag.bind(this)); this["pezzo"+cl].on("pressup", stopdrag.bind(this)); this["pezzo"+cl].b=this["pezzo_base"+cl]; } }
Come ho già anticipato, rispetto alla prima versione del puzzle, i pezzi vengono posizionati lungo i bordi dello stage. Per ottenere questo risultato, è stato creato un array denominato gruppo che memorizza 24 possibili coordinate. Le posizioni sono state ricavate manualmente posizionando i clip uno allo volta lungo i bordi e poi leggendo il valore nel pannello proprietà di Animate. Per fare un esempio il primo elemento dell’array restituiscono le coordinate 644 e 416, valori che vengono restituiti rispettivamente dalle espressioni gruppo[0].x e gruppo[0].y.
Chiaramente, se ci limitiamo a calcolare le coordinate da associare ai vari elementi con una sintassi del tipo:
for(var cl=0; cl<=23; cl++ ){
this[“pezzo”+cl].x=gruppo[cl].x;
this[“pezzo”+cl].y=gruppo[cl].y;
}
avremo un disposizione che non cambia mai. Per cui, il primo clip avrà sempre coordinate indicate nel primo elemento dell’array gruppo, il secondo clip quelle indicate del secondo elemento e così via. Per questo motivo, prima associare i valori contenuti nell’array ai clip filmati, useremo la funzione mix() per mescolare l’ordine degli elementi in modo casuale. Per cui, il primo pezzo del puzzle sarà sempre posizionato in alto a sinistra alle coordinate 644,416, ma quale sarà il primo pezzo dell’elenco sarà stabilito di volta in volta in modo casuale.
Per ulteriori approfondimenti sui numeri casuali, rimando a questo mio vecchio articolo, dove spiego la logica alla base dei giochi basati su questo principio. L’articolo fa riferimento ad AS3, ma sintassi e logica progettuale cambiano pochissimo.
Passiamo quindi ad un altra porzione di codice assente nella precedente versione dell’esempio:
// ---------------------------------------------------------
// gestione pulsante start e menu a scorrimento
// ---------------------------------------------------------
// pulsante start
this.p_start.on("click", attivaFullscreen.bind(this));
window.addEventListener("orientationchange",controllo.bind(this), false);
// ingresso e uscita del menu
this.p_menu.on("click",ingresso.bind(this));
function ingresso(){
if(this.menu_smart.currentFrame==0){
this.menu_smart.gotoAndPlay(1);
}
else if(this.menu_smart.currentFrame==14){
this.menu_smart.gotoAndPlay(15);
}
}
// pulsanti del menu
this.menu_smart.mn.msinfo.visible=false;
this.menu_smart.mn.pm2.on("click",info.bind(this));
function info(){
this.menu_smart.mn.msinfo.visible=true;
}
this.menu_smart.mn.msinfo.on("click",hide_info.bind(this));
function hide_info(){
this.menu_smart.mn.msinfo.visible=false;
}
this.menu_smart.mn.pm3.on("click",ripartenza.bind(this));
this.menu_smart.mn.pm4.on("click",chiudi2.bind(this));
function chiudi2(){
exportRoot.menu_smart.gotoAndPlay(15);
}
this.menu_smart.mn.pm5.on("click",uscita.bind(this));
function uscita(){
disattivaFullscreen();
}
// porta sopra pulsante start e menu
this.sopra=function(){
this.setChildIndex(this.menu_smart,this.numChildren-1);
this.setChildIndex(this.p_menu,this.numChildren-1);
}
La funzione ingresso() attivata dal clic sul pulsante p_menu, ha il compito di aprire o chiudere il menu delle opzioni. In pratica, fa scorrere da destra verso sinistra il menu se è chiuso, e da sinistra verso destra (fuori dall’area visibile) se è aperto. La funzione legge in quale fotogramma si trova l’indicatore di riproduzione e valuta se aprire oppure chiudere. Così facendo sarà possibile usare sempre lo stesso pulsante per controllare il menu. Infine, la funzione sopra(), che ha il compito di collocare in primo piano il menu delle opzioni e relativo pulsante, viene chiamata da questa istruzione:
exportRoot.sopra();
che si trova nella linea temporale del menu di scorrimento (il secondo fotogramma chiave nell’immagine in alto).
Il prossimo blocco di codice ha invece il compito di riavviare il gioco quando l’utente preme il pulsante pm3.
// ---------------------------------------------------------
// codice per ricominciare il gioco
// ---------------------------------------------------------
var i_gruppo=new Array();
for(var cl=0; cl<=23; cl++ ){
i_gruppo[cl]={x:this["pezzo"+cl].x,y:this["pezzo"+cl].y};
}
function ripartenza(){
disattivaSemplice();
this.base.visible=false;
pezzi=0;
this.p_start.visible=true;
this.menu_smart.gotoAndStop(0);
this.foto.gotoAndStop(0);
this.base.gotoAndStop(0);
this.scritta.gotoAndStop(0);
nuovorray=[];
gruppo=[];
creagruppo();
exportRoot.mostra_pezzi();
this.setChildIndex(exportRoot.foto,this.numChildren-1);
this.setChildIndex(exportRoot.p_start,this.numChildren-1);
this.setChildIndex(exportRoot.msg,this.numChildren-1);
for(var c=0; c<=23; c++ ){
this["pezzo"+c].removeAllEventListeners();
this["pezzo"+c].removeAllEventListeners("tick");
}
for(var cl=0; cl<=23; cl++ ){
this["pezzo"+cl].x=i_gruppo[cl].x;
this["pezzo"+cl].y=i_gruppo[cl].y;
this["pezzo"+cl].visible=this["pezzo"+cl].b.visible=true;
}
}
// disattiva il fullscreen senza lasciare la pagina
function disattivaSemplice() {
if(document.exitFullscreen) {
document.exitFullscreen();
} else if(document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if(document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
Il codice in questione, molto intuitivo, svolge il suo compito in due casi: sia quando l’utente decide di ricominciare la partita prima di ultimarla, sia quando il gioco è finito. In entrambi i casi, tutti gli indicatori di riproduzione dei vari clip vengono collocati nella posizione di partenza (scritta conclusiva e menu). Gli array vengono ripristinati alla loro condizione iniziale, gli eventi attivi rimossi e i vari pezzi (nascosti durante il gioco) impostati come visibili.
Prepariamo lo Sniffer
A questo punto il gioco sarebbe completo, ma resta un ultimo passo. Per fare in modo che l’immagine usata risulti sgranata sul monitor di un pc è possibile ricorrere ad una doppia versione del gioco. In realtà si potrebbe studiare un grafica fullscreen adatta per tutti i dispositivi, ma potremmo voler lasciare dello spazio libero per banner o altri elementi nel caso in cui l’utente navighi tramite computer. Insomma, come ho scritto fino alla nausea, questo è solo un esempio. Alla fine, nel caso di un progetto reale, bisognerà valutare quale strategia adottare.
Nella cartella degli esempi che ho messo a disposizione alla fine dell’articolo si trovano quindi tre file html: index.html, pzh5smartphone.html e pzh5desktop.html. Il primo file, tramite un semplicissimo comando JavaScript, verifica quale dispositivo sta usando l’utente e lo rendirizza in base al risultato. Ecco il codice in questione:
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) { // nel caso di un dispositivo mobile location.href = "pzh5smartphone.html"; } else { // nel caso di un computer location.href = "pzh5desktop.html"; }
Come è possibile dedurre osservando il breve listato, il primo if verifica i possibili dispositivi mobili usati dall’utente e in caso di risultato positivo lo reindirizza alla versione mobile. Se invece la condizione non è soddisfatta, la pagina di destinazione sarà quella pensata per i normali monitor.
A questo riguardo, la versione per computer del gioco, fatta eccezione per il menu delle opzioni, è in tutto e per tutto identico all’esempio spiegato in precedenza.
L’esempio online lo trovate a questo indirizzo. Invece, questo è il link per scaricare i sorgenti.
In conclusione
Chiaramente, quello appena illustrato non è l’unico metodo per rilevare il dispositivo usato dall’utente. Ci sono altre soluzioni basate su diversi modi di usare JavaScript o anche linguaggi server side. Analogo il discorso per il gioco, che resta a tutti gli effetti solo un piccolo prototipo. E con questo concludo per ora il mio piccolo corso introduttivo a createJs in ambiente Animate. Ci sarebbero ancora tantissime cose da dire, ma non voglio monopolizzare il blog parlando di un solo argomento.