In questo articolo ho intenzione di parlare di quella che credo sia la parte più importante di ActionScript 3: gli Eventi.
Un Evento è un’azione che avviene solitamente al di fuori del ciclo di un programma, gestita da una porzione di codice all’interno del programma.
Quando si verifica un Evento, una porzione di codice lo intercetta e chiama una Funzione, che viene eseguita. Vediamo un esempio pratico:
- L’utente posiziona il puntatore del Mouse su un pulsante e fa click sul tasto sinistro.
- Il pulsante “sente” il click del Mouse e richiama una Funzione.
- La Funzione apre una nuova pagina web.
Il click del Mouse è l’Evento che vogliamo intercettare. Il pulsante è associato ad un Oggetto che è in grado di intercettare il click del Mouse e che per questo si chiama Listener (ascoltatore). La Funzione contiene il codice da eseguire, e prende il nome di Trigger (innesco).
In ActionScript abbiamo la possibilità di intercettare decine di Eventi legati al Mouse (il Click, il Doppio Click, il Movimento, la Pressione e il Rilascio del pulsante sinistro, il Trascinamento, eccetera), ma anche Eventi legati alla Linea Temporale, alla Tastiera e ad ogni altra Classe esistente; possiamo utilizzare tutti questi Eventi nelle nostre Classi e possiamo addirittura creare degli Eventi personalizzati.
Se questo concetto è chiaro, possiamo iniziare a scrivere la nostra nuova Classe. La Classe oggetto di questo Tutorial si chiamerà Player e contiene tutto il codice necessario alla gestione del serpente.
Definizione del Package e Importazione delle Classi utili.
package snake { import flash.display.Sprite; import flash.events.KeyboardEvent; import flash.events.TimerEvent; import flash.events.Event; public class Player extends Sprite { //... } }
Già è chiaro che in questa Classe utilizzeremo diversi Eventi, infatti abbiamo importato la Classe KeyboardEvent che contiene gli Eventi legati alla tastiera, la Classe TimerEvent che contiene gli Eventi legati ad un Timer, e la Classe Event che contiene Eventi generici.
Definizione delle Variabili
//package snake { // import flash.display.Sprite; // import flash.events.KeyboardEvent; // import flash.events.TimerEvent; // import flash.events.Event; // import flash.utils.Timer; // public class Player extends Sprite { private var _sArray:Array = new Array(); private var _sLength:uint; private var _dir:String; private var _nextDir:String; //... // } //}
Le prime due variabili servono per la gestione della lunghezza del serpente, le altre due servono a stabilire la direzione in cui deve muoversi.
_sArray è un Oggetto di tipo Array, tutti quelli che hanno un po’ di dimestichezza con un qualsiasi linguaggio di Scripting o di Programmazione sanno già che cosa significa, per tutti gli altri possiamo semplificare in questo modo:
Un Array è un contenitore per Oggetti di ogni Tipo. Ogni Elemento dell’Array è associato ad un Indice che parte da Zero, così il primo Elemento dell’Array sarà richiamato utilizzando Array[0], il terzo elemento sarà Array[2], l’ultimo Elemento sarà Array[Array.length – 1].
Utilizzeremo questo Array per memorizzare tutti i quadratini, Istanze di Square, che compongono il Serpente durante il gioco.
_sLength è un numero intero che useremo per ricordarci quanto deve essere lungo il nostro serpente, non utilizziamo direttamente _Array.length perché i due numeri indicano cose diverse:
- _sArray è l’elenco dei quadratini che fanno parte del Serpente, _sArray.length è quindi la lunghezza del Serpente in questo momento.
- _Length è la lunghezza che il Serpente deve avere, quando questo valore cabia dobbiamo aggiungere (o rimuovere) quadratini dall’Array _sArray.
_dir è la direzione che il Serpente sta seguendo, è di tipo String perché ho intenzione di utilizzare delle lettere per memorizzare questo valore:
- N (north) per indicare che il Serpente deve muoversi verso l’alto.
- S (south) per indicare che il serpente deve muoversi verso il basso.
- E (east) per indicare che il serpente deve muoversi verso destra.
- W (west) per indicare che il serpente deve muoversi verso sinistra.
_nextDir è la prossima direzione che i Serpente dovrà seguire, conterrà delle lettere allo stesso modo di _dir, con la differenza che _dir ci dice in che direzione il Serpente si sta muovendo in un determinato momento, _nextDir ci dice in che direzione andrà al passo successivo.
Metodo Costruttore
public function Player(initLenght:int = 5, initDir:String = 'E', initX:int = 1, initY:int = 1) { _dir = initDir; _sLength = initLenght; for (var li:uint = 0; li < _sLength; li++) { var p:Square = new Square(0x006699); p.x = p.w*(initX+(_dir=='E'?li:_dir=='W'?-1*li:0)); p.y = p.h*(initY+(_dir=='S'?li:_dir=='N'?-1*li:0)); _sArray.push(p); this.addChild(p); } this.addEventListener(Event.ADDED_TO_STAGE, isOnStage); }
Abbiamo già visto in precedenza dei Metodi Costruttori, sappiamo che:
- Devono avere lo stesso nome della Classe.
- Vengono eseguiti quando si instanzia una Classe (quindi questo Metodo verrà eseguito ogni volta che scriveremo “new Player();“).
Analizziamo nel dettaglio cosa fa questo Metodo:
public function Player(initLenght:int = 5, initDir:String = 'N', initX:int = 1, initY:int = 1) { }
La Funzione Player è Pubblica e può ricevere fino a 4 parametri. I quattro parametri sono:
- initLength, ovvero la lunghezza iniziale del Serpente, il suo valore di Default è 5.
- initDir, la direzione iniziale, il valore di Default è ‘N’, quindi il Serpente si muoverà verso l’alto se non specifichiamo altri valori.
- initX, la posizione orizzontale di partenza sulla griglia di gioco, il valore di Default è 1, che significa il primo quadratino da sinistra.
- initY, la posizione verticale di partenzasulla griglia di gioco, il valore di Default è 1, che significa il primo quadratino dall’alto.
Qundi con i valori di Default avremo un Serpente lungo 5 quadratini, posizionati a partire dal primo quadratino in alto a sinistra, che si muove verso l’alto.
Vedremo più avanti che modificando questi parametri possiamo far partire il nostro serpente da dove ci pare.
_dir = initDir; _sLength = initLenght;
Queste due righe assegnano semplicemente i valori iniziali alle Variabili _dir e _sLength.
for (var li:uint = 0; li < _sLength; li++) { }
Questo è un Ciclo, nello specifico è un Ciclo For. Mi riprometto di scrivere una lezione specifica per trattare questo argomento, adesso per andare avanti ci limitiamo alla sua definizione:
Un Ciclo For è una Struttura di controllo Iterativa, questo significa che ripete le stesse operazioni più volte. Si basa su tre Parametri: Contatore, Condizione e Incremento. Il Contatore è quasi sempre un Variabile numerica; la Condizione è un controllo che viene effettuato ad ogni iterazione, se il Controllo risulta positivo il Cliclo continua, altrimenti si ferma; l’Incremento modifica il valore del Contatore, dopo aver eseguito tutte le istruzioni.
Il Ciclo che stiamo usando utilizza una variabile chiamata li come Contatore e imposta zero come valore di partenza. La nostra Condizione è che li sia sempre inferiore al valore di _sLength. L’Incremento accresce di 1 il valore del Contatore (usando l’Operatore Incrementale “++”).
Nel dettaglio il nostro Ciclo sarà eseguito 5 volte:
- La variabile li vale 0, è minore di _sLength (che vale 5), esegue le operazioni e incrementa li.
- La variabile li vale 1, è minore di _sLength, esegue le operazioni e incrementa li.
- La variabile li vale 2, è minore di _sLength, esegue le operazioni e incrementa li.
- La variabile li vale 3, è minore di _sLength, esegue le operazioni e incrementa li.
- La variabile li vale 4, è minore di _sLength, esegue le operazioni e incrementa li.
- La variabile li vale 5, non è minore di _sLength (è uguale), non esegue nessuna operazione ed esce dal Ciclo.
Ma quali sono queste operazioni da eseguire 5 volte? Eccole:
var p:Square = new Square(0x006699); p.x = p.w*(initX+(_dir=='E'?li:_dir=='W'?-1*li:0)); p.y = p.h*(initY+(_dir=='S'?li:_dir=='N'?-1*li:0)); _sArray.push(p); this.addChild(p);
Crea una nuova Istanza di Square assegnandole il colore blu; imposta la posizione x e y del quadrato appena creato; conserva un riferimento del quadrato dentro l’Array _sArray utilizzando il metodo push; aggiunge il quadrato con addChild.
Il primo rigo credo che non abbia bisogno di spiegazioni, mentre è il caso di soffermarci sulle assegnazioni di x e y, che sembrano complicate, ma non lo sono.
La Posizione di ogni quadratino deve essere allineata alla Griglia invisibile del gioco. La Griglia in realtà non esiste, ma visto che i movimenti del Serpente sono obbligati e ogni passo ha la stessa dimensione di un quadratino, possiamo immaginare tutte le posizioni possibili, come in questo disegno:
Quelle due righe utilizzano il valore di w e h delle Istanze, il valore di initX e initY di questa Funzione e il valore del Contatore li del Ciclo per posizionare tanti quadratini uno accanto all’altro (o uno sopra all’altro).
Se volete maggiori chiarimenti su come lo fa postatelo nei Commenti, al momento mi sembra suprefluo soffermarmi su questa cosa.
addEventListener
L’ultima riga della Funzione imposta il primo dei nostri Listener, il Metodo utilizzato per assegnare un Listener è addEventListener e prevede due Parametri: il primo Parametro è il tipo di Evento che vogliamo “ascoltare“, il secondo Parametro è la Funzione che dobbiamo richiamare. La riga è questa:
this.addEventListener(Event.ADDED_TO_STAGE, isOnStage);
L’evento ADDED_TO_STAGE viene scatenato nel momento in cui posizioniamo un Elemento sullo Stage.
Ci sono delle operazioni che non possono essere eseguite subito, perché necessitano di un contesto particolare. Una di queste è il Riferimento allo Stage.
Una Classe, anche se ereditata dalla Classe Sprite, o da qualsiasi altro elemento di visualizzazione, non può riferirsi allo Stage a meno che non sia già visibile sullo Stage stesso. Per essere certi che lo Stage sia disponibile si utilizza l’Evento ADDED_TO_STAGE della Classe flash.events.Event.
Nel nostro caso, quando un’Istanza della CLasse Player sarà aggiunta allo Stage verrà richiamata la funzione isOnStage.
- La Funzione isOnStage (che non bbiamo ancora scritto) è il Trigger.
- All’istanza della CLasse Player è stato collegato un Listener.
- Ogni volta che aggiungeremo allo Stage un’Istanza di questa Classe (con il Metodo addChild) scateneremo l’Event.
removeEventListener
Subito dopo il Metodo Costruttore possiamo scrivere la funzione che sarà il Trigger dell’Event appena citato. La Funzione è questa:
public function isOnStage(e:Event) { stage.addEventListener(KeyboardEvent.KEY_DOWN, changeDir); }
L’istruzione addEventListener aggiunge allo stage un Listener per gli Eventi da Tastiera, in particolare l’Evento KEY_DOWN che significa “quando si preme un tasto“.
Analizziamo il Listener appena aggiunto:
- Il Trigger degli Eventi da Tastiera sarà la Funzione changeDir.
- Il Listener degli Eventi da Tastiera è collegato allo Stage (così che vengano intercettati gli Eventi su tutto lo Stage e non soltanto su Oggetti specifici).
- L’Event è qualunque pressione di un tasto sulla Tastiera.
La Funzione isOnStage, oltre ad essere eseguita, riceve anche un Parametro che è l’Evento che ha originato la sua esecuzione. Tutti i Trigger ricevono, come unico Parametro, l’Evento originario, anche se non lo utilizzamo è indispensabile specificarlo.
Utilizzare l’Evento dentro ad un Trigger
Ogni volta che premeremo un tasto sulla Tastiera verrà eseguito il Metodo changeDir, che è questo:
public function changeDir(e:KeyboardEvent) { switch(e.keyCode) { case 40: _nextDir = _dir != 'N' ? 'S' : 'N'; break; case 38: _nextDir = _dir != 'S' ? 'N' : 'S'; break; case 39: _nextDir = _dir != 'W' ? 'E' : 'W'; break; case 37: _nextDir = _dir != 'E' ? 'W' : 'E'; break; } }
Questa Funzione ha il solo compito di cambiare il valore della Variabile _nextDir utilizzando il codice del tasto premuto. La parte importante di questa Funzione è che non si limita a ricevere il Parametro e, ma ne utilizza una Proprietà chiamata keyCode.
Quasi tutti gli Eventi conservano delle Proprietà che possono essere recuperate all’interno delle Funzioni Trigger. Gli Eventi da Tastiera contengono informazioni sul tasto che li ha originati.
Ogni tasto della Tastiera corrisponde ad un codice numerico:
- Il Codice 37 appartiene al tasto Freccia Sinistra.
- Il Codice 38 appartiene al tasto Freccia Su.
- Il Codice 39 appartiene al tasto Freccia Destra.
- Il Codice 40 appartiene al tasto Freccia Giù.
L’Istruzione switch valuta se il keyCode dell’Evento (e.keyCode) è uguale ad uno di questi quattro valori e assegna di conseguenza i valori W, N, E e S alla Variabile _nextDir, controllando anche che la direzione attuale non sia opposta a quella desiderata (per esempio se il Valore attuale è N non può diventare S, perché il Serpente non deve fare retromarcia).
Trigger per Listener esterni
Non necessariamente i Trigger e i Listener devono appartenere alla stessa Classe. La prossima Funzione è un Trigger per un Listener di tipo TIMER che non si trova all’interno di questa Classe.
Quando scriveremo la Classe principale del gioco, nella prossima lezione, avremo un Timer che si occuperà di regolare la velocità del gioco. Ad ogni Intervallo del Timer di gioco il Serpente dovrà muoversi, quindi prepariamo già da subito la Funzione che si occuperà di questo movimento, anche se non ne vedremo gli effetti fino alla prossima lezione.
public function timeOn(e:TimerEvent) { _dir = _nextDir ? _nextDir : _dir; var offsetX = 0; var offsetY = 0; switch(_dir) { case 'E': offsetX = 1; break; case 'W': offsetX = -1; break; case 'N': offsetY = -1; break; case 'S': offsetY = 1; break; } //---------------- var p:Square = new Square(0x006699); p.x = head.x + p.w*(offsetX); p.y = head.y + p.h*(offsetY); p.x = p.x > stage.stageWidth ? 0 : p.x; p.y = p.y >stage.stageHeight ? 0 : p.y; p.x = p.x < 0 ? stage.stageWidth : p.x; p.y = p.y < 0 ? stage.stageHeight : p.y; this.addChild(p); _sArray.push(p); //---------------- while (_sArray.length > _sLength) { if (_sArray[0].parent) { _sArray[0].parent.removeChild(_sArray[0]); } _sArray.splice(0,1); } //---------------- if (hitTail(head)) { dispatchEvent(new Event("hit his own tail")); } }
Tutto quello che serve per la corretta gestione del Serpente è contenuto in questo Metodo. L’ho diviso in quattro parti utilizzando la linea tratteggiata, così da dividere concettualmente tutti i suoi compiti:
La prima parte:
si occupa di decidere la direzione in base al valore della variabile _nextDir. Assegna il valore alla Variabile _dir e in base alla lettera stabilisce i valori delle due Variabili offsetX e offsetY.
La seconda parte:
crea un nuovo quadratino utilizzando la Classe Square, lo aggiunge alla lista _sArray e lo mostra a video nella posizione giusta (sopra, sotto o accanto al Serpente, in base al valore di _dir).
La terza parte:
controlla se il Serpente è troppo lungo e taglia la coda rimuovendo i quadratini dallo schermo e il loro riferimenti dalla lista _sArray.
Quello che succede fino a questo punto è più comprensibile guardando questo disegno:
La quarta e ultima parte
controlla se il Serpente è andato a sbattere contro la propria coda, utilizzando una Funzione che scriveremo tra poco. Nel caso in cui avvenga questo scontro la nostra Istanza di Snake scatenerà un Evento personalizzato.
Questa parte è molto importante perché assegnando un Evento personalizzato potremo reagire al fatto che il Serpente ha sbattuto contro la propria coda semplicemente aggiungendo un Listener per questo Evento!
Il codice sarà simile a questo:
player.addEventListener('hit his own tail', functionGameOver);
Le parti più importanti sono finite, ci rimangono soltanto pochi Metodi…
Il Metodo che controlla se il Serpente ha sbattuto contro la propria coda (quello che abbiamo appena richiamato):
public function hitTail(square):Boolean { for (var i = 0; i < _sArray.length-1; i++) { if (square.hitTestObject(_sArray[i])) { return true; } } return false; }
I Getter per recuperare:
- Il quadratino che rappresenta la Testa.
- La Lunghezza del Serpente.
public function get head():Square { return _sArray[_sArray.length-1]; } public function get length():int { return _sLength; }
Il Setter per impostare la lunghezza del Serpente dall’esterno:
public function set length(l:int) { _sLength = l; }