9: TEMPORIZZIAMO

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()
Nuovo programma: quadrato_mobile1.py
ESERCIZIO 9.1: Salvate il programma precedente con il nuovo nome e modificatelo in modo che il quadrato inizi a muoversi esattamente dopo 3 secondi. Usate la 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).
SOLUZIONI

L'OGGETTO Clock

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:

NUOVO PROGRAMMA: prova_clock.py

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.

ESERCIZIO 9.2: ATTENZIONE! Vi ripeto che il parametro della 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.

ESERCIZIO 9.3: provate a modificare il parametro della riga #37: che cosa succede? In particolare potreste pensare che impostando un numero molto alto il quadrato andrà sempre più veloce, ma questo non succederà: ad un certo punto arriveremo alla massima capacità di calcolo del nostro computer, ed il processore "non ce la farà" ad andare più veloce.
ESERCIZIO 9.4: provate invece a modificare la lista 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).

I TIMERS

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:

ESERCIZIO 9.5: Partendo dal programma precedente, scrivete il programma quadrato_mobile2.py nel quale l'animazione dura esattamente 20 secondi: dovete usare la pygame.time.set_timer() in modo da generare un evento QUIT esattamente dopo 20 secondi.
Naturalmente il timer deve essere innescato prima di entrare nel ciclo principale.
SOLUZIONI

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:

NUOVO PROGRAMMA: prova_timer.py

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)).

ESERCIZIO 9.6: Sempre partendo da quadrato_mobile.py scrivete il programma quadrato_mobile3.py nel quale il quadrato cambia casualmente colore dopo un periodo di tempo variabile tra 1 e 5 secondi. Seguite questi passi:
  1. Prima di iniziare il ciclo principale dovete caricare il timer in modo che generi un evento TIMERSHOT dopo un periodo casuale (come abbiamo visto sopra).
  2. Nel sottociclo degli eventi monitorate l'evento TIMERSHOT, e quando accade fate riferimento alla tecnica che abbiamo già usato qui nel programma click_quadrati.py
  3. ATTENZIONE! Non è più necessario eseguire la blit() e la flip() subito dopo aver colorato il quadrato, perchè ora esse vengono eseguite alla fine di ogni ciclo.
ESERCIZIO 9.7: Se lanciamo più volte il programma precedente noteremo che il quadrato cambierà colore dopo un periodo di tempo casuale, ma poi questo periodo si ripeterà sempre uguale. Se vogliamo che ogni cambio di colore avvenga dopo un tempo casuale dovremo "ricaricare" il timer con un numero casuale ogni volta che scatta. Serviranno quindi due istruzioni per caricare il timer: la prima è quella iniziale che avete già scritto prima di entrare nel ciclo principale; la seconda (del tutto uguale, potete copiarla e incollarla) va messa nel ciclo principale subito dopo che è accaduto il TIMERSHOT, in modo che il timer sia ricaricato ogni volta con un nuovo intervallo casuale.
SOLUZIONI

Fine della lezione