8: COME MUOVERE UNA Surface

MUOVIAMO UN QUADRATO

E' arrivato il momento di imparare come si fa a muovere un'immagine sullo schermo. Penso che tutti voi sappiate che l'illusione del movimento si ottiene al cinema o in un videogioco attraverso una serie di rapidi cambiamenti dello schermo. Ogni "schermata" è detta in inglese frame: se le schermate si susseguono ad una velocità maggiore di circa 20 frames al secondo il nostro occhio non le percepisce più singolarmente ma le "unisce" ed abbiamo la percezione del movimento. La velocità con cui cambiano le schermate è detta in inglese frame rate e si misura in FPS (frames per second).

La nostra prima animazione sarà molto semplice: muoveremo un quadrato giallo all'interno della nostra finestra. Ricorderete che nei nostri primi programmi avevamo disegnato una Surface con il metodo blit(), aggiornando poi lo schermo con la flip() Per muovere la Surface basta spostare queste istruzioni all'interno del ciclo principale, così essa sarà ridisegnata molte volte al secondo. Se ogni volta spostiamo di poco la sua posizione avremo la percezione del movimento.

Useremo la tecnica che abbiamo imparato nella Lezione 5: bisognerà creare un oggetto Surface, associargli un Rect e muovere il Rect cambiandone le coordinate ad ogni ciclo. Il nostro ciclo principale diventerà ora più articolato:

Ecco il programma:

NUOVO PROGRAMMA: quadrato_mobile.py

import pygame
from pygame.locals import *

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

# creiamo il quadrato (una Surface 40x40) e lo coloriamo di giallo
surf_square = pygame.Surface((40, 40))
surf_square.fill("yellow")

# otteniamo dalla Surface square il corrispondente Rect
rect_square = surf_square.get_rect()

# questa e' la velocita' del quadrato: una lista di due elementi
# (cioe' l'incremento delle sue coordinate x e y ad ogni ciclo)
vel_square = [1, 1]

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

    # muoviamo il quadrato: prima aggiorniamo la posizione del Rect    
    rect_square.x += vel_square[0]    
    rect_square.y += vel_square[1]
	
    # poi cancelliamo lo schermo colorandolo di verde
    screen.fill("green4")

    # infine disegniamo il quadrato nella nuova posizione    
    screen.blit(surf_square, rect_square)
	
    # aggiorniamo lo schermo
    pygame.display.flip()
	
pygame.quit()

Dovreste vedere un quadrato giallo che si muove in diagonale su uno sfondo verde ed esce rapidamente dallo schermo (probabilmente la velocità sarà eccessiva e l'animazione piuttosto scadente, ma non preoccupatevi, impareremo a regolarle nella prossima lezione). Facciamo qualche osservazione:

ESERCIZIO 8.1: Per capire a cosa serve la riga #31 provate a commentarla, inserendo un # all'inizio, e guardate cosa succede.
ESERCIZIO 8.2: Fate partire il quadrato da un'altra posizione. E' necessario aggiungere una riga di codice dopo la #12, assegnando un valore ad uno degli attributi di square_rect come abbiamo visto nella Lezione 5. Ad esempio fatelo partire dal centro dello schermo, dalla metà del lato sinistro o dalla metà del lato alto.
SUGGERIMENTO: Per indicare la posizione di partenza del quadrato potete usare delle duple di interi, tenendo presente che conoscete le dimensioni dello schermo. Discuteremo meglio la cosa nell'ESERCIZIO 8.5 più avanti.
ESERCIZIO 8.3: Cambiate anche la velocità del quadrato, facendolo muovere in altre direzioni. Ad esempio: Se non avete le idee ben chiare sulle coordinate fate sempre riferimento qui.
SOLUZIONI

Il QUADRATO RIMBALZA

La situazione è comunque ancora piuttosto frustrante: il nostro quadrato parte ed esce rapidamente dallo schermo, dopodichè non ci rimane che chiudere mestamente la finestra e ricominciare. Sarebbe molto meglio se il quadrato rimbalzasse sulle pareti, rimanendo dentro il nostro schermo.

Questo diventa molto semplice se utilizziamo gli attributi top, bottom, left, right del Rect associato al quadrato: basterà confrontarli con le dimensioni dela nostra finestra principale screen per capire se il quadrato sta "urtando" contro uno dei lati dello schermo. Se ciò avviene ci basterà invertire una delle componenti della sua velocità (cioè la lista vel). In particolare:

Una volta fatte queste correzioni, procederemo ad aggiornare la posizione del quadrato come abbiamo fatto nel programma precedente. Ecco il programma modificato:


import pygame
from pygame.locals import *

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

# creiamo il quadrato (una Surface 40x40) e lo coloriamo di giallo
surf_square = pygame.Surface((40, 40))
surf_square.fill("yellow")

# otteniamo dalla Surface square il corrispondente Rect
rect_square = surf_square.get_rect()

# questa e' la velocita' del quadrato: una lista di due elementi
# (cioe' l'incremento delle sue coordinate x e y ad ogni ciclo)
vel_square = [1, 1]

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

    # controlliamo se il quadrato sta per uscire a sinistra o destra
    if rect_square.left < 0 or rect_square.right > 800:
        # se si' invertiamo la componente x (moltiplicandola per -1)
        vel_square[0] *= -1
		
    # controlliamo se il quadrato sta per uscire in alto o in basso
    if rect_square.top < 0 or rect_square.bottom > 600:
        # se si' invertiamo la componente y (moltiplicandola per -1)
        vel_square[1] *= -1
   
    # muoviamo il quadrato: prima aggiorniamo la posizione del Rect
    rect_square.x += vel_square[0]
    rect_square.y += vel_square[1]
	
    # poi cancelliamo lo schermo colorandolo di verde
    screen.fill("green4")
	
    # infine disegniamo il quadrato nella nuova posizione
    screen.blit(surf_square, rect_square)
	
    # aggiorniamo lo schermo
    pygame.display.flip()
	
pygame.quit()

Abbiamo aggiunto le righe da #26 a #34: nella #27 controlliamo se il quadrato sta per uscire a destra o sinistra (se sì invertiamo la componente x della velocità), mentre in #32 controlliamo se sta per uscire in alto o in basso.

Nella #27 ci si potrebbe chiedere perchè non ci sia scritto square_rect.left <= 0 oppure square_left == 0 (e similmente nella #32). La cosa è piuttosto complessa da spiegare ma se provate a cambiare le istruzioni in questo modo vedrete che il programma non funzionerà (riuscite a capire perchè?). Quindi ricordate di verificare le collisioni contro i lati della finestra in questo modo.

ABBASSO I NUMERI!

C'è ancora un'osservazione da fare sulle righe #27 e #32. Provate a cambiare le dimensioni della finestra screen, cambiando i parametri della pygame.display.set_mode() nella #5: cosa succede?

E' semplice, i numeri 800 e 600 delle due righe devono essere uguali alla larghezza ed altezza della finestra che impostate nella set_mode(), quindi se modificate la riga #5 dovete ricordarvi di aggiornare anche la #27 e #32. Se il vostro programma fosse costituito da migliaia di righe, ogni volta che modificate qualcosa dovreste andare a caccia di tutti gli "effetti collaterali" della vostra modifica e correggerli.

Questo è uno dei motivi per cui i programmatori odiano usare i numeri nei programmi, e preferiscono sempre sostituirli con funzioni e variabili, in modo da rendere le modifiche automatiche.

Per rendere sicuro il programma da questo punto di vista potreste usare la screen.get_rect() che vi fornisce un Rect con le dimensioni della finestra (come abbiamo già visto nelle lezioni precedenti), tuttavia esistono degli altri metodi dell'oggetto Surface che risolvono la cosa più rapidamente:

get_width() restituisce la larghezza in pixel della Surface alla quale è applicato.
get_height() restituisce l'altezza in pixel della Surface alla quale è applicato.
get_size() restituisce in una duple la larghezza e l'altezza della Surface.
ESERCIZIO 8.4: Nelle due righe #27 e #32, al posto dei numeri 800 e 600 sostituite la larghezza e l'altezza della Surface screen ottenute direttamente con questi metodi.
Poi provate a modificare le dimensioni di screen nella #5 e verificate che tutto funziona regolarmente senza altri interventi.
ESERCIZIO 8.5: Anche l'ESERCIZIO 8.2 ha gli stessi problemi: cambiando le dimensioni della finestra bisogna cambiare anche il punto di partenza del quadrato giallo. Rifatelo senza usare numeri per posizionare il quadrato: ricavate il Rect corrispondente a tutta la nostra finestra con rect_screen = screen.get_rect() ed usate poi gli attributi dei due Rect per posizionare il quadrato (vedi qui ed in particolare l'ESERCIZIO 5.4).
Provate poi a cambiare le dimensioni della finestra e verificate che il programma risponde correttamente.
SOLUZIONI

Un'altra tecnica che spesso si usa per risolvere questi problemi è quella di sostituire i numeri con costanti, come faccio vedere in questo approfondimento.

UN PO' DI MATEMATICA (E FISICA)

Concludo con un'osservazione dedicata a chi ha studiato (o sta studiando) Fisica: la lista vel_square usata nel nostro programma è a tutti gli effetti un vettore: vel_square[0] è la componente x del vettore e vel_square[1] la componente y (quelle che in fisica chiameremmo vx e vy), calcolate in "pixel per ciclo". Quando calcoliamo la nuova posizione stiamo facendo un'addizione vettoriale (componente per componente) tra la posizione del quadrato e lo spazio percorso in un ciclo (che corrisponde numericamente alla sua velocità). Infine, per invertire una direzione ci basta cambiare segno alla rispettiva componente. In pratica stiamo realizzando un moto rettilineo uniforme (finchè il quadrato non urta una delle pareti) sommando ad ogni ciclo la stessa quantità di pixel alla posizione del quadrato. Uno dei sottomoduli di pygame, pygame.math, definisce alcuni oggetti e funzioni per eseguire operazioni tra vettori (in questo tutorial introduttivo, però, non ne parleremo).

Fine della lezione