19: SUDDIVIDERE LO SCHERMO

DIVIDERE LO SCHERMO IN SOTTOFINESTRE

In tutti gli esempi realizzati finora abbiamo sempre fatto coincidere l'area di gioco con l'intera Surface screen. Nella realtà le cose vanno, di solito, diversamente: solo una parte della finestra principale sarà dedicata al gioco vero e proprio, e ci saranno altre aree per le info (punteggio, numero di vite...), menù di scelta, etc. Ora che abbiamo imparato a scrivere sulla finestra di pygame possiamo realizzare anche noi una cosa simile.

Per dividere lo schermo in sottofinestre useremo più Surface secondarie, disegnando gli Sprite o le scritte su di esse, e facendo alla fine la blit() delle Surface secondarie su quella principale screen. Questa tecnica è piuttosto laboriosa per il computer (che deve fare molte più blit()), ma è molto comoda per noi, perchè quando disegniamo su una Surface possiamo usare le coordinate relative ad essa (l'angolo in alto a sinistra della Surface ha coordinate (0, 0), etc.) senza bisogno di sapere dove quella Surface sarà poi posizionata nella finestra principale.

In questo modo, se volessimo cambiare la disposizione delle nostre sottofinestre, dovremmo modificare solo le poche righe di codice che posizionano le Surface sullo schermo, mentre il disegno su ogni Surface rimarrebbe immutato. Naturalmente sarebbe anche possibile continuare a disegnare tutto direttamente su screen, cosa che affaticherebbe di meno il computer ma sarebbe più complicata per noi. Potrete provarci quando diventerete esperti programmatori in pygame.

Modifichiamo ora il nostro programma aliens.py in modo da suddividere la finestra in un'area di gioco, un'area info ed un titolo. Ecco uno schema della suddivisione

Immagine screen

Facciamo alcune osservazioni:

NUOVO PROGRAMMA: aliens2.py

Riaprite il vecchio programma (con le modifiche fatte negli esercizi della Lezione 16), salvatelo con il nuovo nome e cominciate a modificarlo:

ESERCIZIO 19.1: Leggendo la documentazione ufficiale della funzione pygame.display.set_mode() apprendiamo che in realtà il primo parametro size ha il valore di default (0, 0). Se omettiamo il parametro otteniamo una finestra delle stesse dimensioni dello schermo del nostro computer, che però è senza la barra superiore con il titolo ed il pulsante per chiuderla. Cambiate quindi l'istruzione: se durante le vostre prove il programma dovesse bloccarsi e doveste chiuderlo potete passare alla finestra di IDLE (in Windows dovete premere <ALT> + <TAB>) e scrivere pygame.quit().

ESERCIZIO 19.2: Modificate la sezione Risorse grafiche introducendo queste nuove variabili:
surf_title Caricate il file grafico "Sprites/Aliens_text.png" (usate la convert_alpha() per attivare la trasparenza)
surf_play Create, con il costruttore pygame.Surface(), una Surface con le stesse dimensioni di surf_back (per trovarle entrambe con una sola funzione potete usare il metodo get_size() che restituisce direttamente la dupla (x, y) (vedi la documentazione ufficiale)
surf_info Create, con il costruttore, una Surface larga 250 pixel ed alta come la surf_play
fnt Caricate, con la funzione SysFont(), il font Arial da 48 pt, assegnandolo alla variabile fnt

In queste ultime lezioni cercheremo di adottare uno stile un po' più professionale eliminando il più possibile i cosiddetti "magic numbers" sostituendoli con delle costanti (vedi l'approfondimento qui), anche a costo di scrivere qualche riga di codice in più. Create quindi un'altra sezione Costanti tra le sezioni Risorse sonore e Altre variabili con un commento simile agli altri.

ESERCIZIO 19.3: Nella sezione Costanti definite le seguenti costanti:
Nome Valore Significato
TIMERSHOT . . . Spostate qui la riga che avevate già scritto nella sezione Inizio del gioco
BACKCOLOR (10, 160, 10) Il colore dello sfondo (verde scuro)
TEXTCOLOR (255, 255, 40) Il colore del testo (giallo)
SHIP_TOPLEFT (320, 500) La posizione iniziale della nostra nave. Modificate la riga nella sezione Altre variabili che assegna la posizione iniziale della nostra nave
PLAY_TOPLEFT tuple di due interi Il punto che useremo come secondo parametro nella blit() per posizionare surf_text
INFO_TOPLEFT tuple di due interi Il punto che useremo come secondo parametro nella blit() per posizionare surf_info

Scrivete voi le espressioni che calcolano le coordinate x ed y di PLAY_TOPLEFT e INFO_TOPLEFT guardando la figura sopra (ricordate che vanno tra parentesi perchè sono delle tuple di Python); per la larghezza e l'altezza delle varie Surface usate i metodi get_width() e get_height() in modo da rendere indipendenti le istruzioni dalla larghezza delle Surface (che potreste anche non conoscere).

ESERCIZIO 19.4: Modificate infine la sezione Aggiornamento dello schermo in questo modo (attenti all'indentazione!):

.   .   .
# --- Aggiornamento dello schermo
    surf_play.blit(surf_back, (0, 0))
    all_sprites.draw(surf_play)
    screen.blit(surf_play, PLAY_TOPLEFT)
    screen.blit(surf_info, INFO_TOPLEFT)
    pygame.display.flip()
.   .   .
  
Nelle #3 e #4 disegniamo lo sfondo e gli Sprite non più su screen ma sulla surf_play. Nella #5 disegniamo l'intera Surface surf_play sulla finestra principale, usando la variabile PLAY_TOPLEFT come angolo in alto a sinistra. Se volessimo cambiare il posizionamento della Surface ci basterebbe cambiare il valore di PLAY_TOPLEFT, lasciando tutto il resto del codice invariato. Analogamente facciamo nella #6 per la Surface surf_info, che per il momento è inutilizzata.

A questo punto potete lanciare il programma, che non dovrebbe andare in crash. Ma ci sono molte cose che non vanno: tutto lo schermo è nero, vediamo alcune astronavi nemiche "disegnate a metà" a destra, la nostra astronave può uscire dall'area di gioco, e a volte il gioco finisce improvvisamente anche se noi abbiamo eliminato tutte le navi nemiche!

ESERCIZIO 19.5: Nella sezione Inizio del gioco aggiungete queste righe:

.   .   .  
screen.fill(BACKCOLOR)
rect_title = surf_title.get_rect()
rect_title.midtop = (surf_play.get_width() // 2 + 20, 20)
screen.blit(surf_title, rect_title)
.   .   .
  
La #2 colora tutto lo schermo di verde. Le altre tre righe servono per posizionare e disegnare la surf_title che contiene il logo del gioco. Durante il ciclo principale non ci sarà più bisogno di aggiornare queste aree.

Ora, se lanciamo il programma, l'effetto è più gradevole: vediamo nettamente le tre Surface (la surf_info è ancora tutta nera), ma il gioco non funziona ancora. Il motivo è semplice: nel ciclo principale si "dava per scontato" che l'area di gioco coincidesse con tutta la Surface screen. Ora invece screen è diventata più grande, mentre le dimensioni dell'area di gioco sono rimaste uguali (e coincidono con quelle di surf_play). Avremo perciò astronavi aliene che sono generate fuori della surf_play e che noi non vediamo.

ESERCIZIO 19.6: Andate a caccia, in tutto il Ciclo principale con le sue sottosezioni, delle istruzioni che fanno riferimento alla larghezza ed altezza della Surface screen, e sostituitele con istruzioni che usano larghezza ed altezza di surf_play. Questo è quello che succede quando un programma non è ben progettato dall'inizio: introdurre una nuova funzionalità comporta la modifica di molte righe di programma separate tra loro, con una difficile ricerca. I programmatori esperti (presto lo sarete anche voi!) sanno come evitare queste situazioni. Io ho contato 3 righe da modificare, riuscite a trovarle anche voi?

Bene, ora il gioco dovrebbe funzionare correttamente, senza navi che escono dallo schermo. Rimane da implementare il punteggio e da disegnarlo sulla Surface surf_info. Ci sarà naturalmente bisogno di una variabile score per memorizzare il punteggio; possiamo stabilire che per ogni nemico abbattuto esso aumenti di 10 punti.

ESERCIZIO 19.7: Implementiamo il punteggio.

Vi faccio notare che il colore con cui coloriamo lo sfondo di surf_info nella riga #6 dell'esempio precedente deve essere lo stesso con cui all'inizio coloriamo tutta la finestra screen (nell'Esercizio 19.5): usando la costante BACKCOLOR questo si ottiene automaticamente senza che noi dobbiamo preoccuparcene nel caso volessimo cambiare colore.

A questo punto abbiamo finito questa terza parte, vi lascio il programma realizzato da me per confrontarlo con il vostro. Nel prossimo capitolo aggiungeremo altri effetti grafici.


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

# --- Inizializzazione
pygame.init()
screen = pygame.display.set_mode()
pygame.display.set_caption("Aliens")
clk = pygame.time.Clock()

# --- Risorse grafiche
surf_back = pygame.image.load(join("Backgrounds", "Space.png")).convert()
surf_ship = pygame.image.load(join("Sprites", "SpaceArt", "player.png")).convert_alpha()
surf_alien = pygame.image.load(join("Sprites", "SpaceArt", "enemyShip.png")).convert_alpha()
surf_bullet = pygame.image.load(join("Sprites", "SpaceArt", "laserGreen.png")).convert_alpha()
surf_title = pygame.image.load(join("Sprites", "Aliens_text.png")).convert_alpha()
surf_play = pygame.Surface(surf_back.get_size())
surf_info = pygame.Surface((250, surf_play.get_height()))
fnt = pygame.font.SysFont("Arial", 48)

# --- Risorse sonore
sound_laser = pygame.mixer.Sound(join("Effects","smc-wwvi", "synthetic_laser.ogg"))
sound_expl = pygame.mixer.Sound(join("Effects", "smc-wwvi", "synthetic_gunshot_2.ogg"))
sound_end = pygame.mixer.Sound(join("Effects", "smc-wwvi", "big_explosion.ogg"))
pygame.mixer.music.load(join("Music", "SoundImage", "Techno-Gameplay_Looping.ogg"))

# --- Costanti
TIMERSHOT = pygame.event.custom_type()
BACKCOLOR = (10, 160, 10)
TEXTCOLOR = (255, 255, 40)
SHIP_TOPLEFT = (320, 500)
PLAY_TOPLEFT = (20, 40 + surf_title.get_height())
INFO_TOPLEFT = (40 + surf_play.get_width(), 40 + surf_title.get_height())

# --- Altre variabili
pressed = None
all_sprites = pygame.sprite.Group()
bullets = pygame.sprite.Group()
aliens = pygame.sprite.Group()
spr_ship = pygame.sprite.Sprite(all_sprites)
spr_ship.image = surf_ship
spr_ship.rect = surf_ship.get_rect()
spr_ship.rect.topleft = SHIP_TOPLEFT
score = 0

# --- Inizio del gioco
pygame.mixer.music.play(-1)
pygame.time.set_timer(TIMERSHOT, random.randrange(400, 1501))
screen.fill(BACKCOLOR)
rect_title = surf_title.get_rect()
rect_title.midtop = (surf_play.get_width() // 2 + 20, 20)
screen.blit(surf_title, rect_title)

# --- Ciclo principale
done = False
while not done:
    # --- Ciclo degli eventi
    for ev in pygame.event.get():
        if ev.type == pygame.QUIT:
            done = True
        elif ev.type == KEYDOWN and ev.key in (K_LEFT, K_RIGHT):
            pressed = ev.key
        elif ev.type == KEYUP and ev.key == pressed:
            pressed = None
        elif ev.type == KEYDOWN and ev.key == K_SPACE and len(bullets) < 8:
            bullet = pygame.sprite.Sprite(all_sprites, bullets)
            bullet.image = surf_bullet
            bullet.rect = surf_bullet.get_rect()
            bullet.rect.midbottom = spr_ship.rect.midtop
            sound_laser.play()
        elif ev.type == TIMERSHOT:
            pygame.time.set_timer(TIMERSHOT, random.randrange(400, 1501))
            alien = pygame.sprite.Sprite(all_sprites, aliens)
            alien.image = surf_alien
            alien.rect = surf_alien.get_rect()
            alien.rect.bottomleft = (random.randrange(0, surf_play.get_width() - alien.rect.width + 1), -1)

    # --- Logica del gioco
    if pressed == K_LEFT and spr_ship.rect.left > 0:
        spr_ship.rect.x -= 5
    elif pressed == K_RIGHT and spr_ship.rect.right < surf_play.get_width():
        spr_ship.rect.x += 5
    for bullet in bullets:
        bullet.rect.y -= 4
        if bullet.rect.bottom < 0:
            bullet.kill()
    for alien in aliens:
        alien.rect.y += 2
        if alien.rect.top > surf_play.get_height():
            sound_end.play()
            done = True
    if pygame.sprite.groupcollide(bullets, aliens, True, True):
        score += 10
        sound_expl.play()
    if pygame.sprite.spritecollide(spr_ship, aliens, True):
            sound_end.play()
            done = True       

    # --- Aggiornamento dello schermo
    surf_play.blit(surf_back, (0, 0))
    all_sprites.draw(surf_play)
    screen.blit(surf_play, PLAY_TOPLEFT)
    surf_info.fill(BACKCOLOR)
    surf_info.blit(fnt.render("Score: " + str(score), True, TEXTCOLOR), (0, 0))
    screen.blit(surf_info, INFO_TOPLEFT)
    pygame.display.flip()

    clk.tick(30)

# --- Uscita
pygame.mixer.music.stop()
while pygame.mixer.get_busy():
    clk.tick(30)
pygame.quit()

Fine della lezione