In questo nuovo tutorial di programmazione iOS parleremo di un argomento interessante e utile ad ogni sviluppatore iPhone o iPad: le animazioni. Se per un genere di applicazioni queste sono infatti addirittura fondamentali (vedi i videogame) per molte altre possono tornare utili ed essere sfruttate per rendere più gradevoli le applicazioni mobili che si stanno realizzando. Lungo il tutorial prenderò ad esempio la mia app “ABC x Alieni” (è gratuita per pochi giorni, scaricatela! 😉 ), un gioco per bambini di età prescolare, di cui illustrerò e renderò open source gran parte del codice. Questo gioco mi è stato “commissionato” dai miei figli e in un primo tempo avevo pensato di utilizzare cocos2D, poi visto che le dinamiche mi sembravano relativamente semplici ho pensato che potesse essere una buona palestra per studiare Core Animation e le animazioni in generale. Questo per un motivo molto semplice: i framework “game” mal si adattano alla realizzazione di app “utility” o business e anche solo la loro integrazione con l’UIKit è talora farraginosa e poco pratica. Le tecniche illustrate in questo articolo potranno invece facilmente essere utilizzate in qualunque app vogliate sviluppare al fine di renderla più accattivante e piacevole, requisito ormai fondamentale anche in una semplice to-do list (vedi il successo mondiale dell’app Clear)!
Nota: Una piccola precisazione prima di iniziare: siete liberi di utilizzare tutto o in parte il codice riportato (possibilmente citando la fonte…) ma non le immagini (opera di Irene Dapo), che sono fornite al solo titolo di rendere completo il tutorial.
Il Progetto
Cominciamo con una breve panoramica. La schermata di gioco dell’app, presenta due bottoni direzionali su cui viene gestita la pressione continua (quindi non solo il tap singolo), un oggetto in movimento (il carrellino), due alieni che si animano (e alle volte saltano) quando il carrello si muove nella loro direzione, un oggetto che cade dall’alto. Inoltre muovendo il dispositivo l’ombra degli oggetti cambia di inclinazione.
La disposizione di partenza degli oggetti nella view è fatta usando un normalissimo xib di una UIView di un UIViewController. Rimando alle guide di base la creazione del progetto, del view controller e dello xib. Vi faccio notare però i primi due “trucchi” usati: primo i vari oggetti che verranno animati non sono delle UIImageView, ma delle semplici UIView, perché ci interessa definire qui solo l’ingombro e la posizione, e secondo le “sprite” sono messe in modo che gli oggetti siano sempre contro il bordo destro o sinistro, a prescindere dalle dimensioni della view, in modo da ottenere senza sforzo un’app che si adatti per l’iPhone 4 o 5. Non è sempre possibile, ma spesso con piccoli accorgimenti come questo si può evitare di dover avere 2 differenti xib per gestire le differenti versioni dello schermo degli iPhone.
La gestione degli eventi avviene nel “game loop”, ovvero un ciclo “infinito”, scandito da un timer, che ogni decimo di secondo controlla e aggiorna lo stato del gioco. Così facendo ho già deciso la cadenza degli eventi: aggiornamenti, movimenti e interazioni sono valutate ogni decimo di secondo, un tempo che mi sembrava adatto al tipo di gioco che volevo realizzare: simpatico, non frenetico e “rilassante”.
Completano il tutto alcune funzioni per la generazione di numeri casuali (usate per scegliere da quale punto e quale numero far cadere e per decidere quanto in alto far saltare gli alieni) e l’attivazione della rilevazione dell’accellerometro: quest’ultimo non è usato per gestire il movimento, ma per dare un tocco pseudo-3D al gioco, aggiungendo delle ombre che si muovano in base all’inclinazione, come se gli oggetti avessero una profondità e cambiasse l’inclinazione della luce.
Per comprendere meglio quanto appena descritto e quanto seguirà vi consiglio di scaricarvi il progetto allegato (vedi a fondo articolo) e provarlo.
Tecniche usate: Sprite Sheet
Una tecnica molto usata per le animazioni nei videogiochi è quella dello Sprite Sheet, chiamati anche “sprite atlas”, atlanti), ovvero la creazione di una (grossa) immagine contenente tutti i vari “frame” di un’animazione per poi visualizzarne un pezzo per volta. Perché complicarsi la vita vi chiederete? Non è più semplice avere 10 immagini separate e visualizzare di volta in volta quella necessaria? Beh forse è più semplice, ma le prestazioni cambiano drasticamente: ogni volta che si visualizza un’immagine questa viene caricata nella memoria della GPU (il processore grafico) e questa operazione è abbastanza lenta. Mentre mostrare una parte dell’immagine che già è in memoria è di gran lunga più veloce. Quindi la tecnica migliore consiste nel creare i vari “fotogrammi” singolarmente e poi accorparli usando metodi più o meno ottimizzati. Ovviamente consuma più memoria, quindi ci vuole un po’ di attenzione.
Esistono Tool professionali (pensati per cocos2D, Corona e altri framework simili) che consentono di “impacchettare” all’estremo le immagini, eliminando le parti trasparenti e memorizzando poi delle mappe di coordinate grazie alle quali si potrà ricostruire l’oggetto iniziale, ma qui vi illustro il metodo più semplice e “fai da te”, ovvero quello in cui tutti i frame hanno la stessa dimensione e orientamento. Così facendo basta sapere quanto è grande il frame di riferimento e individuare la porzione giusta dell’atlante da visualizzare. Non solo, creare uno sprite sheet diviene una semplice operazione di copia-incolla gestibile con Anteprima, senza ricorrere a Photoshop, Gimp o altri programmi specializzati.
Un ultimo dettaglio riguarda le dimensioni. Nella GPU gli sprite sheet posso avere come dimensione solo quadrati con potenze di due di lato: perciò avere un atlas rettangolare di 1025 per 500 pixel vuol dire occupare un quadrato di 2048×2048 pixel in memoria! Ma questo è solo un limite della GPU, non di Core Animation, quindi in questo tutorial ho volutamente usato spritesheet rettangolari, perché lo scopo era illustrare le potenzialità del framework, anche a livello di semplicità: credetemi se non dovete animare troppi oggetti è inutile stare ad impazzire per convincere il vostro grafico della necessità di “schiacciarli” in pochi pixel 😉
Per gestirli ho rielaborato una classe open source di Miguel Angel Friginal (trovata mentre cercavo altro, come spesso capita), di cui per correttezza ho mantenuto il nome originale. Vediamo come funziona.
Per prima cosa dobbiamo creare uno sprite sheet, o meglio una subclass di CALayer che lo gestisca. La init di questo layer vuole due parametri: l’immagine con l’atlas degli sprite e la dimensione del singolo sprite. L’immagine diventerà il contents del layer (che contenendo tutti gli sprite li caricherà una volta sola nella memoria della GPU) mentre il CGSize sarà usato per definire il bounds (la dimensione) del layer e il suo contentsRect. Quest’ultimo è un parametro un po’ “strano” a prima vista, perché è un CGFrame che può contenere solo valori (float) nel range 0.0-1.0. Serve ad indicare quale porzione del contents deve essere realmente visualizzata, ma è una misure relativa, dove 0 è niente e 1 è tutto.
Quindi se gli dico (0.5,0.5),(1,1) gli dico che il quarto in basso a destra dell’immagine complessiva è quello che va visualizzato nel layer:
- (id)initWithImage:(CGImageRef)img sampleSize:(CGSize)size;{
self = [super init];
if (self != nil){
self.contents = (__bridge id)img;
self.spriteIndex = 1;
CGSize sampleSizeNormalized = CGSizeMake(size.width/CGImageGetWidth(img), size.height/CGImageGetHeight(img));
self.bounds = CGRectMake( 0, 0, size.width, size.height);
self.contentsRect = CGRectMake( 0, 0, sampleSizeNormalized.width, sampleSizeNormalized.height );
}
return self;
}
La property “spriteIndex” indica quale sprite vogliamo visualizzare (per semplicità di calcolo deve partire da 1. Pertanto quando dobbiamo visualizzare il layer basterà calcolare il “contentsRec”t in base a quella property e automaticamente verrà visualizzato lo sprite desiderato.
/* calcola le coordinate esatte della porzione di sheet da visualizzare */
- (void)display;
{
if ([self.delegate respondsToSelector:@selector(displayLayer:)])
{
[self.delegate displayLayer:self];
return;
}
unsigned int currentSampleIndex = [self currentSampleIndex];
if (!currentSampleIndex)
return;
CGSize sampleSize = self.contentsRect.size;
self.contentsRect = CGRectMake(
((currentSampleIndex - 1) % (int)(1/sampleSize.width)) * sampleSize.width,
((currentSampleIndex - 1) / (int)(1/sampleSize.width)) * sampleSize.height,
sampleSize.width, sampleSize.height
);
}
A questo punto non ci resta che usarlo: prima lo creo e posiziono:
self.zipSprite = [MCSpriteLayer layerWithImage:[UIImage imageNamed:@"ZIP2.png"].CGImage sampleSize:CGSizeMake(100, 132)];
self.zipSprite.position = CGPointMake(50, 66);
[self.zipView.layer addSublayer: self.zipSprite];
Quindi, nei punti dove gestisco la logica che sta dietro all’animazione degli sprite mi basterà cambiare l’indice, ovvero:
[self.zipSprite showFrame:zipIndex];
Ombre e accellerometro
Spesso l’accellerometro è usato nei giochi per gestire il movimento del personaggio. Io invece l’ho usato per dare un po’ di senso prospettico al gioco. Gli oggetti in movimento hanno infatti un’ombra che si muove in base all’inclinazione del dispositivo, dando un piacevole effetto di profondità alla scena. Le ombre sono una caratteristica fornita dai CALayer e gestibile con semplici property, quindi è facile ottenere buoni effetti con poche istruzioni. Vediamole:
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
// mi registro agli eventi dell'accellerometro
UIAccelerometer *accelerometer = [UIAccelerometer sharedAccelerometer];
accelerometer.delegate = self;
accelerometer.updateInterval = 0.25;
...
}
qui informo iOS che sono interessato agli eventi dell’accellerometro, che mi saranno comunicati in:
- (void) accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration{
CGSize shadow = CGSizeMake(acceleration.y*GAMELOOP_ShadowsRadius,(acceleration.x+acceleration.z)*GAMELOOP_ShadowsRadius);
self.zipView.layer.shadowOffset = shadow;
...
}
dove semplicemente calcolo il raggio e l’inclinazione dell’ombra e lo imposto a tutti gli oggetti che mi interessano.
Pressione continua
iOS non fornisce automaticamente un UIGestureRecognizer per gestire la pressione continua di un bottone: ci sarebbe il “long press”, ma in realtà non è la stessa cosa e non va bene per simulare un joypad. In questo gioco l’interazione è molto semplice, basta premere (e tener premuto…) uno dei bottoni e il carrellino si muoverà in quella direzione.
Per ottenere il risultato creo due IBAction e in Interface Builder le assegno ad alcuni eventi significativi dei due bottoni di direzione: theTouchDown all’event Touch Down e theTouchUp a Touch Up Inside e a Touch Up Outside. In entrambi i metodi setto la variabile globale moveButtonPressed che verrà poi utilizzata durante il game loop per sapere se e in che direzione muovere il carrellino.
/* il bottone risulta premuto fino a che non ricevo un touch up */
- (IBAction)theTouchDown:(id)sender{
moveButtonPressed = ((UIButton*)sender).tag;
}
/* fine della "pressione continua" */
- (IBAction)theTouchUp:(id)sender{
if (((UIButton*)sender).tag == moveButtonPressed) // solo se il sollevato è quello che aveva originato la pressione
moveButtonPressed = 0;
}
In pratica mi “segno” solo qual è il bottone che il giocatore sta premendo, poi gestirò le conseguenza di questa azione nel game loop.
Movimento
Qui ho usato un mix di tecniche, cercando sempre la strada più semplice nel rispetto della fluidità del gioco. Essendo gli oggetti dei CoreAnimationLayer avrei potuto usare le animazioni di quel framework (potenti ma non sempre banali da gestire), ma dato che ogni CALayer è incapsulato dentro una UIView e che queste ultime hanno delle animazioni di alto livello facilissime da usare la scelta è caduta su queste ultime.
Ad esempio il carrello lo muovo così:
// cambio lo sprite da visualizzare
[self.carrelloSprite showFrame:carrelloIndex];
// e lo muovo: l'animazione viene fluida ma nello stesso tempo a "scatti" che sa molto di giochino per bimbi
[UIView animateWithDuration:GAMELOOP_AnimationInterval delay:0 options:UIViewAnimationCurveLinear animations:
^{
self.carrelloView.center = CGPointMake(xpos,self.carrelloView.center.y);
} completion:nil];
Mentre per gli alieni che saltano uso semplicemente due animazioni “accodate”:
/* l'oggetto è stato preso, gli alieni saltano felici :) */
- (void) zipZapHappy{
// faccio fare ai due alieni un salto di altezza casuale
float zipH = (arc4random()%50)+10;
float zapH = (arc4random()%50)+10;
// l'uso di UIViewAnimationCurveEaseOut per la parte di salita e di easeIn per la discesa rende il salto più "naturale"
[UIView animateWithDuration:GAMELOOP_AnimationInterval*8 delay:0 options:UIViewAnimationCurveEaseOut animations:
^{
self.zipView.center = CGPointMake(self.zipView.center.x,self.zipView.center.y-zipH);
self.zapView.center = CGPointMake(self.zapView.center.x,self.zapView.center.y-zapH);
} completion:^(BOOL finished) {
[UIView animateWithDuration:GAMELOOP_AnimationInterval*7 delay:0 options:UIViewAnimationCurveEaseIn animations:
^{
self.zipView.center = CGPointMake(self.zipView.center.x,self.zipView.center.y+zipH);
self.zapView.center = CGPointMake(self.zapView.center.x,self.zapView.center.y+zapH);
} completion:nil];
}];
...
}
Prima faccio una animazione “in frenata” (UIViewAnimationCurveEaseOut) verso l’alto, poi una “in accelerazione” (UIViewAnimationCurveEaseIn) verso il basso: questo unito all’altezza casuale del salto rende il movimento degli alieni più naturale, pur senza usare alcun motore fisico come Box2d, Chipmunk e simili.
Da notare che essendo la scelta degli sprite e lo loro visualizzazione indipendente da queste animazioni, se il giocatore preme i bottoni gli alieni si animeranno anche mentre saltano: un effetto speciale completamente gratuito! 😉
Conclusione
Spero che questo tutorial vi abbia dato le basi per poter esplorare Core Animation e fornito spunti per impreziosire le UI delle vostre app con effetti e animazioni gradevoli e di effetto. Nel codice ci sono molti commenti che dovrebbero aiutare a comprendere meglio il tutto, ma se avete qualche dubbio o domanda potete aprite un post qui sul nostro forum gratuito.
Alla prossima
Il Malvagio Dott. Prosciutto
(@theEvilDocHam)
Se avete problemi con il progetto presentato, questo è il link per scaricare il nostro esempio (in arrivo).
2 Responses to “T#112 – Animazioni ed effetti per le nostre applicazioni iPhone e iPad”
4 Febbraio 2013
SimoneBel tutorial aspetto l’esempio!
17 Agosto 2013
CronoluisCiao, potreste caricare l’esempio? Qualcosa non mi quadra…
Grazie mille, fantastici!