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:
fill()
, e poi posizioneremo il quadrato nelle sue nuove coordinate con la blit()
.Ecco il programma:
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:
rect_square
con le giuste dimensioni a partire
dalla Surface surf_square
come abbiamo imparato qui. Ripeto che in questo
momento il Rect è posizionato con l'angolo topleft in posizione (0, 0).vel_square
di due numeri: il primo (vel_square[0]
)
è l'incremento della x del quadrato, mentre il secondo (vel_square[1]
) quello della y. In ogni ciclo, quindi, il quadrato
si muoverà in diagonale di un pixel verso destra e di uno verso il basso.vel
.screen
colorandolo di verde
(in #31) e poi copiamo il quadrato su di esso nella nuova posizione (in #34).blit()
è la Surface che rappresenta il
quadrato, mentre il secondo è la posizione (il nostro Rect).pygame.display.flip()
(cioè la funzione che aggiorna lo schermo) è stata trasferita
all'interno del ciclo principale (in #37): in questo modo ogni volta che il quadrato
si muove lo schermo sarà aggiornato. Da ora in poi la flip()
sarà quasi sempre in questa posizione, alla fine
del ciclo principale, dopo aver disegnato tutto il necessario.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.vel_square
di conseguenza);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:
left
e right
se il quadrato urta contro la parete sinistra o destra: in questo
caso dovremo invertire la componente x della velocità: basterà cambiare il suo segno (x positiva: il quadrato si muove da sinistra
a destra, x negativa da destra a sinistra).top
e bottom
se il quadrato urta contro la parete superiore ed inferiore:
agiremo allora sulla y della velocità (y positiva dall'alto verso il basso, y negativa dal basso verso l'alto).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.
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. |
screen
ottenute direttamente con questi metodi.screen
nella #5 e verificate che tutto funziona
regolarmente senza altri interventi.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).Un'altra tecnica che spesso si usa per risolvere questi problemi è quella di sostituire i numeri con costanti, come faccio vedere in questo approfondimento.
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