Questa è la traduzione dell'articolo HTML5 Gaming: animating sprites in Canvas with EaselJS di David Rousset pubblicato originariamente il 21 Luglio 2011 su MSDN Blog. La traduzione viene qui presentata con il consenso dell'editore.
Quando si creano giochi usando l'elemento Canvas di HTML5 bisogna trovare il modo di gestire le forme (gli sprite). Ci sono diverse librerie che consentono di semplificare la creazione di giochi come ImpactJS, CraftyJS e altre. Io ho deciso di usare EaselJS, utilizzata per creare PiratesLoveDaisies, un eccellente gioco in HTML5 del tipo "difendi la torre". In questo articolo vedremo come usare gli sprite che si hanno a disposizione e come animarli.
Introduzione
Sul sito ufficiale di EaselJS si potranno trovare esempi e documentazione di base. Noi useremo alcuni esempi base di sprite assieme ad altre risorse disponibili nell'esempio XNA 4.0 Platformer.
Se seguite il mio blog, probabilmente sapete che spesso ho utilizzato questi esempi. Li ho usati, ad esempio, in due precedenti articoli:
- Windows Phone 7 Platformer Starter Kit for XNA Studio 4.0
- Silverlight 4 XNA Platformer Level Editor for Windows Phone 7
Il Platformer sample è stato aggiornato dal team XNA ed è disponibile per Xbox 360, PC e Windows Phone 7 (App Hub – platformer). Scaricatelo ed estraete gli sprite da usare con EaselJS.
Userò due file PNG come sequenza di sprite.
Un mostro che corre, suddiviso in 10 sprite.
Scarica l'originale da usare negli esempi.
E il mostro in posizioni di riposo, suddiviso in 11 sprite diverse
Scarica l'originale da usare negli esempi.
Nota: questi esempi non funzionano a dovere in Firefox 5.0, probabilmente a causa di un bug nell'implementazione del canvas. Sono invece stati testati con successo in IE9, IE10, Chrome 12, Opera 11 e Firefox Aurora 7.0.
Passo 1: gestire gli oggetti SpriteSheet e BitmapSequence
Iniziamo con il far correre il mostro da un lato del canvas all'altro. Il primo passo è caricare la sequenza completa contenuta nel file PNG attraverso questo codice:
var imgMonsterARun = new Image();
function init() {
//recupera il canvas e carica le immagini
//attendi fino a quando l'ultima immagine è caricata
canvas = document.getElementById("testCanvas");
imgMonsterARun.onload = handleImageLoad;
imgMonsterARun.onerror = handleImageError;
imgMonsterARun.src = "img/MonsterARun.png";
}
Il codice verrà richiamato all'inizio dell'applicazione per inizializzare il contenuto del nostro gioco. Una volta caricato, il gioco può essere avviato. EaselJS espone un oggetto SpriteSheet per gestire lo sprite. In questo modo usando il codice:
var spriteSheet = new SpriteSheet(
imgMonsterARun, //immagine da usare
64, //larghezza di ogni sprite
64, //altezza di ogni sprite
{
walk_left: [0, 9]
});
... posso indicare di voler creare una nuova sequenza chiamata walk_left
che verrà composta dall'immagine imgMonsterARun
. L'immagine sarà suddivisa in 10 frame di dimensioni 64x64 pixel. Questo è l'oggetto principale che ci permetterà di caricare il nostro sprite e di creare la nostra sequenza. Se si desidera si possono avere diverse sequenze create dallo stesso file PNG, come accade nell'esempio dei topolini sul sito di EaselJS.
Dopo aver fatto ciò, si dovrà usare l'oggetto BitmapSequence che aiuta ad animare la sequenza e a posizionare gli sprite sullo schermo.
Vediamo insieme il codice che inizializza l'oggetto.
// crea un'istanza BitmapSequence per visualizzare e riprodurre la striscia di sprite
bmpSeq = new BitmapSequence(spriteSheet);
// imposta il punto di registrazione al centro delle dimensioni del frame
// (il punto in cui si trova l'oggetto e intorno al quale ruota)
bmpSeq.regX = bmpSeq.spriteSheet.frameWidth/2|0;
bmpSeq.regY = bmpSeq.spriteSheet.frameHeight / 2 | 0;
// avvia la riproduzione della prima sequenza
bmpSeq.gotoAndPlay("walk_left"); // animazione
// imposta un'ombreggiatura. Va detto che le ombre sono avide di risorse. Evitando le ombre è possibile mostrare centinaia di topolini animati.
bmpSeq.shadow = new Shadow("#454", 0, 5, 4);
bmpSeq.name = "monster1";
bmpSeq.speed = 1;
bmpSeq.direction = 90;
bmpSeq.vX = 3|0.5;
bmpSeq.vY = 0;
bmpSeq.x = 16;
bmpSeq.y = 32;
// ogni mostro viene avviato in uno specifico momento
bmpSeq.currentFrame = 0;
stage.addChild(bmpSeq);
Il costruttore dell'oggetto BitmapSequence ha bisogno solo dell'elemento SpriteSheet come parametro. Dovremo poi dare un nome alla sequenza e impostare alcuni parametri come la velocità e la posizione iniziale del nostro primo frame. Alla fine aggiungeremo la sequenza per la visualizzazione usando l'oggetto Stage e il suo metodo addChild()
.
Successivamente è necessario decidere cosa si intende fare all'interno del loop di animazione. Il loop è richiamato ogni xxx millisecondi e permette di aggiornare la posizione degli sprite. A questo scopo, EaselJS espone l'oggetto Ticker che invia un "tic" (un segnale) ad un intervallo prestabilito a tutti gli elementi in ascolto.
Tutto ciò che c'è da fare è "sottoscrivere" l'evento tick e implementare un metodo .tick()
di callback.
Con questo codice registriamo l'evento:
Ticker.addListener(window);
// Impostiamo il miglior tasso di immagini al secondo (60 FPS)
Ticker.setInterval(17);
E questo è il codice che verrà richiamato ogni 17 ms (se possibile) per aggiornare la posizione del nostro mostro.
function tick() {
// Verifichiamo l'ampiezza dello schermo altrimenti rischiamo che lo sprite scompaia
if (bmpSeq.x >= screen_width - 16) {
// Abbiamo raggiunto il lato destro dello schermo
// Dobbiamo tornare indietro verso sinistra per raggiungere la posizione iniziale
bmpSeq.direction = -90;
}
if (bmpSeq.x < 16) {
// Abbiamo raggiunto il lato sinistro dello schermo
// Adesso dobbiamo camminare verso destra
bmpSeq.direction = 90;
}
// Muovere lo sprite in base alla direzione e alla velocità
if (bmpSeq.direction == 90) {
bmpSeq.x += bmpSeq.vX;
bmpSeq.y += bmpSeq.vY;
}
else {
bmpSeq.x -= bmpSeq.vX;
bmpSeq.y -= bmpSeq.vY;
}
// aggiorniamo lo stage
stage.update();
}
Potete verificare il risultato finale qui:
Il codice completo si trova su easelJSSpritesTutorial01.
Ma attenzione! Ci sono due problemi con questa animazione!
- I passi dell'animazione sono incoerenti poiché l'omino esegue i cicli di movimento troppo velocemente.
- L'omino cammina normalmente solo da destra verso sinistra; nell'altro senso sembra invece che stia facendo il moonwalk!
Correggiamo i problemi.
Passo 2: controllare la velocità dell'animazione e invertire dello sprite
Il metodo più semplice che ho trovato per correggere la velocità dell'animazione è quello di usare un "operatore modulo" (modulus operator) per evitare di disegnare/aggiornare la sequenza dopo ogni tic. C'è inoltre un problema aperto su questo punto per EaselJS 0.3.2: https://github.com/gskinner/EaselJS/issues/60
Invece, per fare camminare regolarmente l'omino da sinistra verso destra doppiamo semplicemente invertire specularmente ogni frame. EaselJS espone a questo riguardo un oggetto SpriteSheetUtils e un metodo flip()
. Ecco il codice che permette di utilizzarlo
// {nameOfFlippedSequence:["derivativeSequence", flipHorizontally, flipVertically, optionNameOfNextSequence]}
spriteSheet = SpriteSheetUtils.flip(
spriteSheet,
{
walk_right: ["walk_left", true, false, null]
});
Ciò che stiamo facendo è essenzialmente creare una sequenza derivata chiamata walk_right
basata sulla sequenza walk_left
che abbiamo invertito lungo l'asse orizzontale. Alla fine, ecco il codice che ci permette di rallentare l'animazione e gestire quale sequenza riprodurre in base alla posizione dell'omino.
function tick() {
// Per rallentare l'animazione non dobbiamo ridisegnarla ad ogni tic
// Con un modulo 4, dividiamo la velocità per 4
var speedControl = Ticker.getTicks() % 4;
if (speedControl == 0) {
// Verifichiamo l'ampiezza dello schermo altrimenti rischiamo che lo sprite scompaia
if (bmpSeq.x >= screen_width - 16) {
// Abbiamo raggiunto il lato destro dello schermo
// Dobbiamo tornare indietro verso sinistra per raggiungere la posizione iniziale
bmpSeq.direction = -90;
bmpSeq.gotoAndPlay("walk_left")
}
if (bmpSeq.x < 16) {
// Abbiamo raggiunto il lato sinistro dello schermo
// Adesso dobbiamo camminare verso destra
bmpSeq.direction = 90;
bmpSeq.gotoAndPlay("walk_right");
}
// Muovere lo sprite in base alla direzione e alla velocità
if (bmpSeq.direction == 90) {
bmpSeq.x += bmpSeq.vX;
bmpSeq.y += bmpSeq.vY;
}
else {
bmpSeq.x -= bmpSeq.vX;
bmpSeq.y -= bmpSeq.vY;
}
// aggiornare lo stage:
stage.update();
}
}
Ecco il risultato finale:
Codice completo su easelJSSpritesTutorial02
Passo 3: caricare più sprite e utilizzare più animazioni
È tempo di caricare il mostro nella sua posizione di riposo. L'idea è quella di far fare al mostro un singolo viaggio (da destra a sinistra e ritorno) e poi fermarlo. Dobbiamo, in questo caso, caricare più di un file PNG dal server. È molto importante attendere che tutte le risorse siano caricate, altrimenti si potrebbero disegnare elementi incompleti.
Ecco un modo semplice per farlo:
var numberOfImagesLoaded = 0;
var imgMonsterARun = new Image();
var imgMonsterAIdle = new Image();
function init() {
// recupera il canvas e carica le immagini
// attende fino a quando l'ultima immagine è caricata
canvas = document.getElementById("testCanvas");
imgMonsterARun.onload = handleImageLoad;
imgMonsterARun.onerror = handleImageError;
imgMonsterARun.src = "img/MonsterARun.png";
imgMonsterAIdle.onload = handleImageLoad;
imgMonsterAIdle.onerror = handleImageError;
imgMonsterAIdle.src = "img/MonsterAIdle.png";
}
function handleImageLoad(e) {
numberOfImagesLoaded++;
// Non facciamo partire il gioco fino a quando l'ultima immagine è caricata
// Altrimenti si potrebbe cominciare a disegnare senza la risorse e incorrere in questa eccezione del DOM: INVALID_STATE_ERR (11) per il metodo drawImage
if (numberOfImagesLoaded == 2) {
numberOfImagesLoaded = 0;
startGame();
}
}
Il codice è molto semplificato. Per esempio, non gestisce gli errori in modo accurato cercando di ricaricare la risorsa una seconda volta dopo un tentativo fallito. Naturalmente non stiamo creando un gioco vero e proprio, dovrete dunque progettare un download manager specifico se la libreria JavaScript che state usando non lo ha già. Per aggiungere la sequenza con l'omino a riposo e impostare i parametri di posizionamento, dovrete semplicemente usare il codice visto in precedenza:
var spriteSheetIdle = new SpriteSheet(
imgMonsterAIdle, // immagine da usare
64, //larghezza dello sprite
64, //altezza dello sprite
{
idle: [0, 10]
});
bmpSeqIdle = new BitmapSequence(spriteSheetIdle);
bmpSeqIdle.name = "monsteridle1";
bmpSeqIdle.speed = 1;
bmpSeqIdle.direction = 0;
bmpSeqIdle.x = 16;
bmpSeqIdle.y = 32;
Ora, nel metodo tick()
, dobbiamo fermare l'omino che cammina quando ha raggiunto il lato sinistro dello schermo e riprodurre l'animazione di riposo. Ecco il codice per fermare il mostro:
if (bmpSeq.x < 16) {
// Abbiamo raggiunto il lato sinistro dello schermo
// Adesso dobbiamo camminare verso destra
bmpSeq.direction = 90;
bmpSeq.gotoAndStop("walk_left");
stage.removeChild(bmpSeq);
bmpSeqIdle.gotoAndPlay("idle");
stage.addChild(bmpSeqIdle);
}
Ecco il risultato finale:
Qui il codice completo: easelJSSpritesTutorial03.
L'autore
David Rousset è developer evangelist di Microsoft, specializzato in HTML5 e sviluppo per il Web. Leggete il suo blog su MSDN o seguitelo su Twitter con l'account @Silversurfeur.