Nelle lezioni precedenti abbiamo visto come usare una Surface assieme ad un Rect con le sue stesse dimensioni per posizionare le immagini sullo schermo. Ho molto insistito su questa tecnica perchè è fondamentale per capire il movimento in pygame. Ora che (spero!) avete una buona confidenza con le Surface ed i Rect posso rivelarvi un segreto: esiste in pygame un oggetto più evoluto, che è proprio l'unione di una Surface ed un Rect: questo oggetto si chiama Sprite (il nome deriva da uno dei primissimi computer da casa, il famoso Commodore 64, che aveva la possibilità di programmare degli oggetti grafici così chiamati).
Uno Sprite è un oggetto che ha due attributi fondamentali:
image |
Una Surface che rappresenta l'immagine dello Sprite |
rect |
Un Rect che rappresenta la posizione dello Sprite |
Potreste chiedervi quale vantaggio ci sia ad usare questo nuovo oggetto, quando ormai già sappiamo usare Surface e Rect separatamente. L'uso degli Sprite è più coerente con la programmazione ad oggetti, che prima o poi sarà necessario approfondire: ad una sola immagine sullo schermo corrisponde una sola variabile nel programma (che contiene nei suoi attributi tutto ciò che è necessario per disegnarla). Vedremo più avanti i vantaggi che ci offre questa situazione.
Un altro importante motivo è che gli Sprite sono ottimizzati per quelle situazioni in cui nello schermo
sono presenti molti oggetti simili: finora abbiamo avuto al massimo due oggetti in movimento, ma molti giochi sono molto
più "affollati". Nelle prossime lezioni realizzeremo uno space shooter, e sullo schermo dovremo disegnare contemporaneamente
molte astronavi nemiche e molti proiettili laser. Questa situazione comporta l'uso di liste di oggetti e (nonostante le liste
di Python siano piuttosto facili da usare) porterebbe a complicazioni notevoli. Nel sottomodulo sprite
di pygame sono definiti anche dei particolari tipi di liste, i Group, ottimizzati per lavorare con gli Sprite.
Vediamo ora come si usano concretamente gli Sprite. Per creare uno Sprite sono necessarie di solito quattro linee di codice. Infatti dobbiamo:
pygame.sprite.Sprite()
(il primo sprite
, con la s
minuscola, è il nome del modulo, mentre il secondo è il nome della funzione)image
(l'immagine che verrà disegnata)rect
(di solito ricavandolo dall'immagine)Come al solito aprite il programma originale, salvatelo con il nuovo nome e cominciate a modificarlo. Ecco l'inizio del programma modificato:
import pygame
from pygame.locals import *
from os.path import join
pygame.init()
screen = pygame.display.set_mode((800, 600))
clk = pygame.time.Clock()
# crea lo Sprite del panda
spr_panda = pygame.sprite.Sprite()
spr_panda.image = pygame.image.load(join("Sprites", "panda.png")).convert_alpha()
spr_panda.rect = spr_panda.image.get_rect()
. . .
Nella #10 creiamo lo Sprite spr_panda
, nella #11 assegniamo al
suo atributo image
l'immagine del nostro file e nella #12 assegniamo all'attributo
rect
il Rect corrispondente. Notate la sintassi a destra del segno =
nella #12:
stiamo applicando il metodo get_rect()
all'attributo
image
dell'oggetto spr_panda
, e quindi dobbiamo concatenare due volte
l'operatore punto. Dovremo abituarci ad usare molto spesso istruzioni simili. Non abbiamo aggiunto una quarta riga di codice
perchè ci va bene la posizione predefinita in (0, 0) del Rect.
Muoviamo adesso il nostro panda, modificando la parte centrale del ciclo principale in questo modo:
. . .
# movimento
if spr_panda.rect.left < 0 or spr_panda.rect.right > screen.get_width():
vel_panda[0] *= -1
if spr_panda.rect.top < 0 or spr_panda.rect.bottom > screen.get_height():
vel_panda[1] *= -1
spr_panda.rect.x += vel_panda[0]
spr_panda.rect.y += vel_panda[1]
. . .
Anche qui dovete notare, nelle istruzioni che abbiamo cambiato, la stessa sintassi: il lato sinistro del Rect che contiene il
panda è spr_panda.rect.left
(cioè l'attributo left
dell'attributo
rect
dell'oggetto spr_panda
) e così via.
Dobbiamo infine disegnare il panda sullo schermo, ma a questo punto abbiamo una sorpresa: l'oggetto Sprite non ha
un metodo equivalente al blit()
della Surface e quindi non possiamo disegnare un singolo Sprite sullo schermo. Per
fare questo è necessario introdurre un altro oggetto, il Group.
Un Group è in pratica una lista di Sprites: possiamo aggiungerne uno con il metodo add()
, eliminarlo con il
metodo remove()
, ma soprattutto possiamo disegnare contemporaneamente tutti gli Sprite contenuti
nel Group semplicemente chiamando il metodo draw()
. Per disegnare uno Sprite esso deve necessariamente appartenere
ad un Group (uno Sprite può appartenere anche a più Group contemporaneamente). Quando creiamo uno Sprite possiamo assegnarlo
ad uno o più Group direttamente nel costruttore, scrivendo i loro nomi tra i parametri della funzione. Modificate ancora il
programma così:
import pygame
from pygame.locals import *
from os.path import join
pygame.init()
screen = pygame.display.set_mode((800, 600))
clk = pygame.time.Clock()
# crea un Group con il costruttore
all_sprites = pygame.sprite.Group()
# crea lo Sprite del panda
spr_panda = pygame.sprite.Sprite(all_sprites)
spr_panda.image = pygame.image.load(join("Sprites", "panda.png")).convert_alpha()
spr_panda.rect = spr_panda.image.get_rect()
. . .
# aggiornamento dello schermo
screen.fill("darkgreen")
all_sprites.draw(screen)
pygame.display.flip()
clk.tick(30)
pygame.quit()
Nella #10 abbiamo creato il Group all_sprites
con la funzione costruttore, che restituisce
un Group vuoto. Nella #13 abiamo modificato la chiamata al costruttore Sprite()
, inserendo
il nuovo Sprite spr_panda
nel Group all_sprites
. Infine nella #21 usiamo il
metodo draw()
dell'oggetto Group, che disegna tutti gli Sprite contenuti nel Group (in questo caso il solo
spr_panda
) nelle loro posizioni (il metodo prende come parametro la Surface sulla quale deve disegnare le immagini).
A questo punto abbiamo finito e possiamo vedere nuovamente il nostro panda muoversi e rimbalzare.
Probabilmente le vostre perplessità saranno aumentate: usare gli Sprite richiede in genere più linee di codice, sintassi più complicate e disegnarli sullo schermo sembra un'operazione piuttosto macchinosa. Come vi ho detto l'uso degli Sprite è conveniente in quelle situazioni in cui le immagini da disegnare sullo schermo sono molte.
Ecco un esempio più significativo. Questo programma crea dei fantasmini (quelli di Pac Man) di quattro colori diversi ad intervalli e posizioni casuali, facendoli muovere da sinistra a destra sullo schermo. Per creare i fantasmi il programma usa un timer di pygame, caricandolo con intervalli di tempo generati casualmente (abbiamo già imparato a farlo qui).
import pygame, random
from pygame.locals import *
from os.path import join
# inizializzione
pygame.init()
screen = pygame.display.set_mode((800, 600))
clk = pygame.time.Clock()
# risorse e variabili
ghost_images = []
fname = join("Sprites", "pac-classic", "ghost-red-right.png")
ghost_images.append(pygame.image.load(fname).convert_alpha())
fname = join("Sprites", "pac-classic", "ghost-lblue-right.png")
ghost_images.append(pygame.image.load(fname).convert_alpha())
fname = join("Sprites", "pac-classic", "ghost-orange-right.png")
ghost_images.append(pygame.image.load(fname).convert_alpha())
fname = join("Sprites", "pac-classic", "ghost-pink-right.png")
ghost_images.append(pygame.image.load(fname).convert_alpha())
all_ghosts = pygame.sprite.Group()
TIMERSHOT = pygame.event.custom_type()
pygame.time.set_timer(TIMERSHOT, random.randrange(200, 2501))
# ciclo principale
done = False
while not done:
for ev in pygame.event.get():
if ev.type == QUIT:
done = True
# se l'evento e' il timer ...
elif ev.type == TIMERSHOT:
pygame.time.set_timer(TIMERSHOT, random.randrange(200, 2501))
ghost = pygame.sprite.Sprite(all_ghosts)
ghost.image = ghost_images[random.randrange(4)]
ghost.rect = ghost.image.get_rect()
maxy = screen.get_height() - ghost.rect.height - 9
ghost.rect.topright = (-1, random.randrange(10, maxy))
for ghost in all_ghosts:
ghost.rect.x += 4
screen.fill((160, 160, 255))
all_ghosts.draw(screen)
pygame.display.flip()
clk.tick(30)
pygame.quit()
Ci sono parecchie novità e dobbiamo analizzarlo attentamente (cercate soprattutto di capire l'uso delle varie funzioni del modulo
random
, che troviamo in più punti).
ghost_images
e dalla riga
#12 in poi carichiamo nella lista le immagini dei quattro fantasmini, in modo che ad ogni indice nella lista
corrisponda un colore diverso (ricorderete che il metodo append()
aggiunge un elemento in fondo a una lista: in questo
modo ghost_imges[0]
è il fantasma rosso, ghost_images[1]
quello azzurro, etc.);fname
il nome del file ottenuto con la join()
e
nella seconda uso questa variabile come argomento della load()
; le linee guida della programmazione in Python raccomandano
di evitare righe più lunghe di 80 caratteri o difficilmente leggibili;all_ghosts
che ci servirà per disegnare gli Sprite;all_ghosts
.image
con un'immagine casuale tra le quattro caricate, poi l'attributo rect
. Infine, nella
#38 posizioniamo il Rect (la x del lato destro è -1, quindi il fantasmino è inizialmente fuori dallo
schermo a sinistra, la y è ancora una volta casuale, con i parametri della randrange()
) scelti in modo che l'immagine
sia tutta entro lo schermo). Riuscite a capire come ho calcolato la variabile maxy
nella #37?for
su tutti i fantasmi del Group all_ghosts
, nel quale spostiamo tutti i fantasmi a destra di
4 pixel.all_ghosts
usando il metodo draw()
.Fate partire il programma e finalmente vedrete una scena con oggetti che compaiono, si muovono e spariscono. Manca ancora una cosa:
dobbiamo eliminare dal Group all_ghosts
i fantasmi man mano che escono dallo schermo. Infatti ogni volta che creiamo un
fantasma aggiungiamo uno Sprite al Group, ed il programma (nella #40) continua ad aggiornare la posizione di
tutti i rispettivi Rect, sprecando potenza di calcolo per muovere anche i fantasmi che ormai non si vedono più.
In un animato livello di un videogioco potreste magari creare migliaia di Sprite (pensate a giochi tipo shooter dove si sparano
proiettili continuamente): se non si eliminassero quelli ormai inutili ad un certo punto il vostro ciclo principale comincierebbe
a rallentare impegnando inutilimente il processore del computer.
Oltre al costruttore Sprite()
, che permette di inserire immediatamente lo Sprite in uno o più Group, ci sono altri
metodi dell'oggetto Sprite che controllano la sua appartenenza ai Group:
Metodo | Esempio | Significato |
---|---|---|
add() |
sp.add(all_sprites) |
Aggiunge lo Sprite (già esistente) ad uno o più Group, che vanno indicati nei parametri |
remove() |
sp.remove(all_sprites) |
Rimuove lo Sprite (già esistente) da uno o più Group, indicati nei parametri. |
kill() |
sp.kill() |
Rimuove lo Sprite da tutti i Group al quale appartiene. E' il metodo che si chiama di solito quando uno Sprite "non serve più" e può essere eliminato. |
alive() |
if sp.alive(): |
Restituisce True se lo Sprite appartiene a qualche Group. |
. . .
for ghost in all_ghosts:
ghost.rect.x += 4
if ghost.rect.left > screen.get_width():
ghost.kill()
. . .
ESERCIZIO 14.2: Aumentate o diminuite la frequenza dei fantasmi agendo sui parametri dei timer. Cercate anche di capire il significato
dei parametri della randrange()
nella riga #38 osservando cosa succede se li cambiate.
Facciamo infine qualche considerazione: a differenza dei nostri primi programmi, nei quali avevamo uno o due oggetti che rimanevano sullo schermo dall'inizio alla fine, fantasmi.py crea e distrugge dinamicamente un numero imprecisato di Sprite durante la sua esecuzione (una situazione piuttosto comune nei videogiochi): da questo nasce spontaneamente il bisogno di un "contenitore" per i nostri Sprite. Se avessimo utilizzato il nostro vecchio metodo con Surface e Rect separati avremmo avuto bisogno di due liste di Python separate: una per le Surface (i fantasmi hanno immagini diverse l'uno dall'altro) ed una per i Rect (ogni fantasma è in una posizione diversa). Inoltre avremmo dovuto tenerle sincronizzate, aggiungendo o togliendo oggetti sempre a tutte e due le liste contemporaneamente. In questi casi comincia ad essere chiaro il vantaggio di avere tutte le nostre informazioni racchiuse in una sola variabile; altri vantaggi, legati alla programmazione ad oggetti, li vedremo più avanti.
Anche per le collisioni ci sono nel sottomodulo sprite
delle funzioni apposite, ottimizzate per gli Sprite e
per i Group; qui possiamo vedere solo le principali.
La funzione
left
e right
controllando se i loro Rect si intersecano (come
abbiamo fatto anche noi fino ad adesso). Nel modulo ci sono altre funzioni che usano
metodi più precisi (ad esempio un cerchio o una "maschera") per controllare se gli Sprite collidono; vi invito a leggere la
documentazione qui:
https://www.pygame.org/docs/ref/sprite.html
per saperne di più.
La funzione
Parametro | Tipo di dato | Significato |
---|---|---|
sprite |
oggetto Sprite | Lo Sprite da testare |
group |
oggetto Group | Il Group degli Sprite da testare |
dokill |
booleano | Obbligatorio: se è True gli eventuali Sprite che collidono saranno rimossi
automaticamente da ogni Group eseguendo il metodo kill() |
collided |
funzione | Opzionale: se non è presente la funzione esegue il test standard usando i Rect degli Sprite. Un utente esperto potrebbe usare una sua funzione personalizzata |
La funzione restituisce una lista in cui sono contenuti tutti gli Sprite del Group che collidono con lo Sprite di partenza.
Notate che la possibilità, offerta dal parametro dokill
, di eliminare automaticamente gli Sprite che collidono senza
dover scrivere altre istruzioni, è una notevole comodità.
Esiste anche una funzione simile per le collisioni tra due Group:
pygame.sprite.groupcollide(group1, group2, dokill1, dokill2, collided = None) -> Sprite_dict
Parametro | Tipo di dato | Significato |
---|---|---|
group1 |
oggetto Group | Il primo Group da testare |
group2 |
oggetto Group | Il secondo Group da testare |
dokill1 |
booleano | Se è True gli eventuali Sprite che collidono in group1 saranno cancellati eseguendo
il metodo kill() |
dokill2 |
booleano | Se è True gli eventuali Sprite che collidono in group2 saranno cancellati eseguendo
il metodo kill() |
collided |
funzione | Come nella funzione precedente |
Questa funzione restituisce un dizionario di Python, cioè un tipo di dato nel quale ad ogni Sprite del Group
group1
è associata una lista di tutti gli Sprite del Group group2
che collidono con esso. Se avete bisogno
di aiuto sui dizionari potete consultare qui il mio tutorial intermedio.
In una delle prossime lezioni vi mostrerò un esempio dell'uso di questa funzione.
Infine si può usare anche una funzione più semplice per le collisioni tra uno Sprite e un Group:
pygame.sprite.spritecollideany(sprite, group, collided = None) -> Sprite
sprite
ed il Group group
: se non ce ne sono ritorna None
,
altrimenti il primo Sprite del Group che collide (l'ordine è casuale); non consente di cancellare gli Sprite.
ghost = pygame.sprite.Sprite()
if ... else
con la spritecollideany()
: se esso è
vero usate la kill()
sullo Sprite ghost
, altrimenti aggiungetelo al Group con la
add()
.spritecollideany()
restituirà sempre True.kill()
, perchè lo Sprite non
è ancora stato assegnato ad alcun Group. Potete modificare il programma usando solo l'if
senza l'else
.E' ora il momento di mettere in pratica quanto abbiamo imparato sugli Sprite, realizzando un altro gioco nelle prossime due lezioni.
Fine della lezione