20: SCROLLING - ANIMAZIONI

SCROLLING!

Una caratteristica di molti giochi è lo "scrolling" dello sfondo, cioè il lento movimento per simulare l'avanzare del protagonista; questo effetto si può ottenere in vari modi diversi.

Una prima tecnica per lo scrolling è quella di usare una superficie di sfondo (chiamiamola surf_back come nel nostro gioco) molto più grande dell'area di gioco (surf_play), come mostrato nella figura:

Immagine scrolling

Lo scorrimento si ottiene facendo la blit() della surf_back sulla surf_play in una posizione opportuna, e tenendo conto che pygame non disegna tutto quello che è fuori dalla superficie di destinazione. Nel nostro esempio, se la distanza dx fosse uguale a 200 pixel, basterebbe scrivere:

surf_play.blit(surf_back, (-dx, 0))

(perchè la surf_back va disegnata 200 pixel più indietro rispetto all'inizio (0, 0) della surf_play). Facendo variare dx possiamo muovere il nostro sfondo a sinistra e destra: questa soluzione è conveniente quando il nostro personaggio si muove in un ambiente limitato ma più grande dello schermo, nel quale può avanzare e retrocedere.

Un'altra tecnica, usata nei casi di un avanzamento continuo e regolare, è quella di spostare fisicamente i pixel dello sfondo in modo "circolare" (facendo cioè rientrare da un lato i pixel che escono dall'altro). In pygame esiste un metodo scroll() dell'oggetto Surface che compie questa operazione, ma stranamente i programmatori di pygame hanno dimenticato di far rientrare dal lato opposto i pixel che escono, e quindi questo metodo non è molto utilizzabile. Così ho deciso di scrivere una breve funzione per lo scrolling di una Surface (eccola qui sotto):


def vertical_scroll(surf, dy):
    rect1 = pygame.Rect(0, 0, surf.get_width(), surf.get_height() - dy)
    rect2 = pygame.Rect(0, surf.get_height() - dy, surf.get_width(), dy)
    surf_temp = surf.copy()
    surf.blit(surf_temp, (0, 0), rect2)
    surf.blit(surf_temp, (0, dy), rect1)

La funzione prende come parametri surf (la Surface da scrollare) e dy (il numero di righe da muovere). Il funzionamento è molto semplice: divide la Surface in due Rect rect1 e rect2 di dimensioni opportune, poi sposta rect1 in basso e rect2 in alto, secondo questo schema:

Immagine scrolling

Modifichiamo ora il nostro programma aliens2.py (qui) aggiungendo lo scrolling:

ESERCIZIO 20.1: Scrolling dello sfondo

Ecco fatto! Con pochissimo sforzo abbiamo ottenuto un effetto molto gradevole!

La funzione vertical_scroll() è molto semplice e può effettuare solo lo scrolling dall'alto verso il basso. Se volete provare le vostre capacità potete, per esercizio, modificarla per renderla più versatile.

ESERCIZIO 20.2: Partendo dalla vertical_scroll() create un'altra funzione horizontal_scroll(surf, dx) che scrolli la Surface da destra verso sinistra.
ESERCIZIO 20.3 (più difficile): modificate le due funzioni in modo che accettino anche valori dy e dx negativi (facendo muovere lo schermo rispettivamente dal basso verso l'alto e da sinistra a destra).
SOLUZIONI

IMMAGINI ANIMATE

Un effetto che non manca mai nei videogiochi sono le esplosioni! Anche nel nostro gioco sarebbe carino se i nemici colpiti scomparissero con qualche esplosione fiammeggiante. Abbiamo però un problema: un'esplosione non è un'immagine fissa, ma una piccola animazione, cioè un rapido susseguirsi di immagini diverse, dette in Inglese frames. I file in formato .gif, molto usati nei siti web, possono contenere piccole animazioni (il file è composto in realtà da più immagini da visualizzare in sequenza). Sfortunatamente pygame non supporta nativamente nessun tipo di immagine animata (e questo è decisamente un problema rispetto ad altre piattaforme): se caricate un file .gif animato la Surface rappresenterà solo il primo frame.

Molti programmatori hanno aggirato questo problema costruendo autonomamente degli oggetti di pygame che supportano le animazioni. Anch'io ho seguito questa strada, creando un oggetto che estende le funzionalità dello Sprite, e che ho chiamato AnimSprite. Vediamo le sue caratteristiche ed il suo uso:

Vediamo in dettaglio i metodi e gli attributi dell'oggetto:

Funzione Significato
AnimSprite(*args) -> AnimSprite Costruttore: il suo uso è analogo a quello del costruttore Sprite(). Si possono indicare come parametri opzionali uno o più Group ai quali l'AnimSprite verrà aggiunto. E' opportuno creare un Group che contenga tutti e soli gli AnimSprite (ad es. animations)
set_images(img_list, loop=False) -> None Definisce la sequenza di frames che compongono l'animazione. Il parametro img_list è una lista di Surface (i vari frames dell'animazione) oppure una lista di string (i nomi dei file da caricare). La funzione inizializza l'attributo rect usando le dimensioni del primo frame (tutti i frames devono avere le stesse dimensioni, altrimenti l'animazione non funzionerà). Per il parametro loop vedi sotto.
anim_stop(frame=None) -> None Ferma l'animazione mostrando un frame fisso. Se il parametro frame (opzionale) viene omesso l'animazione si ferma sul frame attuale (visualizzato in quel momento), altrimenti l'utente può indicare quale frame deve essere visualizzato (0 = inizio dell'animazione, ecc.)
anim_start(frame=None) -> None Riprende l'animazione dopo una anim_stop(). Il parametro opzionale frame si usa come nella funzione precedente.
set_rate(rate) -> None Determina la velocità dell'animazione. Di default essa cambia frame ogni volta che viene chiamata la funzione update() di un suo Group. Possiamo rallentare l'animazione indicando come parametro in questa funzione un numero maggiore di uno (ponendo rate = 2 il frame cambierà ogni due chiamate della funzione ed essa sarà il doppio più lenta). Il parametro rate può essere anche un numero float, purchè positivo.
update() -> None Serve a far progredire l'animazione. Deve essere chiamata prima che l'AnimSprite venga disegnato con la draw() del suo Group.
set_loop(loop) -> None Determina se l'animazione, una volta terminata la sequenza dei frames, debba ricominciare da capo o no. Se loop è False (default) l'AnimSprite esegue automaticamente la kill() dopo l'ultimo frame (ad es. in un'esplosione); se invece lo poniamo a True l'animazione ricomincerà proseguendo all'infinito (o finchè non cancelliamo l'oggetto manualmente con la kill()).
get_loop() -> bool Restituisce il tipo di animazione (True se il loop è impostato, False altrimenti).

Nella cartella Sprite/Animations trovate un lungo elenco di file per le animazioni. Quasi tutte le animazioni sono composte da 24 o 36 frames, ed hanno nomi del tipo "expl_02_0000.png", "expl_02_0001.png" ecc. : le ultime quattro cifre sono il numero del frame (che va da 0 ad n - 1), mentre i primi caratteri sono il nome dell'animazione: quindi, per caricare l'animazione "expl_02" (esplosione) dovremo caricare tutti i file da "expl_02_0000.png" a "expl_02_0023.png". Scrivere le istruzioni per caricare tutti questi file è chiaramente molto noioso, e possiamo rimediare usando un ciclo for.

ESERCIZIO 20.4: Importate il modulo animimage ed aggiungete, nella sezione Risorse grafiche, le linee di codice seguenti:

.   .   .
anim_expl = []
for i in range(32):
    filename = join("Sprites", "Animations", "expl_06_{:0>4}.png".format(i))
	img = pygame.image.load(filename).convert_alpha()
    anim_expl.append(img)
anim_big_expl = []
for i in range(32):
    filename = join("Sprites", "Animations", "expl_10_{:0>4}.png".format(i))
	img = pygame.image.load(filename).convert_alpha()
    anim_big_expl.append(img)
.   .   .
  

Queste istruzioni creano le due liste anim_expl e anim_big_expl che contengono i frames rispettivamente dell'esplosione di un nemico e dell'esplosione della nostra astronave. Nelle righe #4 e #9, per ottenere i nomi dei file, ho usato il metodo format() della string, argomento che non tutti potreste conoscere: in pratica format() prende uno o più argomenti e li sostituisce al posto delle parentesi graffe { } nella string, formattandoli in base a delle regole piuttosto complicate. Nel nostro esempio i caratteri :0>4 all'interno delle graffe dicono a Python di sostituire alle graffe i anteponendo degli zeri fino a raggiungere 4 caratteri (quindi avremo expl_06_0000.png, expl_06_0001.png, ... expl_06_0010.png ecc.): questo risultato non si poteva raggiungere con la semplice concatenazione, che avrebbe dato expl_06_0000.png, expl_06_0001.png, ... expl_06_00010.png ecc. Se volete approfondire l'uso del metodo format() potete consultare la Lezione 5 del mio tutorial intermedio su Python.

ESERCIZIO 20.5: Per prima cosa abbiamo bisogno di un nuovo Group che contenga tutti i nostri AnimSprite: basterà creare, nella sezione Altre variabili, il Group animations. Nella sezione Aggiornamento dello schermo, prima delle blit(), aggiungete poi la riga

    animations.update()
  
che serve ad aggiornare, per ogni ciclo, le animazioni di tutti gli AnimSprite contenuti nel Group.

ESERCZIO 20.6: Ora possiamo aggiungere al gioco le nostre esplosioni. Cominciamo dalle collisioni tra alieni e proiettili: nel nostro programma erano gestite dalle righe (nella sezione Logica del gioco):

.   .   .
    if pygame.sprite.groupcollide(bullets, aliens, True, True):
        score += 10
        sound_expl.play()
.   .   .
  
Ho già detto qui che la groupcollide() controlla le collisioni tra gli Sprite di due Group, li rimuove dai Group (perchè abbiamo impostato il terzo e quarto parametro a True) e restituisce un dizionario di Python con tutte le collisioni. La vecchia versione controllava solamente (con l'if) che il dizionario non fosse vuoto, ma se vogliamo posizionare le nostre esplosioni ci serve ora l'elenco esatto di tutti i nemici colpiti. Dobbiamo quindi conoscere meglio la struttura del dizionario: le chiavi (keys) sono tutti gli Sprite del primo Group (nel nostro caso bullets) che presentano qualche collisione, e ad ognuno di essi corrisponde una list con tutti gli Sprite del secondo Group (aliens) che collidono con esso. Sostituite le tre righe con queste:

.   .   .
    collisions = pygame.sprite.groupcollide(bullets, aliens, True, True)
    for i in collisions.keys():
        for alien in collisions[i]:
            score += 10
            sound_expl.play()
            expl = animimage.AnimSprite(animations, all_sprites)
            expl.set_images(anim_expl)
            expl.rect.center = alien.rect.midbottom

.   .   .

Commentiamo il nostro codice: nella #2 memorizziamo le collisioni nel dizionario collisions, nella #3 iniziamo un ciclo for su tutte le chiavi del dizionario (i proiettili in collisione). Se il dizionario è vuoto Python salterà tutto il ciclo, altrimenti, per ogni proiettile, collisions[i] (nella #4) ci darà la list di tutte le navicelle aliene colpite dal proiettile, e per ognuna di esse aggiungiamo l'esplosione: nella #7 creiamo l'AnimSprite expl aggiungendolo ai Group animations (per l'animazione) e all_sprites (per il disegno); nella #8 gli assegniamo la lista di frames anim_expl ed infine nella #9 posizioniamo il suo rect: il centro dell'esplosione coincide con il centro del lato inferiore della navicella aliena (dato che l'astronave è stata colpita in basso da un proiettile).

Come vedete la creazione di un'AnimSprite è abbastanza simile a quella di un normale Sprite; basta usare il metodo set_images() per impostare i frames dell'animazione e ricordarsi, prima di disegnare l'AnimSprite, di chiamare la animations.update() che selezionerà il frame giusto per l'attributo image. Lanciate il programma e, se non avete commesso errori, godetevi le esplosioni dei nemici (se le trovate un po' troppo piccole non preoccupatevi, è facile rimediare).

ESERCZIO 20.7: Ora occupiamoci dell'esplosione della nostra astronave quando viene colpita da una navicella nemica: cercate nella sezione Logica del gioco le righe che controllano se l'astronave collide con un nemico:

.   .   .
    if pygame.sprite.spritecollide(spr_ship, aliens, True):
        sound_end.play()
        done = True
.   .   .
  
ed aggiungete queste:

.   .   .
    if pygame.sprite.spritecollide(spr_ship, aliens, True):
        sound_end.play()
        done = True
		spr_ship.kill()
        expl = animimage.AnimSprite(animations, all_sprites)
        expl.set_images(surf_big_expl)
        expl.rect.center = spr_ship.rect.midtop
.   .   .
  
Qui non abbiamo bisogno di molti cambiamenti perchè possiamo posizionare l'esplosione in base alla nostra astronave spr_ship: notate che questa volta dobbiamo cancellare noi la nostra nave con la riga #5 (perchè la spritecollide() non può farlo), mentre le tre righe #6 #7 #8 sono simili a quelle che abbiamo scritto nell'esercizio precedente.

Sembra tutto molto semplice e simile alla situazione precedente: lanciate ilprogramma e, ... avrete un'amara sorpresa! L'animazione non funziona ma si blocca al primo frame: riuscite a capire perchè (cercate di arrivarci da soli ricordando bene come funziona il vostro ciclo principale).

Ecco la risposta: quando l'astronve esplode ponete done = True provocando l'uscita dal ciclo principale; quindi il programma smette di aggiornare lo schermo e presenta un'immagine fissa. E' un problema piuttosto seccante ma per fortuna di facile soluzione: ricorderete che, prima di uscire, il programma esegue un altro ciclo while per aspettare che tutti gli effetti sonori finiscano. Bene, in questo ciclo dovete copiare le istruzioni della sezione Aggiornamento dello schermo continuando a far progredire l'animazione .

ESERCIZIO 20.8: Non c'è bisogno di copiare l'intera sezione Aggiornamento dello schermo: riuscite a capire quali istruzioni sono necessarie? Provate a pensarci o a fare qualche prova ...

RISPOSTA Dovete continuare a ridisegnare la sola surf_play mentre la surf_info non ha più bisogno di essere aggiornata. Togliete anche l'istruzione che chiama la vertical_scroll() e non dimenticate, naturalmente, la animations.update() per aggiornare l'animazione.

Se tutto funziona abbiamo quasi finito, manca ancora qualche dettaglio.

ESERCIZIO 20.9: Provocate un'esplosione (con la lista surf_big_expl anche quando un'astronave aliena riesce ad oltrepassare il lato basso di surf_play: le istruzioni per crearla sono le stesse, posizionate il Rect dell'esplosione in modo che il suo centro coincida con il centro del lato alto dell'alieno.

ESERCIZIO 20.10: Qualche miglioramento estetico. In effetti le esplosioni sono un po' piccoline, ma possiamo rimediare facilmente: SOLUZIONI

Con queste ultime modifiche il risultato dovrebbe essere abbastanza soddisfacente: potete lanciare il vostro programma e godervi le vostre animazioni.

CREARE LE PROPRIE ANIMAZIONI

Come ho detto, nella cartella Sprite/Animations trovate alcune animazioni già suddivise in frames. E se aveste bisogno di qualcosa che non trovate? Esistono molti programmi di grafica e diversi tool online che permettono di suddividere una .gif animata nei suoi frames. Tra i programmi che potete installare nel vostro computer posso citare IrfanView (https://www.irfanview.com/) e 7GIF (https://www.xtreme-lab.net/7gif/it/). Entrambi sono disponibili anche in italiano, credo solo per Windows. Una volta caricata una .gif animata, hanno l'opzione di salvare separatamente i vari frames, dando loro un nome formato da un prefisso seguito da un numero (come quelli che abbiamo usato sopra), e scegliendo il formato (per pygame conviene salvare in .png).

Se non volete installare un'applicazione sul vostro computer potete trovare anche delle utility online: la più nota è Ezgif (https://ezgif.com/split) nel quale potete fare l'upload della vostra .gif e scaricare un file .zip con i frames separati.

Altre volte i vari frames dell'animazione sono riuniti in uno spritesheet, cioè un unico grande foglio dal quale bisogna ritagliare tutti i quadratini. Anche in questo caso possiamo trovare alcune utility dedicate a questo scopo, tra cui troviamo ancora Ezgif (https://ezgif.com/sprite-cutter): un tool online nel quale potete caricare il foglio ed ottenere tutti i frames separati.

Fine della lezione