13: WORKSHOP - PONG

UN SEMPLICE PONG

Cerchiamo ora di applicare tutto quello che abbiamo imparato in modo da ottenere qualche programma più completo. In questa lezione realizzeremo il classico effetto della pallina e della racchetta. Potete partire dallo "scheletro" di programma che ho preparato qui sotto: l'ho diviso in sezioni logiche mediante i commenti in modo da dare un ordine a quello che dobbiammo scrivere.

NUOVO PROGRAMMA: pong.py

Aprite un nuovo file nel vostro editor e copiate ed incollate il programma qui sotto.


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

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

# --- Risorse grafiche

# --- Risorse sonore

# --- Altre variabili

# --- Ciclo principale 
done = False
while not done:

    # --- Ciclo degli eventi
    for ev in pygame.event.get():
        if ev.type == pygame.QUIT:
            done = True
            
    # --- Logica del gioco
    
    # --- Aggiornamento dello schermo     
    pygame.display.flip()

    clk.tick(30)

# --- Uscita
pygame.quit()

LE RISORSE

Per prima cosa modifichiamo la caption della nostra finestra in "Pong". Carichiamo poi le risorse grafiche e sonore.

ESERCIZIO 13.1: Per realizzare il programma ci servono due immagini: la pallina e la racchetta. Nella sezione Risorse grafiche caricate i files "ball_blue.png" e "bar2_blue.png" che si trovano nella cartella Sprites/Pong e assegnateli alle variabii surf_ball e surf_bar (ricordo che con i file .png conviene usare la convert_alpha() perchè potrebbero esserci problemi con la trasparenza). Dato che dovremo muoverle e verificarne le collisioni, ricaviamo da esse i rispettivi Rect, assegnandoli alle variabili rect_ball e rect_bar.
ESERCIZIO 13.2: Per quanto riguarda i suoni, avremo bisogno di un suono per il rimbalzo della pallina sulle pareti e di un altro per il rimbalzo sulla racchetta. Nella sezione Risorse sonore caricate i due files sonori "beep_8.wav" e "hit_1.wav" (nella cartella Effects/SFX) assegnandoli alle variabili sound_bounce e sound_hit.

Potete ora lanciare il programma, almeno per sincerarvi di non aver commesso errori (state attenti ad indicare correttamente i percorsi dei files o otterrete un errore Couldn't open file). Ora passiamo a disegnare le nostre immagini.

ESERCIZIO 13.3: Sempre nella sezione Risorse grafiche posizionate il rettangolo della pallina con il topleft nel punto (300, 200); fate lo stesso per la racchetta nel punto (300, 560)
Ora disegniamo le figure nella sezione Aggiornamento dello schermo: coloriamo screen con il colore (60, 160, 60) (un verde chiaro) e facciamo il blit() della pallina e della racchetta su di esso.

Se tutto funziona dovremmo vedere la pallina e la racchetta. La pallina però è decisamente troppo piccola rispetto alla racchetta. Cerchiamo di rimediare:

ESERCIZIO 13.4: Possiamo facilmente raddoppiare le dimensioni della pallina con la funzione scale2x() del sottomodulo transform: vedi qui.
Chiaramente questa modifica va fatta prima di ottenere dalla Surface il corrispondente Rect, altrimenti il Rect non avrà le dimensioni giuste.

Dopo queste modifiche il programma dovrebbe essere più o meno così (naturalmente è possibile che la posizione di qualche riga sia diversa). Lanciandolo si dovrebbero vedere la pallina e la racchetta fermi.


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

# --- Inizializzazione
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Pong")
clk = pygame.time.Clock()

# --- Risorse grafiche
surf_ball = pygame.image.load(join("Sprites", "Pong", "ball_blue.png")).convert_alpha()
surf_ball = pygame.transform.scale2x(surf_ball)
rect_ball = surf_ball.get_rect()
rect_ball.topleft = (300, 200)
surf_bar = pygame.image.load(join("Sprites", "Pong", "bar2_blue.png")).convert_alpha()
rect_bar = surf_bar.get_rect()
rect_bar.topleft = (300, 560)

# --- Risorse sonore
sound_bounce = pygame.mixer.Sound(join("Effects", "SFX", "beep_8.wav"))
sound_hit = pygame.mixer.Sound(join("Effects", "SFX", "hit_1.wav"))

# --- Altre variabili

# --- Ciclo principale 
done = False
while not done:
    
    # --- Ciclo degli eventi
    for ev in pygame.event.get():
        if ev.type == QUIT:
            done = True
            
    # --- Logica del gioco
    
    # --- Aggiornamento dello schermo
    screen.fill((60, 160, 60))
    screen.blit(surf_ball, rect_ball)
    screen.blit(surf_bar, rect_bar)   
    pygame.display.flip()

    clk.tick(30)

# --- Uscita
pygame.quit()

IL MOVIMENTO

Muovere la pallina non dovrebbe essere molto difficile, in quanto abbiamo già visto come fare nelle lezioni precedenti. Potete partire dal programma panda.py che potete rivedere qui.

ESERCIZIO 13.5: Realizziamo il movimento della pallina Notate che, provvisoriamente, facciamo rimbalzare la pallina anche sul lato inferiore, ma poi dovremo cambiare questo comportamento (se la pallina arriverà al lato inferiore avremo perso la partita).

Ora vogliamo muovere la racchetta con i due tasti freccia a destra e a sinistra. Una prima semplice idea potrebbe essere questa: ci basta monitorare l'evento KEYDOWN; quando accade verifichiamo tramite l'attributo key dell'Event se è stata premuta la freccia a sinistra (K_LEFT) o a destra (K_RIGHT). Nel primo caso spostiamo la racchetta 4 pixel a sinistra, nel secondo a destra.

ESERCIZIO 13.6: Modificate la sezione Ciclo degli eventi nel modo indicato . Se non ricordate bene come fare guardate qui per ripassare l'uso dell'attributo key.

Se non avete fatto errori la racchetta si dovrebbe muovere, ma la giocabilità è piuttosto scarsa: dobbiamo continuamente premere e rialzare i tasti freccia, facciamo molta fatica e la racchetta si muove lentamente. Sarebbe meglio se premendo un tasto la racchetta iniziasse a muoversi, e si fermasse solo quando lo rialziamo.

Ottenere questo comportamento è un po' più difficile. Consideriamo che pygame ci mette a disposizione due eventi: KEYDOWN e KEYUP. In altre parole riconosce quando un tasto si abbassa e quando si alza, ma nell'intervallo tra il KEYDOWN e il KEYUP non riceve nessun evento: quindi il programma non sa, in un certo momento, quali tasti sono abbassati, e dovrà "ricordare" mediante una variabile se in quel momento qualche tasto è premuto.

Dobbiamo perciò dividere il nostro compito in due parti:

  1. Nel ciclo degli eventi il programma controlla se è stato premuto o rilasciato un tasto, ed in base a ciò assegna un valore ad una variabile (che ricorderà il tasto premuto in quel momento)
  2. Dopo il ciclo degli eventi muoviamo la racchetta in base al valore della variabile
ESERCIZIO 13.7: Realizziamo il movimento della racchetta

Il ciclo degli eventi che abbiamo scritto va analizzato attentamente (i numeri di riga si riferiscono allo schema precedente, se avete aggiunto delle righe i vostri dovrebbero essere diversi):

Alla fine del ciclo degli eventi pressed conterrà quindi K_LEFT o K_RIGHT (se è premuto un tasto freccia) oppure None (non è premuto nessun tasto), e quindi il nostro programma ricorderà in ogni momento quale tasto è premuto. Potremo quindi muovere (dopo il ciclo, nella sezione Logica del gioco) la racchetta come sappiamo già fare (aumentate/diminuite la x del suo Rect di 4 pixel)

Se non avete commesso errori la racchetta dovrebbe ora muoversi nel modo che noi vogliamo. Qualcuno potrebbe pensare che nella #36 basterebbe scrivere elif event.type == KEYUP: (cioè basterebbe controllare se un tasto è stato rilasciato). Questo funziona finchè premiamo un tasto per volta, ma nei videogiochi si premono spesso più tasti insieme: provate a modificare la #36 come vi ho detto, premete più tasti insieme e guardate cosa succede.

Il programma a questo punto dovrebbe essere più o meno così (vi mostro solo la parte modificata):


.   .   .
# --- Altre variabili
vel_ball = [3, 3]
pressed = None

# --- 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
            
    # --- Logica del gioco
    if rect_ball.left < 0 or rect_ball.right > screen.get_width():
        vel_ball[0] *= -1
        sound_bounce.play()
    if rect_ball.top < 0 or rect_ball.bottom > screen.get_height():
        vel_ball[1] *= -1    
        sound_bounce.play()
    rect_ball.x += vel_ball[0]
    rect_ball.y += vel_ball[1]
    if pressed == K_LEFT:
        rect_bar.x -= 4
    elif pressed == K_RIGHT:
        rect_bar.x += 4
.  .  .
ESERCIZIO 13.8: C'è ancora qualcosa da sistemare! La nostra racchetta può tranquillamente uscire dallo schermo a destra e sinistra e dobbiamo modificare le righe #50 e #52. Basta aggiungere con un and una ulteriore condizione: nella #50 dobbiamo spostare a sinistra la racchetta se pressed == K_LEFT e la x del lato sinistro del suo rect è maggiore di 0. Modificate in maniera analoga la #52.

IL RIMBALZO

Infine modifichiamo ancora la sezione Logica del gioco in modo da far rimbalzare la pallina sulla racchetta.

ESERCIZIO 13.9: Eliminate il rimbalzo della pallina sul fondo dell'area di gioco: dovete trovare la riga che controlla se la pallina sta rimbalzando in alto o in basso e dividere l'if in due blocchi distinti (eliminando l'or).

Lanciate il programma: la pallina dovrebbe tristemente uscire quasi subito dalla parte in basso: dobbiamo quindi aggiungere subito il rimbalzo sulla racchetta. Ma questo è molto facile se usiamo il metodo colliderect() (vedi qui) dell'oggetto Rect. Basta verificare con un if se i due Rect rect_bar e rect_ball collidono. Ricordate che dovete usare la sintassi per gli oggetti: uno dei due Rect va scritto a sinistra del punto, mentre l'altro va scritto come argomento della funzione.

ESERCIZIO 13.10: Modificate la sezione Logica del gioco in modo da implementare il rimbalzo della pallina sulla racchetta (per rimbalzare dovrete invertire la componente verticale della velocità della pallina). Se avviene il rimbalzo fate anche partire il suono sound_hit.

Adesso la pallina dovrebbe rimbalzare, dando finalmente un senso compiuto ai nostri sforzi: c'è però ancora un piccolo baco nel nostro programma, una situazione che non avevamo previsto. Provate a colpire la pallina lateralmente (cioè a farla passare e poi muovere la racchetta sopra di essa mentre è ancora in campo): cosa succede? I due Rect si sovrappongono e quindi la velocità viene invertita. Ma nel ciclo successivo i due Rect saranno ancora sovrapposti e quindi si innescherà una serie di rimbalzi su e giù che noi non vogliamo. Una soluzione piuttosto semplice, simile a quella che abbiamo già visto qui, è di far rimbalzare la pallina solo se i due Rect collidono e la sua velocità verticale è positiva (cioè sta andando verso il basso).

ESERCIZIO 13.11: Eseguite quest'ultima modifica: si tratta solo di aggiungere con un and un'ulteriore condizione all'if che controlla se la pallina deve rimbalzare sulla racchetta. SOLUZIONI

A questo punto abbiamo un programma molto semplice ma funzionante (ecco qui la mia versione finale). Per il momento ci fermiamo qui, vi do comunque qualche suggerimento nel caso vgliate sperimentare un po' per conto vostro:

  1. Provate ad aggiungere una colonna sonora durante il gioco, come abbiamo visto nella Lezione 12.
  2. Inserite, tra la fine del ciclo principale e la chiusura del programma con la pygame.quit(), una scritta "GAME OVER" usando qualcuno degli effetti che abbiamo visto qui (caricate uno dei files che trovate nella cartella Sprites, chiamate la funzione con i parametri appropriati ed inserite poi 2 secondi di pausa prima di chiudere il programma).

Fine della lezione