10: LE IMMAGINI

FILES GRAFICI

E' giunta l'ora di migliorare drasticamente l'aspetto grafico dei nostri programmi, usando le immagini anzichè quadratini colorati. Prima di vedere come si fa è necessario soffermarci un attimo sui vari formati di files grafici supportati da pygame.

.bmp Sono files nei quali sono memorizzati uno dopo l'altro tutti i pixel della nostra immagine. Possono avere profondità di colore diverse (da 2 bit per ogni pixel, cioè in bianco e nero, a 32 bit, con 16 milioni di colori più la trasparenza) e quindi si possono usare sia per disegni che per immagini fotografiche. Sono però files non compressi e quindi occupano molto spazio. Nei giochi vengono utilizzati qualche volta per piccoli disegni.
.jpg o .jpeg Sono files compressi, ottimizzati per la resa di immagini fotografiche (con molte sfumature di colori diverse). Nei videogiochi vengono utilizzati soprattutto per gli sfondi.
.gif Sono files compressi, ottimizzati per rappresentare disegni (con poche sfumature di colore): possono avere al massimo 256 colori diversi, di cui uno può essere usato per la trasparenza (in modo da avere immagini non rettangolari). Supportano anche una semplice forma di animazione: in un solo file si possono racchiudere più immagini che vengono poi visualizzate in sequenza. Purtroppo pygame non supporta questa caratteristica e dovremo più avanti trovare un modo per aggirare questa mancanza.
.png Sono simili ai .gif (in effetti nacquero come un'alternativa al formato .gif che era protetto da copyright). Come essi possono avere al massimo 256 colori, ma ogni pixel può essere reso più o meno trasparente in maniera indipendente dagli altri. Sono probabilmente il formato oggi più popolare per le immagini dei videogiochi.
altri formati pygame supporta anche altri formati meno diffusi che sono elencati qui: http://www.pygame.org/docs/ref/image.html

Secondo la documentazione ufficiale il supporto è garantito solo per i file .bmp, ma per gli altri non dovrebbero esserci problemi sui sistemi operativi attuali. Per essere sicuri che tutti i formati siano supportati potete digitare in IDLE:


>>> import pygame
>>> pygame.image.get_extended()

True

Se la get_extended() vi risponde True tutti i formati dei file sono disponibili.

Vi domanderete: ma dove li troviamo questi files grafici? Bene, ci sono molti siti da cui li potete scaricare gratis; in genere, quando cercate qualcosa di preciso, la ricerca diventa lunga e faticosa e richiede parecchia esperienza. Per questo tutorial vi ho preparato io una nutrita collezione (circa 45 Mb) di risorse liberamente utilizzabili: scaricate il file Risorse.zip ed estraete il suo contenuto nella cartella dove salvate i vostri programmi pygame. Troverete quattro nuove cartelle Backgrounds, Effects, Music e Sprites contenenti files grafici e sonori che useremo nel seguito (ognuna di loro è a sua volta divisa in altre cartelle che raccolgono il lavoro dei vari autori).

Ho preparato anche un approfondimento che vi spiega dove potete cercare questi file e quali sono i problemi legali nell'utilizzo di risorse create da altre persone. L'approfondimento contiene anche le informazioni di Copyright, con il nome dell'autore e il link originale di ogni file.

USARE LE IMMAGINI

Per utilizzare le immagini nei nostri programmi la prima cosa che dovremo fare è caricarle da un file grafico. Per fare questo si usa la funzione load() che si trova nel sottomodulo image. Questa funzione prende come argomento una string (il nome del nostro file) e ci restituisce una Surface con l'immagine del file. A questo punto, potremo usare la Surface ed il Rect corrispondente come abbiamo fatto nei programmi precedenti. Quindi per caricare un file dovremo scrivere:

surf = pygame.image.load("nome_del_file")

ed otterremo la Surface surf con la nostra immagine.

Purtroppo c'è una difficoltà in più: se il file non è nella stessa cartella del nostro programma .py è necessario indicarne il percorso (cioè anche la cartella di appartenenza). Questo accade molto spesso, in quanto i programmatori hanno l'abitudine di separare la grafica, i suoni ed il codice in cartelle diverse per non creare confusione, ma sfortunatamente Windows, Linux e Mac gestiscono i percorsi (path) in maniera diversa (potete consultare questo approfondimento). Se non indicate con precisione il percorso la load() non troverà il file ed il programma si fermerà con un errore couldn't open file.

Per superare queste difficoltà Python mette a disposizione una serie di funzioni che si trovano nel modulo os.path. La documentazione ufficiale di pygame consiglia in effetti di usare, per compatibilità con tutti i sistemi operativi, la funzione os.path.join(), e noi ci adegueremo indicando nel seguito del tutorial i percorsi dei file in questo modo:

Come primo esercizio modificheremo il vecchio programma quadrato_mobile.py (qui) sostituendo il nostro vecchio quadrato giallo con l'immagine di un panda. Ecco il programa:

NUOVO PROGRAMMA: panda.py

import pygame
from pygame.locals import *
from os.path import join

pygame.init()
screen = pygame.display.set_mode((800, 600))
clk = pygame.time.Clock()

# carica l'immagine del panda
surf_panda = pygame.image.load(join("Sprites", "panda.png"))
rect_panda = surf_panda.get_rect()

# velocita' del panda
vel_panda = [2, 2]

# ciclo principale
done = False
while not done:
    # sottociclo degli eventi
    for ev in pygame.event.get():
        if ev.type == QUIT:
            done = True

    # movimento
    if rect_panda.left < 0 or rect_panda.right > screen.get_width():
        vel_panda[0] *= -1
    if rect_panda.top < 0 or rect_panda.bottom > screen.get_height():
        vel_panda[1] *= -1

    rect_panda.x += vel_panda[0]
    rect_panda.y += vel_panda[1]
    
    # aggiornamento dello schermo
    screen.fill("green4")
    screen.blit(surf_panda, rect_panda)
    pygame.display.flip()

    clk.tick(30)

pygame.quit()

Come vedete le uniche novità sono nelle istruzioni #3, che importa la funzione join(), e #10, che carica l'immagine e la assegna alla Surface surf_panda; subito dopo, nella #11, usiamo il metodo get_rect() per costruire il corrispondente Rect (cosa a questo punto utilissima perchè noi non conosciamo le dimensioni della Surface). Tutto il resto procede come abbiamo visto nel programma quadrato_mobile.py della lezione precedente.

ESERCIZIO 10.1: Sostituite l'immagine del panda con qualche altro file grafico: provate ad esplorare le sottocartelle della cartella Sprites (caricando, ad es. "Sprites/CrawlStone/apple.png" ed altri simili).

UN' IMMAGINE COME SFONDO

Adesso abbandoniamo anche il nostro colore di sfondo uniforme per ottenere un effetto più gradevole. La tecnica comunemente usata per gli sfondi è quella del tiling (dall'inglese tile: mattonella). Si prende una immagine di piccole dimensioni e la si affianca molte volte in righe e colonne in modo da ricoprire tutta l'area di sfondo. Naturalmente l'immagine deve essere disegnata in modo da poter essere affiancata sia orizzontalmente che verticalmente senza che si vedano i margini.

Dato che dovremo probabilmente usare spesso questa tecnica ci conviene definire una funzione di Python, per poterla riutilizzare nei nostri programmi. La funzione prenderà come parametri due Surface, surf_little (la nostra mattonella) e surf_big (la superficie da ricoprire).


def tile_surface(surf_little, surf_big):       # surf_little e' la mattonella, surf_big l'area da ricoprire
    w_little = surf_little.get_width()         # larghezza della mattonella
    h_little = surf_little.get_height()        # altezza della mattonella
    w_big = surf_big.get_width()               # larghezza da ricoprire
    h_big = surf_big.get_height()              # altezza da ricoprire
    for y in range(0, h_big, h_little):        # incremento della y
        for x in range(0, w_big, w_little):    # incremento della x
            surf_big.blit(surf_little, (x, y)) # posiziona la mattonella

Non dovrebbe essere molto difficile capire cosa fa la funzione: nelle righe #2 e #3 assegniamo alle variabili w_little e h_little la larghezza e l'altezza della mattonella (w sta per width, larghezza ed h per height, altezza), ricavandole dai metodi get_width() e get_height() della Surface. Similmente nelle righe #4 e #5 per la larghezza e l'altezza della superficie da ricoprire. A questo punto cominciamo un doppio ciclo, copiando la mattonella sulla nostra superficie: il ciclo più esterno (con la y) è il passo verticale: per ogni y copiamo la mattonella affiancata a destra finchè non abbiamo coperto tutta l'immagine (con il ciclo sulla x). Ricordate che il terzo parametro della range() è il passo: la y e la x si incrementeranno del giusto valore per posizionare le mattonelle esattamente affiancate.

Nella cartella Backgrounds troverete alcune immagini adatte ad essere usate come sfondi; utilizziamo ora la nostra funzione per ricoprire il nostro schermo (cioè la Surface screen).

NUOVO PROGRAMMA: panda_sfondo1.py
ESERCIZIO 10.2: Riprendete il programma panda.py, salvatelo con il nuovo nome e modificatelo in questo modo (non modificate il file originale, che ci servirà ancora più avanti):
  1. Per prima cosa copiate la funzione tile_surface() tra gli import e l'inizio del programma (vi ricordo che le definizioni delle funzioni vanno sempre scritte prima del programma principale);
  2. Ora caricate il file grafico "concrete01.jpg" che si trova nella cartella "Backgrounds" assegnandolo alla variabile surf_tile, come abbiamo visto sopra. Vi conviene scrivere questa riga dopo la #10 del programma precedente, in modo da raggruppare tutte le righe che eseguono operazioni simili. Non c'è bisogno di associare un Rect alla surf_tile, perchè la nostra funzione si preoccuperà di posizionarla correttamente;
  3. Infine sostituite la riga #34 del programma (che colorava di verde lo schermo) con questa:
    
    .   .   .
    tile_surface(surf_tile, screen)
    .   .   .
    
    (naturalmente dovete indentarla correttamente). Vi ricordo che quando chiamiamo la funzione i parametri attuali, cioè quelli che indichiamo nella chiamata (surf_tile e screen) vengono sostituiti al posto dei parametri formali indicati nella definizione (surf_little e surf_big). Inoltre le variabili create all'interno della funzione sono variabili locali e non hanno nulla a che fare con quelle esterne (neanche se hanno lo stesso nome).
SOLUZIONI

Se avete fatto tutto senza errori l'effetto dovrebbe essere abbastanza piacevole: il panda ora si muove su un gradevole sfondo anzichè su un colore uniforme. Vi faccio però notare che, dal punto di vista di un programmatore, il programma presenta una notevole incongruenza: inserendo la chiamata a tile_surface() all'interno del ciclo principale la chiamiamo 30 volte al secondo, facendole ridisegnare ogni volta le mattonelle sullo schermo. Questo è uno spreco di risorse: è senz'altro più efficiente fare il disegno una volta per tutte su una Surface apposita e poi, nel ciclo principale, usare la solita blit() (essa è infatti ottimizzata a livello hardware, quindi è velocissima rispetto alla serie di istruzioni Python che compongono la funzione tile_surface()).

Possiamo risolvere la cosa in questo modo: prima di iniziare il ciclo principale possiamo scrivere:


.   .   .
surf_back = screen.copy()               # crea una nuova Surface con le stesse dimensioni dello schermo
tile_surface(surf_tile, surf_back)      # copre la surf_back con le mattonelle (una volta per tutte)
.   .   .

dentro il ciclo, quando aggiorniamo lo schermo, scriveremo:


.   .   .
screen.blit(surf_back, (0, 0))         # ricopre screen con lo sfondo già disegnato
screen.blit(surf_panda, rect_panda)    # disegna il panda
.   .   .
ESERCIZIO 10.3: Modificate ancora il programma in questo modo. Ho volutamente omesso i numeri di riga e l'indentazione, dovete pensarci voi.
SOLUZIONI

Probabilmente non noterete nessun cambiamento, perchè i moderni computer sono velocissimi e non si fanno certo rallentare da un po' di calcoli, ma è bene abituarsi a non sprecare inutilmente risorse di calcolo.

ESERCIZIO 10.4: Nella cartella Backgrounds/Dim troverete altre immagini adatte ad essere usate come mattonelle; sostituite la mattonella dello sfondo con qualcuna di esse.

Oltre al tiling c'è anche la possibilità di avere effettivamente un'unica grossa immagine come sfondo: questo è necessario se vogliamo, ad esempio, avere come sfondo un paesaggio. Qui sorge il problema delle dimensioni dell'immagine, che potrebbero essere diverse da quelle della finestra. Parleremo di questo nella prossima lezione.

I METODI convert() E convert_alpha()

La documentazione ufficiale della funzione load() raccomanda di usare sulle Surface caricate da file i due metodi convert() o convert_alpha(), che trasformano il formato interno della Surface in uno ottimizzato per essere disegnato più velocemente dalla blit(). A dire il vero (vedi anche qui) la documentazione è un po' oscura: la convert() dovrebbe essere più veloce della seconda ma non mantiene la trasparenza pixel per pixel (che invece è conservata dalla convert_alpha()).

La load() e la convert() (o convert_alpha()) vengono di solito riunite in un'unica riga di programma, concatenando i punti in questo modo:

surf_panda = pygame.image.load(join("Sprites", "panda.png")).convert()
surf_panda = pygame.image.load(join("Sprites", "panda.png")).convert_alpha()

notate la sintassi: la riga applica il metodo convert() alla Surface restituita dalla funzione pygame.image.load(join("Sprites", "panda.png")).

L'utente naturalmente si chiede: quale delle due devo usare? Il problema è piuttosto fastidioso, perchè se sbaglite pygame potrebbe non riconoscere la trasparenza e voi vedreste le immagini muoversi circondate da un antipatico sfondo rettangolare. Come ho detto all'inizio, i diversi formati dei files grafici gestiscono la trasparenza in maniera diversa. I file .png hanno la possibilità di indicare un livello di trasparenza separato per ogni pixel (cosa non riconsciuta dalla convert()), mentre i .gif hanno un unico colore trasparente. Quindi, in pratica, dovremo usare la convert_alpha() per caricare i file .png trasparenti. Nel seguito del tutorial userò questa strategia:

ESERCIZIO 10.5: Modificate, nel programma panda_sfondo1.py la riga che carica il file "panda.png", aggiungendo la funzione convert(). Se lanciate il programma dovreste vedere un rettangolino nero attorno al panda: abbiamo (volutamente!) sbagliato. Correggete usando la convert_alpha(). Per la mattonella surf_tile (che sicuramente non avrà pixel trasparenti) usate la convert().
SOLUZIONI

A volte può succedere che la trasparenza non funzioni neanche con le .gif; questo è probabilmente dovuto al fatto che lo sfondo dell'immagine è stato impostato con un colore "solido" e non trasparente. Se sapete usare un editor di immagini potreste modificare voi il file rendendo lo sfondo trasparente; una scappatoia è data da queste linee di codice:


surf = pygame.image.load("nome_file").convert()
surf.set_colorkey(surf.get_at((0, 0)))

Dopo aver caricato la vostra immagine con la riga #1 potete aggiungere la #2. Se siete curiosi di sapere come funziona eccovi una spiegazione: pygame ha un modo autonomo di rendere un colore trasparente, indipendentemente dal file grafico caricato. Una Surface ha la possibilità di indicare uno dei suoi colori come trasparente, tramite il metodo set_colorkey(), che prende come parametro la solita tripla (r,g,b). Nella #2 diciamo a pygame di considerare trasparente il colore del pixel di coordinate (0, 0) (cioè l'angolo in alto a sinistra, che molto probabilmente farà parte dello sfondo), ottenuto con il metodo get_at(): in questo modo pygame non disegna quel colore. Di solito questo trucchetto funziona.

Fine della lezione