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:
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))
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:
Modifichiamo ora il nostro programma aliens2.py (qui) aggiungendo lo scrolling:
vertical_scroll()
.blit()
di surf_back
su surf_play
, aggiungete la riga
vertical_scroll(surf_back, 1)
che chiama la funzione scrollando lo sfondo di un pixel.
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.
vertical_scroll()
create un'altra funzione horizontal_scroll(surf, dx)
che
scrolli la Surface da destra verso sinistra.dy
e dx
negativi (facendo muovere lo schermo rispettivamente dal basso verso l'alto e da sinistra a destra).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:
import
del file e
poi potrete usare gli oggetti contenuti come qualsiasi altro oggetto di pygame.image
e
rect
, può essere aggiunto ai normali Group e disegnato chiamando il metodo draw()
di un Group a
cui appartiene.image
).update()
del loro Group, che farà avanzre i frames selezionando l'immagine giusta tra quelle
memorizzate.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
.
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.
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.
. . .
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).
. . .
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 .
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.
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.surf_expl
sostituite la riga
surf_expl.append(img)
con questa:
surf_expl.append(pygame.transform.scale2x(img))
che raddoppia le dimensioni di tutte le immagini caricate.surf_expl
sostituite la riga
surf_big_expl.append(img)
con questa:
surf_big_expl.append(pygame.transform.rotozoom(img, 0, 4.0))
che quadruplica le dimensioni delle immagini.
expl.set_rate(3.5)
in entrambe le parti del programma in cui la create (indentatela correttamente)Con queste ultime modifiche il risultato dovrebbe essere abbastanza soddisfacente: potete lanciare il vostro programma e godervi le vostre 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