Nei programmi scritti nella lezione precedente (vedi qui) la velocità effettiva del nostro quadrato in movimento dipende dalla potenza di calcolo del computer: Python esegue continuamente il ciclo principale e ogni volta muove il quadrato, quindi più cicli al secondo = maggiore velocità. Questa situazione è qualcosa di molto rozzo, perchè non ci permette di controllare effettivamente la velocità (che varierebbe da un computer all'altro) ma anche perchè è molto dispendiosa in termini di capacità di calcolo.
In pygame ci sono alcuni oggetti e funzioni per controllare la temporizzazione degli eventi, contenuti nel sottomodulo
time
. Questo è il link alla documentazione:
https://www.pygame.org/docs/ref/time.html.
Per cominciare, soffermiamoci su due funzioni definite nel sottomodulo:
wait(milliseconds) -> time |
Ferma il programma finchè non è passato il tempo indicato (in millisecondi). Durante la pausa rilascia il processore al sistema operativo, in modo che mentre il nostro programma aspetta esso possa occuparsi delle altre applicazioni aperte contemporaneamente. Restituisce il numero di millisecondi effettivamente trascorso. |
delay(milliseconds) -> time |
Stessa cosa. E' più precisa della precedente, ma blocca il processore impedendo al resto del sistema operativo di svolgere le sue funzioni. |
In genere, se non si ha l'esigenza di una temporizzazione precisissima, è meglio usare la prima.
Per comodità vi riscrivo il programma quadrato_mobile.py omettendo molti dei commenti che avevo scritto nella lezione precedente. Potete partire da questo per fare i prossimi esercizi.
import pygame
from pygame.locals import *
pygame.init()
screen = pygame.display.set_mode((800, 600))
surf_square = pygame.Surface((40, 40))
surf_square.fill("yellow")
rect_square = surf_square.get_rect()
vel_square = [1, 1]
# ciclo principale
done = False
while not done:
# sottociclo degli eventi
for ev in pygame.event.get():
if ev.type == QUIT:
done = True
# muoviamo il quadrato
if rect_square.left < 0 or rect_square.right > screen.get_width():
vel_square[0] *= -1
if rect_square.top < 0 or rect_square.bottom > screen.get_height():
vel_square[1] *= -1
rect_square.x += vel_square[0]
rect_square.y += vel_square[1]
screen.fill("green4")
screen.blit(surf_square, rect_square)
pygame.display.flip()
pygame.quit()
pygame.time.wait()
prima di iniziare il ciclo principale
(se la inseriste all'interno del ciclo sarebbe chiamata ad ogni passo, rendendo lentissimo il movimento del quadrato).L'oggetto più usato del modulo è comunque il Clock, un vero orologio che misura i millesimi di secondo.
Questo oggetto ha un metodo tick()
che rappresenta un "battito" dell'orologio. Deve essere chiamato con un argomento che
rappresenta il frame rate (FPS) che si vuole ottenere: ogni volta che viene chiamato calcola il tempo
trascorso dall'ultimo tick()
ed arresta il programma per il tempo necessario ad ottenere la temporizzazione.
Useremo il tick()
come ultima istruzione del ciclo principale: dopo aver
svolto tutti i compiti richiesti Python aspetterà il tempo appropriato prima di ricominciare il ciclo, ottenendo così una
temporizzazione esatta. Vediamo un semplice esempio:
import pygame
from pygame.locals import *
pygame.init()
pygame.display.set_mode((800, 600))
# creiamo un oggetto Clock
clk = pygame.time.Clock()
# ciclo principale
done = False
while not done:
print("Tick!")
for ev in pygame.event.get():
if ev.type == QUIT:
done = True
# aspetta un secondo
clk.tick(1)
pygame.quit()
Questo demplice programmino scriverà Tick!
nella finestra di IDLE una volta al secondo: nella riga
#8 creiamo la variabile clk
(un oggetto Clock) con la funzione costruttore, poi entriamo nel
ciclo principale. Alla fine di esso, nella #19, abbiamo inserito la tick()
:
il nostro Clock aspetterà un secondo e poi farà ripartire il ciclo.
tick()
rappresenta il frame rate
che vogliamo ottenere. Se ora sostituite la #19 con clk.tick(2)
otterrete due tick al secondo,
con clk.tick(0.5)
un tick ogni due secondi e così via. Fate un po' di prove.
Riprendiamo ancora quadrato_mobile.py e modifichiamolo in questo modo:
import pygame
from pygame.locals import *
pygame.init()
screen = pygame.display.set_mode((800, 600))
# creiamo un oggetto Clock
clk = pygame.time.Clock()
surf_square = pygame.Surface((40, 40))
surf_square.fill("yellow")
rect_square = surf_square.get_rect()
vel_square = [1, 1]
# ciclo principale
done = False
while not done:
# sottociclo degli eventi
for ev in pygame.event.get():
if ev.type == QUIT:
done = True
# muoviamo il quadrato
if rect_square.left < 0 or rect_square.right > screen.get_width():
vel_square[0] *= -1
if rect_square.top < 0 or rect_square.bottom > screen.get_height():
vel_square[1] *= -1
rect_square.x += vel_square[0]
rect_square.y += vel_square[1]
screen.fill("green4")
screen.blit(surf_square, rect_square)
pygame.display.flip()
#temporizziamo il ciclo a 30 FPS
clk.tick(30)
pygame.quit()
Sono state aggiunte solo due righe: nella #8 creiamo la variabile clk
(una istanza
dell'oggetto Clock) con la funzione costruttore, nella #37 chiudiamo il ciclo principale con la
tick()
: il nostro Clock aspetterà il tempo necessario ad ottenere un FPS di 30 frames/sec dopodichè
farà ripartire il ciclo.
Il risultato è che il quadrato si muove molto più lentamente, perchè ora Python, arrivato alla fine del ciclo principale, non
ricomincia subito il nuovo ciclo ma aspetta 1/30 di secondo. Per far muovere più velocemente il quadrato possiamo aumentare i FPS,
oppure modificare la variabile vel_square
.
vel_square
mettendo dei numeri più grandi. In questo caso il
quadrato andrà più veloce senza impegnare il processore, però il suo movimento sarà meno fluido, perchè "salterà" alcuni pixel.In genere un frame rate di 30 FPS è un buon compromesso, che garantisce un'esecuzione fluida e non "affatica" il processore. Possiamo aumentare la velocità del quadrato sostituendo la riga #13 con:
vel_square = [2, 2]
Notate che da ora in poi il nostro ciclo principale finirà quasi sempre con le due istruzioni
pygame.display.flip()
(aggiorna lo schermo dopo che abbiamo disegnato tutto quello che volevamo) e
clk.tick(30)
(aspetta finchè non è il momento di iniziare un nuovo ciclo).
Un'altra risorsa importante che si trova nel modulo time
sono i timers. Un timer è un po' come una sveglia: possiamo
impostare quanto tempo deve aspettare, dopodichè aspetterà e poi "scatterà" provocando un evento di sistema del tutto uguale a quelli
che abbiamo già visto. La versione 2 di pygame ha introdotto alcune nuove funzionalità per i timers. Iniziamo
analizzando la definizione della funzione set_timer()
, che serve a far partire un timer:
set_timer(event, millis, loops=0) -> None
.Questo è il significato dei parametri:
Parametro | Tipo di dato | Significato |
---|---|---|
event |
Numero intero oppure oggetto Event | Il tipo di evento che dovrà generare il timer (ne parliamo sotto). |
millis |
Numero intero | Il tempo di attesa in millisecondi. |
loops |
Numero intero | Il numero di volte che il timer scatterà. Lasciando il valore di default 0 il timer continuerà a scattare finchè non verrà
"spento" chiamando nuovamente la set_timer() con lo stesso parametro event e
millis=0 |
Il parametro event
necessita di qualche chiarimento: in genere useremo un numero intero, che risulterà l'attributo
type
dell'evento generato. Potremmo per esempio usare tutte le costanti predefinite di pygame che già conosciamo:
>>> pygame.time.set_timer(MOUSEBUTTONDOWN, 2000, 5)
>>> pygame.time.set_timer(MOUSEMOTION, 1000)
>>> pygame.time.set_timer(MOUSEMOTION, 0)
La prima istruzione genera 5 eventi MOUSEBUTTONDOWN ogni 2 secondi (fittizi, generati dal timer anzichè dal mouse), la seconda un numero infinito di eventi MOUSEMOTION ogni secondo, la terza spegne il timer impostato nella seconda. In ogni caso non è assolutamente una buona idea usare eventi già definiti, perchè per pygame sarebbe difficile capirne l'origine. Ecco un semplice caso in cui questo potrebbe essere utile:
pygame.time.set_timer()
in modo da generare un evento
QUIT esattamente dopo 20 secondi.In pygame esiste l'evento USEREVENT
che è appositamente lasciato libero ed a disposizione
del programmatore, ed è di solito usato per i timer. A partire dalla versione 2 la documentazione di pygame suggerisce
di usare la funzione pygame.event.custom_type()
per generare un numero di evento sicuro (che non coincida con nessuno degli
eventi predefiniti). Se ci servono più timer con eventi diversi potremo usarla più volte, e ci fornirà numeri diversi. Potremo così
assegnare il valore restituito ad una costante (se non l'avete già fatto leggete l'approfondimento) ed usarla
nel nostro ciclo degli eventi alla pari delle costanti predefinite. Ecco un esempio:
import pygame
from pygame.locals import *
pygame.init()
pygame.display.set_mode((800, 600))
# otteniamo un nuovo id evento ed impostiamo un timer
TIMERSHOT = pygame.event.custom_type()
pygame.time.set_timer(TIMERSHOT, 500, 15)
# creiamo una variabile contatore
count = 0
# ciclo principale
done = False
while not done:
for ev in pygame.event.get():
if ev.type == QUIT:
done = True
elif ev.type == TIMERSHOT:
# incrementiamo il contatore e stampiamo un messaggio
count += 1
print("Scatto n.", count)
pygame.quit()
Nella riga #8 assegniamo alla variabile TIMERSHOT
(scritta tutta in maiuscolo per segnalare
che è una costante) il valore restituito dalla custom_type()
, e nella riga successiva impostiamo un timer per generare
tale evento 15 volte a distanza di 500 ms. Abbiamo inserito anche una variabile contatore count
che conta il numero degli
scatti.
In alternativa avremmo potuto sostituire le due righe #8 #9 con
pygame.time.set_timer(USEREVENT, 500, 15)
e monitorare nella #20 l'evento USEREVENT
: questa tecnica è usata nei programmi sviluppati per
le versioni precedenti la 2.
Nei videogiochi è abbastanza comune il caso in cui il giocatore non deve poter prevedere a priori quando accadrà un certo evento
(ad esempio la comparsa di un nuovo nemico). In questi casi si usa il timer insieme al vecchio caro modulo random
.
Ad esempio:
import pygame, random
. . .
TIMERSHOT = pygame.event.custom_type()
next_time = random.randrange(5000, 10001)
pygame.time.set_timer(TIMERSHOT, next_time)
. . .
Nella #4 impostiamo un numero casuale compreso tra 5000 e 10000, e lo passiamo poi alla set_timer()
,
così il timer scatterà casualmente tra 5 e 10 secondi. Avremmo anche potuto scrivere direttamente
set_timer(TIMERSHOT, random.randrange(5000, 10001))
.
blit()
e la flip()
subito dopo aver colorato il quadrato,
perchè ora esse vengono eseguite alla fine di ogni ciclo.Fine della lezione