Sign in / Join

Drag and drop e collisioni con createJS (prima parte)

0
Share:

Il trascinamento interattivo è una delle funzioni multimediali imprescindibili per la creazione di siti dinamici e instant game. Questo effetto visivo si può realizzare in vari modi, tuttavia, poiché l’articolo attuale fa parte di una guida su createJS in ambiente Animate, useremo come di consueto la libreria creata dal guru JavaScript Grant Skinner.
Nel corso di questo appuntamento vedremo anche altri due aspetti spesso associati al drag and drop, ovvero la gestione della profondità e le collisioni tra movieclip. Quando abbiamo la possibilità di trascinare più oggetti tramite il puntatore del mouse (o il tocco su un dispositivo touchscreen), abbiamo infatti bisogno di collocare l’elemento “agganciato” sempre in primo piano. Senza prendere questo accorgimento, rischieremmo di confondere visivamente l’utente, con l’oggetto trascinato che potrebbe nascondersi dietro altri elementi durante l’interazione.
Per quanto riguarda invece le collisioni, difficilmente ci ritroveremo a sviluppare un progetto interattivo dove l’elemento trascinabile non ha un bersaglio di riferimento. Che sia un gioco di carte, un puzzle o anche solo un menu, avremo sempre una zona precisa dove collocare quello che agganciamo.

Gestire la profondità 

La principale formula magica per gestire la profondità di uno o più oggetti collocati sullo stage è il metodo setChildIndex, la cui sintassi generale è: setChildIndex (object, index), dove per object intendiamo l’elemento di cui vogliamo cambiare la profondità e per index il valore numerico del nuovo livello.
Un altro metodo, più semplice ma non meno utile, attraverso il quale possiamo manipolare la profondità di un oggetto è swapChildren. In questo caso abbiamo una sintassi del tipo: swapChildren (child1, child2), attraverso la quale la profondità del primo oggetto (child1) viene scambiata con quella del secondo (child2).
Per non limitarci alla teoria vediamo un piccolo esempio pratico. A questo indirizzo è possibile osservare una pagina HTML5 canvas caratterizzata nella parte sinistra da 4 carte da gioco sovrapposte. Sulla destra abbiamo invece cinque pulsanti che se premuti modificano la profondità dell’asso di cuori.drag_fig1

Il file prevede delle semplici interpolazioni che posizionano quattro clip filmati sullo stage: a_quadri, a_cuori, a_fiori e a_picche. L’ordine di sovrapposizione iniziale è stato realizzato proprio grazie alle animazioni sulla linea temporale create in modo visuale. Di seguito vediamo il codice:

this.p1.on("click", sopra.bind(this));
function sopra(){
this.setChildIndex(this.a_cuori,this.numChildren-1);
}
this.p2.on("click", sotto.bind(this));
function sotto(){
this.setChildIndex(this.a_cuori,0);
}
this.p3.on("click",swap1.bind(this));
function swap1(){
this.swapChildren(this.a_cuori,this.a_fiori);
}
this.p4.on("click",swap2.bind(this));
function swap2(){
this.swapChildren(this.a_cuori,this.a_picche);
}
this.p5.on("click",swap3.bind(this));
function swap3(){
this.swapChildren(this.a_cuori,this.a_quadri);
}

In pratica, per collocare l’asso di cuori in primo piano diamo come valore index del metodo setChildIndex() il valore numerico restituito da numChildren-1. Mentre per portare l’oggetto sullo sfondo diamo all’argomento index il valore 0.
Per quanto riguarda invece la profondità variata tramite swapChildren(), il codice è ancora più intuitivo, basta scrivere i nomi degli oggetti per effettuare lo scambio. Non importa se gli oggetti si trovano vicini nell’ordine di sovrapposizione o meno. Basta scrivere i relativi nomi ed il gioco è fatto.

Gestire le collisioni con hitTest 

La buona notizia è che questo metodo è molto simile al suo omonimo in AS3, per cui se siete a vostro agio il linguaggio di Adobe AIR, usarlo sarà una passeggiata. La cattiva notizia è che non è uno strumento ideale per tutte le situazioni. Purtroppo la versione in createJS va bene per collisioni semplici, ideali quando si progettano dei giochi come i puzzle o giochi di carte. Tuttavia, se le collisioni sono veloci e gli oggetti molto numerosi, è preferibile utilizzare altre soluzioni.
Detto questo, volendo andare al succo del discorso, il metodo hitTest prevede la possibilità di analizzare due tipologie di collisioni:

  • con il mouse mosso dall’utente;
  • con un oggetto in movimento (non importa se trascinato o animato diversamente).

In entrambi i casi avremo una sintassi del tipo:

myDisplayObject.hitTest(localx, localy);

Dove myDisplayObject è l’oggetto per il quale vogliamo rilevare eventuali collisioni e le variabili localx e localy sono invece le coordinate relative a tutto ciò che tocca myDisplayObject. Per la precisione, le coordinate in questione possono appartenere al mouse oppure ad un secondo oggetto che entra in collisione con il primo.
L’istruzione hitTest, una volta verificata l’avvenuta collisione, restituisce un valore booleano true. Viceversa, in caso di mancata collisione, restituisce false. Di conseguenza, a prescindere dal tipo di progetto che andremo a sviluppare, il nostro codice dovrà sempre essere inglobato in un’istruzione condizionale che verificherà tramite un evento la collisione. Per cui avremo una sintassi del tipo:

on (“evento”, chiamata_evento);
funzione chiamata_evento(){
if (myDisplayObject.hitTest(localx, localy) ) {

// fai qualcosa
}
}

Esaurita l’inevitabile dose di chiacchiere passiamo agli esempi. A questo indirizzo è possibile osservare un esempio basato sulle interazioni del mouse. Come è possibile notare osservando la pagina HTML5, abbiamo un elemento a forma di lampadina posto al centro della scena. drag_fig2

Quando l’utente avvicina il mouse al clip filmato, un’istruzione creatJS muove l’indicatore di riproduzione della sua linea temporale interna. Così facendo, il clip scatterà in un fotogramma nel quale la parte centrale del disegno cambia colore: in questo modo simuleremo l’effetto lampadina accesa.
Qualcuno potrebbe giustamente obiettare: per quale motivo scomodare le collisioni, quando ci troviamo di fronte ad un normale rollover? Non potremmo ottenere lo stesso risultato con un codice che semplicemente rileva il passaggio del mouse?
In realtà la collisione interagisce con il clip filmato in modo molto più preciso. Un normale rollover implicherebbe un’area attiva rettangolare che avvolge tutto il clip filmato. Il metodo hitTest ci garantisce invece la possibilità di attivare l’effetto solo quando il mouse (o il tocco) incontra esattamente l’oggetto. Questo vuol dire che se collochiamo il puntatore nelle zone vuote del disegno, hitTest restituirà false e non otterremo l’effetto “acceso”. Una differenza poco importante se dobbiamo creare un semplice pulsante, ma un bel vantaggio se stiamo invece sviluppando un webgame in HTML5. A questo punto diamo uno sguardo al codice:

this.on("tick",hit.bind(this));
function hit() {
this.bersaglio.gotoAndStop(0); 
var local = stage.localToLocal(stage.mouseX,stage.mouseY,this.bersaglio);
if (this.bersaglio.hitTest(local.x, local.y)) { 
this.bersaglio.gotoAndStop(1); 
}
}

Come abbiamo anticipato, l’istruzione che rileva la collisione è inglobato in un if a sua volta richiamato dal metodo tick. Come vedremo più avanti in questo stesso articolo, ci sono anche altri sistemi per monitorare l’istruzione condizionale, tuttavia l’uso di tick è il più accurato ma anche il più dispendioso in termini di risorse.
La lampadina dell’esempio è costituita da un clip filmato il cui nome di istanza è bersaglio. Quando l’indicatore di riproduzione è sul fotogramma 0 di bersaglio abbiamo l’effetto spento, viceversa, quando si trova sul fotogramma 1 otteniamo l’effetto acceso.
Osservando il codice, è possibile notare l’uso di una variabile denominata local il cui scopo è quello di creare un riferimento alle coordinate del mouse. Si tratta di un passaggio obbligato per poter rendere efficace la collisione. Quando si rileva la collisione di un oggetto tramite le coordinate del mouse, è necessario usare prima il metodo localToLocal per convertirle da globali a locali. Questo perché, mentre le coordinate del mouse sono da reputare globali, quelle riferite al clip sono locali. Quasi come due sistemi metrici diversi che prima di essere confrontati tra di loro hanno bisogno di essere convertiti. Il metodo localToLocal prevede tre argomenti: le due coordinate dell’oggetto che vogliamo far collidere con l’oggetto target, e il nome dello stesso oggetto target.
Come anticipato, esiste un altro tipo di collisione basata sullo scontro tra due diversi oggetti. Trovate un piccolo esempio concreto a questo indirizzo.drag_fig3

Anche questa volta abbiamo nella parte centrale dello stage un clip filmato denominato bersaglio caratterizzato dal disegno di una lampadina. Sulla sinistra abbiamo invece un secondo clip filmato denominato mioclip, che tramite un’interpolazione movimento si muove da sinistra verso destra ciclicamente. Quando mioclip incontra bersasglio, mette in moto la testina di riproduzione di quest’ultimo creando un effetto lampadina accesa analogo a quello descritto in precedenza (per eventuali dubbi su come sono strutturate le varie linee temporali, basta dare un’occhiata ai sorgenti che trovate alla fine dell’articolo). Di seguito ecco il codice dell’esempio:

this.on("tick", miotick.bind(this));
function miotick(event) {
this.bersaglio.gotoAndStop(0);
var local = this.mioclip.localToLocal(0, 0, this.bersaglio);
if (this.bersaglio.hitTest(local.x, local.y)) {
this.bersaglio.gotoAndStop(1);
}
}

Come si può notare abbiamo lo stesso codice visto nell’esempio precedente. Cambia solo la riga che converte le coordinate da globali a locali. In pratica, quando usiamo il metodo localToLocal, questa volta non ci riferiamo allo stage ma a this.mioclip, ovvero all’oggetto che deve scontrarsi con il nostro bersaglio. Inoltre le coordinate che vogliamo convertire vengono indicate tramite il valore numerico di partenza (0).

Gestire il drag and drop con createJS (leggi la nota alla fine)

Esaurite tutte le premesse, possiamo finalmente affrontare il drag and drop. Quando si crea una pagina HTML5 con createJS è necessario agganciare l’oggetto che vogliamo spostare alle coordinate dell’oggetto mouse. Un esempio molto utile per vedere in azione un drag and drop gestito da createJS è disponibile nel pannello Snippet di codice alla voce Cursore mouse personalizzato presente nella sottosezione Azioni. Tuttavia quell’esempio, per quanto utile, non tiene conto dei vari aspetti tecnici di cui abbiamo parlato nel corso di questo articolo.
A questo indirizzo è disponibile un piccolo esempio che tira le fila del discorso fatto fino ad ora.

NOTA: Gli esempi in questione non sono ottimizzati per i dispositivi mobili. Trovate tutte le indicazioni su come ovviare al problema nella seconda parte di questo articolo.

drag_fig4

Nell’esempio abbiamo quattro movieclip caratterizzati da quattro carte da gioco inclinate e sovrapposte. Ogni singolo oggetto, trascinabile tramite il mouse, può essere collocato in una sua “area target” collocata nella parte alta dello stage. Per individuare l’area di riferimento basta associare l’asso al seme corrispondente: cuori con cuori, quadri con quadri e così via.
Se una carta da gioco viene rilasciata nell’area corretta, si raddrizza e perde la possibilità di essere trascinata nuovamente. Inoltre ad ogni clic l’elemento trascinato è sempre in primo piano: in questo modo l’utente vede sempre cosa sta trascinando senza sovrapposizioni indesiderate. Anche in questo caso, come nel primo esempio di questo articolo, le carte da gioco entrano sulla scena grazie a delle brevi interpolazioni. Di seguito il codice del nostro esempio:

createjs.Touch.enable(stage);
// quadri
this.a_quadri.b=this.bersaglio_q;
this.a_quadri.on("pressmove", startdrag.bind(this));
this.a_quadri.on("pressup",stopdrag.bind(this));
// picche
this.a_picche.b=this.bersaglio_p;
this.a_picche.on("pressmove", startdrag.bind(this));
this.a_picche.on("pressup",stopdrag.bind(this));
// fiori
this.a_fiori.b=this.bersaglio_f;
this.a_fiori.on("pressmove", startdrag.bind(this));
this.a_fiori.on("pressup",stopdrag.bind(this));
// cuori
this.a_cuori.b=this.bersaglio_c;
this.a_cuori.on("pressmove", startdrag.bind(this));
this.a_cuori.on("pressup",stopdrag.bind(this));
// funzioni
function startdrag(e) {
this.setChildIndex(e.currentTarget,this.numChildren-1); 
var local = stage.globalToLocal(stage.mouseX,stage.mouseY,e.currentTarget);
e.currentTarget.x=local.x;
e.currentTarget.y=local.y;
stage.update();
}
function stopdrag(e){
var local = e.currentTarget.b.localToLocal(0,0,e.currentTarget);
if (e.currentTarget.hitTest(local.x,local.y)) { 
e.currentTarget.x=e.currentTarget.b.x;
e.currentTarget.y=e.currentTarget.b.y;
e.currentTarget.rotation=0; 
e.currentTarget.removeAllEventListeners("pressmove");
}
}

La prima riga di codice attiva gli eventi touch. Senza questa istruzione la pagina HTML5 non è in grado di intercettare correttamente le interazioni dell’utente quando sono generate tramite smartphone o tablet. Ci sarebbero altre cosette da dire su come ottimizzare un gioco per gli smartphone, ma conto di tornarci più avanti in modo da non mettere troppa carne al fuoco.
A seguire, il listato è stato suddiviso in base al tipo di carta da gioco. Per cui abbiamo quattro blocchi di codice (uno per ogni asso) e due funzioni generiche usate da tutti gli oggetti trascinabili.
Cominciamo ad analizzare le tre istruzioni relative all’asso di quadri. La prima riga del blocco associa all’oggetto a_quadri una proprietà b. Siccome createJS non è fortemente tipizzato, possiamo facilmente associare delle proprietà agli oggetti visivi usando la semplice sintassi del punto.

this.a_quadri.b=this.bersaglio_q;

Alla proprietà b viene passato il percorso del clip che funge da bersaglio, ovvero bersaglio_q. Questo trucchetto ci consentirà di fare riferimento al clip in questione senza bisogno di chiamarlo per esteso tutte le volte. In altre parole, per individuare la zona dove posizionare la carta da gioco ci basterà usare una sintassi del tipo: this.a_quadri.b.
La seconda istruzione del blocco associa al clip filmato una funzione listener denominata startdrag. Tale funzione verrà innescata dall’evento pressmove, che si attiva quando si clicca su un oggetto e si muove senza rilasciare il mouse (o le dita).
La terza e ultima istruzione del blocco associa al clip filmato la funzione listener stopdrag. Questa funzione attivata al rilascio del mouse, oltre ad interrompere il trascinamento interattivo, centra la carta da gioco nella posizione corretta.
Le stesse istruzioni si ripetono per le altre tre carte da gioco cambiando solo i nomi delle istanze. A questo punto non ci resta che analizzare meglio le due funzioni che regolano il trascinamento interattivo di  tutte le carte da gioco.
Cominciamo dalla funzione startdrag che per comodità incollo nuovamente.

function startdrag(e) {
this.setChildIndex(e.currentTarget,this.numChildren-1); 
var local = stage.globalToLocal(stage.mouseX,stage.mouseY,e.currentTarget);
e.currentTarget.x=local.x;
e.currentTarget.y=local.y;
stage.update();
}

Prima di tutto usiamo l’argomento e in modo da poter ricorrere all’istruzione e.currentTarget per ottenere il nome dell’oggetto con una sintassi relativa. Questa soluzione ci permette di evitare la scrittura di una funzione per ogni singolo clip filmato: l’oggetto trascinato ricava da solo nome e percorso.
Le istruzioni interne sono abbastanza intuitive, tramite setChildIndex manteniamo l’oggetto sempre in primo piano e tramite l’operatore di assegnazione passiamo alle sue coordinate quelle del mouse. L’unico passaggio in più riguarda l’uso della variabile local per convertire le coordinate da globali a locali. Passaggio inevitabile per garantire un corretto funzionamento sui dispositivi mobili.

Nota: per evitare un rallentamento in google chrome, è necessario usare il metodo update() dell’oggetto stage. In teoria, quando si usa Animate per creare un pagina in HTML5 non sarebbe necessario.  Pare che il browser di google abbia bisogno di questa istruzione per forzare un ulteriore update dello stage. Tuttavia questa soluzione non è del tutto ottimizzata per i dispositivi mobili. Vedi la nota alla fine dell’articolo.

Passiamo all’istruzione stopdrag.

function stopdrag(e){
var local = e.currentTarget.b.localToLocal(0,0,e.currentTarget);
if (e.currentTarget.hitTest(local.x,local.y)) { 
e.currentTarget.x=e.currentTarget.b.x;
e.currentTarget.y=e.currentTarget.b.y;
e.currentTarget.rotation=0; 
e.currentTarget.removeAllEventListeners("pressmove");
}
}

Anche in questo caso usiamo l’argomento e per ottenere il percorso dell’oggetto rilasciato senza ricorrere ad una sintassi assoluta. Per il resto abbiamo una semplice istruzione hitTest analoga a quella vista negli esempi precedenti. Quando l’utente rilascia il mouse, viene verificata la collisione con il clip che funge da bersaglio. Se la condizione è soddisfatta, la carta da gioco assume le stesse coordinate del clip con il quale è avvenuta la collisione e fissa il valore della proprietà rotation su 0. Infine viene rimosso l’evento pressmove associato alla carta da gioco per impedire ulteriori movimenti.
Infine, non bisogna dimenticare un passaggio molto importante prima di pubblicare. Poiché (nel momento in  cui scrivo questo articolo) Google Chrome ha qualche problema nel gestire correttamente i canvas derivato da disegni sul mobile, per evitare fastidiosi rallentamenti, bisogna attivare per ogni carta da gioco l’opzione Memorizza in cache come bitmap che si trova nel pannello Proprietà. L’alternativa è quella di usare solo immagini bitmap all’interno dei clip filmati.

Chi fosse interessato, può scaricare tutti i sorgenti dal seguente indirizzo:

scarica i sorgenti.

A fronte di alcuni rallentamenti riscontrati testando gli esempi sui dispositivi mobili, questo articolo prevede una seconda parte dove spiego come ottimizzare il trascinamento interattivo per smartphone e tablet. Lo trovate QUI

Share:

Leave a reply

*