Una guida per principianti su PyGame

o

Le cose che io ho imparato a tentoni voi non dovete.

o

Come ho imparato a fermare le preoccupazioni e ad amare il blit

Pygame è un wrapper per SDL scritta da Pete Shinners. Ciò significa che, usando pygame, potete scrivere giochi o altre applicazioni multimediali che potranno essere eseguite inalterate su qualsiasi piattaforma supportata da SDL (Windows, Unix, Mac, beOS e altri).

Pygame può essere facile da imparare, ma il mondo della programmazione grafica può essere abbastanza confuso verso i nuovi arrivati. Io scrissi questo per “estrarre” la conoscenza pratica che ho guadagnato negli anni passati lavorando con pygame, e il suo preprocessore, pySDL. Ho provato a classificare questi suggerimenti in ordine di importanza, ma come sarà importante ogni consiglio dipenderà principalmente dal vostro lavoro e dai dettagli del vostro progetto.

Saper programmare bene in Python

La cosa più importante è avere confidenza con Python. Imparare qualcosa di potenzialmente complicato come la programmazione grafica sarà complicato se non avete familiarità col linguaggio che usate.

Scrivete un po’ di programmi non grafici in python, analizza alcuni file di testo, scrivi un gioco a indovinelli o un journal-entry program o qualsiasi cosa. Prendete confidenza con la stringhe e manipolazione di liste, imparate come dividere, slice e combinare stringhe e liste. Sapere come lavora import, provate a scrivere un programma che si muove fra diversi files sorgenti. Scrivete le vostre funzioni, e fate pratica manipolando numeri e caratteri; imparate a convertire fra i due. Arrivate al punto in cui usare liste e dizionari è la vostra seconda natura, non dovrete aver bisogno di sfogliare la documentazione ogni volta che avrete bisogno di ordinare un set di chiavi. Resistete alla tentazione di scrivere a una mailing list, comp.lang.python, o andare sull'IRC ogni volta che siete in dubbio. Invece accedete l'interprete e giocate con il problema per alcune ore. Stampate il riferimento veloce di Python e tenetelo vicino al vostro computer.

Questo potrebbe suonare incredibilmente noioso, ma la sicurezza che acquisterete attraverso la familiarità con Python sarà preziosa quando verrà il tempo di scrivere il vostro gioco. Il tempo che spenderete scrivendo codice Python sarà nulla comparato al tempo che risparmierete quando scriverete codice vero.

Riconoscete di quali parti di Pygame avete realmente bisogno.

Guardando al mucchio di classi nell'Indice della Documentazione di Pygame vi potreste sentire confusi. La cosa importante è capire che potete fare molte cose con un piccolissimo sottoinsieme di funzioni. Molte classi probabilmente non le userete mai – in un anno, io non ho toccatole funzioni: Channel, Joystick, cursor, Userrect, surfarray e version.

Sapere cos'è una superficie.

La parte più importante di Pygame è la superficie. Pensate alla superficie come un foglio di carta vuoto. Potete fare tantissime cose con una superficie – ci potete disegnare sopra linee, riempierne le parti con colori, copiare immagini da e su di essa e selezionare o leggere il colore di ogni singolo pixel. Una superficie può essere di qualsiasi dimensione (ragionevole) e se ne possono avere quante se ne vogliono (ancora, ragionevolmente). Una superficie è speciale – che si può creare con pygame.display.set_mode(). Questa “display surface” rappresenta lo schermo; qualsiasi cosa farete apparirà sullo schermo dell'utente. Però potete averne solo una di queste – è una limitazione di SDL, non una di Pygame.

Allora come si creano le superfici? Come detto sopra, potete creare la “display surface” con pygame.display.set_mode(). Potete creare una superficie che contiene un'immagine utilizzando image.load(), o potete farne una che contiene testo con font.render(). Potete anche creare una superficie vuota con Surface().

La maggior parte delle funzioni per le superfici non sono complicate. Imparate solo blit(), fill(), set_at() e get_at(), e sarete a posto.

Usare surface.convert()

Quando lessi per la prima volta la documentazione per surface.convert(), pensai che non ci fosse nulla di cui preoccuparsi. “Io uso solo .png quindi qualunque cosa che farò sarà nello stesso formato. Quindi non ho bisogno di convert()”. Capii presto di essermi sbagliato.

Il formato a cui convert() si riferisce non è il formato del file (ad esempio png, jpg, gif) ma è quello che è chiamato “pixel format”. Si riferisce al modo particolare in cui una superficie registra colori in un pixel specifico. Se il formato superficie non è lo stesso del formato display, SDL dovrà convertirlo all'istante per ogni blit – un processo piuttosto dispendioso di tempo. Non vi preoccupate troppo della spiegazione; notate solo che convert() è necessario se volete ogni tipo di velocità per i vostri blits.

Come si usa convert? Basta chiamarlo dopo aver creato una superficie con la funzione image.load(). Invece di fare solo così:

surface = pygame.image.load('foo.png')

fate:

surface = pygame.image.load('foo.png').convert()

É semplice. Avete solo bisogno di chiamarlo una volta per superficie, quando caricate un'immagine dal disco. Sarete felici dei risultati; io vedo un aumento di circa 6 volte della velocità di blitting chiamando convert().

Le sole volte che non dovrete usare convert() saranno quando vorrete il controllo assoluto sul formato interno dell'immagine – per esempio quando state scrivendo un programma di conversione delle immagini o simili, e avrete il bisogno di essere sicuri che il file in uscita abbia esattamente lo stesso pixel format del file di input. Se state scrivendo un gioco, avete bisogno di velocità. Usate convert().

Dirty rect animation

La causa più comune di inadeguati fps (frame per second) è il risultato di errori nel comprendere la funzione pygame.display.update(). Con pygame, soltanto disegnare qualcosa sulla “display surface” non produce nessuna apparizione sullo schermo – avete bisogno di pygame.display.update(). Ci sono tre modi per chiamare questa funzione:

  1. pygame.display.update()- Questo causa l'aggiornamento di tutta la finestra (o dell'intero schermo per attività a pieno schermo)

  2. pygame.display.flip() - Questo fa la stessa cosa, e fa pure la cosa giusta se state usando accelerazioni hardware con doppio buffer, che non state usando...

  3. pygame.display.update(un rettangolo o alcune liste di rettangoli)– Questo aggiorna solo l'area rettangolare che avete specificato.

La maggior parte delle persone appena entrate nel mondo della programmazione grafica usano la prima opzione – fanno aggiornare tutto lo schermo ogni frame. Il problema è che è inaccettabilmente lento per molte persone. La funzione update() prende 35 millisecondi sulla mia macchina, che non sembra molto, finchè non si realizza che 1000/35 = 28 fotogrammi per secondo come massimo. E questo succede senza alcuna logica di gioco, nessun blits, nessun input, no intelligenza artificiale, niente. Sono solo seduto qui che guardo lo schermo che si aggiorna, e 28 fps è la mia massima frequenza di aggiornamento.

La soluzione si chiama “dirty rect animation”. Invece di aggiornare l'intero schermo ogni frame, vengono aggiornate solo le parti che sono cambiate dopo l'ultimo aggiornamento. Lo faccio tenendo traccia di questi rettangoli in una lista, poi chiamo update(the_dirty_rectangles) alla fine del frame. In dettaglio per un'immagine in movimento, la procedura è:

  1. fare il blit a un pezzo di sfondo sopra alla corrente posizione dell'immagine, cancellandolo
  2. aggiungere la posizione corrente dell'immagine alla lista chiamata dirty_rects.
  3. Muovere l'immagine
  4. Disegnare l'immagine alla sua nuova locazione
  5. Aggiungere anche questa posizione alla lista dirty_rects
  6. Chiamare la funzione display.update(dirty_rects)

La differenza in velocità è sbalorditiva. Considerate che solawolf ha dozzine di immagini in costante movimento che si aggiornano senza problemi, e ha ancora abbastanza tempo per mostrare un campo di stelle nello sfondo e aggiornare anche quello.

Ci sono due casi in cui questa tecnica non funziona. Il primo è quando l'intera finestra o schermo sono aggiornate ogni frame – pensate a un motore che scorre senza problemi come un gioco di strategia in tempo reale visto dall'alto o un gioco che scorre lateralmente. Cosa fareste in questo caso? Bene, la risposta corta è: “Non scrivete questo tipo di giochi in pygame”. La risposta lunga è: ”Fate scorrere parti di diversi pixel in una volta; non provate a farlo scorrere perfettamente, senza problemi. Il vostro giocatore apprezzerà che il gioco proceda veloce e non si accorgerà troppo che lo sfondo in realtà “salta” per tutto il tempo.

Non c'è niente al punto sei

Le Hardware Surface sono più pericolose di quanto sono utili.

Se avete guardato ai vari argomenti che potete usare con pygame.display.set_mode(), potreste aver pensato una cosa come: “Wow, HWSURFACE! Bene, le voglio! A chi non piace l'accelerazione hardware. Ooo... DOPPIO BUFFERING; bene sembra molto veloce, voglio anche questo!” Non è colpa vostra; siamo stati abituati da anni di giochi 3d a credere che l'accelerazione hardware è buona mentre il rendering software è lento.

Sfortunatamente, il rendering hardware ha una lunga lista di inconvenienti:

L'Hardware Rendering ha il suo posto. Lavora bene sotto Windows, così se non siete interessati a progetti cross-platform, potrebbe darvi un buon aumento di velocità. Comunque, c'è un costo – aumenta mal di testa e difficoltà. È ancora meglio con le vecchie affidabili SWSURFACE finché non sapete cosa state facendo.

Non fatevi distrarre da questioni di non vitale importanza.

Qualche volta, i principianti nel mondo della programmazione dei videogiochi spendono troppo tempo preoccupandosi di problemi che non contribuiscono, in maniera influente, al successo del loro gioco. Il desidero di avere anche i problemi di secondo piano sotto controllo è comprensibile, ma all'inizio del processo di creazione di un gioco, non potete sapere quali sono i problemi importanti, scegliete da soli la risposta che volete. Il risultato può essere tante tergiversazioni senza significato .

Per esempio considerate come organizzare i vostri file grafici. Ogni frame dovrebbe avere il proprio file grafico, o ogni sprite? Forse tutta la grafica dovrebbe essere zippata in un archivio? Un bel po' di tempo del vostro progetto sarebbe sprecato, facendo queste domande su una mailing lista, discutendo la risposta, ecc, ecc. Questo è un problema secondario; ogni minuto perso discutendolo avrebbe dovuto essere dedicato scrivendo il vostro gioco.

Qui è intuitivo capire che è meglio avere una “buona soluzione” che è già implementata, rispetto a una perfetta soluzione che dovrete scrivere voi e probabilmente non ci penserete nemmeno.

I rect sono vostri amici.

Il wrapper di Pete Shinners può avere fantastici effetti alpha e una buona velocità di blit, ma devo ammettere che la mia parte preferita di pygame è la classe Rect. Un Rect è semplicemente un rettangolo definito solo dalla posizione del suo angolo in alto a sinistra, la sua larghezza e la sua altezza. Molte funzioni di pygame prendono rect come argomenti; e prendono anche “rectstyles”, una sequenza che ha gli stessi valori di un rettangolo. Così se necessito di un rettangolo che definisce l'area tra 10, 20 e 40, 50, posso fare:

rect = pygame.Rect(10, 20, 30, 30)

rect = pygame.Rect((10, 20, 30, 30))

rect = pygame.Rect((10, 20), (30, 30))

rect = (10, 20, 30, 30)

rect = ((10, 20, 30, 30))

Se usate una delle prime tre funzioni, avete accesso alle funzioni di Rect, che includono: move, shrink e inflate rect, trovare l'unione di due rettangoli, e una varietà di funzioni per trovare le collisioni.

Per esempio, supponete di voler ottenere la lista di tutte le immagini (sprite) che contengono un punto (x, y) – forse che il giocatore ha cliccato, o forse la corrente posizione di un proiettile. È semplice, se ogni sprite ha un membro .rect – basta fare semplicemente:

sprites_clicked = [sprite for sprite in all_my_sprite_list if sprite.rect.collidepoint(x, y)]

I rects non hanno altre relazioni a superfici o funzioni grafiche, oltre che poterli usare come argomenti. Potete anche usarli in campi che non hanno niente a che fare con la grafica, ma avete comunque bisogno di definire rettangoli. Ogni progetto scopro alcuni nuovi modi per usare rects anche se non avrei mai pensato di usarli in quella maniera.

Non vi preoccupate di fare in modo che le collisioni siano trovate con precisione del pixel.

Avete degli sprite in movimento, e avete bisogno di sapere se stanno o no rimbalzando uno contro l'altro. Potete essere tentati di scrivere qualcosa tipo:

  1. Controlla se i rect sono in collisione. Se non lo sono ignorali.
  2. Per ogni pixel nell'area sovrapposta, controlla se i pixel corrispondenti da entrambi gli sprite sono opachi. Se si c'è una collisione.

Ci sono molti altri modi di fare questo, con ANDing sprite masks e così via, ma in qualunque modo lo farete in pygame, il risultato probabilmente sarà lento. Per la maggior parte dei giochi, è meglio fare una “sub-rect collision” - creare cioè un rect un po' più piccolo per ogni sprite, e usare questo per la collisione. Sarà molto più veloce, e nella maggior parte dei casi il giocatore non si accorgerà dell'imprecisione.

Gestire il subsistema degli eventi

Il sistema degli eventi di pygame è complicato. Ci sono due modi differenti per vedere cosa sta facendo un dispositvo di input (tastiera, mouse o joystick). Il primo consiste nel controllare direttamente lo stato del dispositivo. Si può fare tramite pygame.mouse.get_pos() o pygame.key.get_pressed(). Queste due funzioni diranno lo stato del dispositivo di input al momento in cui chiamate le chiamate.

Il secondo metodo è di usare la coda (queue) degli eventi SDL. Questa queue è una lista di eventi – che sono aggiunti alla lista quando sono trovati e rimossi quando l'evento termina.

Per ogni metodo ci sono vantaggi e svantaggi. Il controllo di stato (sistema 1) dà precisione – sapete perfettamente quando un certo input è stato inserito – se mouse.get_pressed([0]) dà 1, vuol dire che, in questo momento, il tasto sinistro del mouse è premuto. Invece la cosa di eventi dice che il tasto era premuto nel passato; se controllate la queue abbastanza spesso, può andare bene, ma se lo ritardate per controllare altre code, il tempo di latenza cresce. Un altro vantaggio della funzione che controlla lo stato è che trova “accordi” facilmente, cioè diversi stati nello stesso momento. Se volete sapere se i tasti t e f sono premuti allo stesso istante, basta scrivere

   1 if (key.get_pressed[K_t] and key.get_pressed[K_f]):
   2     print "Yup!"

Nel sistema a coda ogni tasto arriva nella queue come un evento completamente separato, così dovrete ricordare che il tasto t è premuto, e non è ancora entrato in coda, perchè sta controllando il tasto f. Un po' più complicato.

Il primo sistema comunque ha una grande debolezza. Riporta solo lo stato del dispositivo al momento in cui chiamate la funzione; se l'utente preme un bottone del mouse e poi lo rilascia appena prima che venga chiamata mouse.get_pressed(), ritornerà 0 – get_pressed() ha perso completamente il bottone. I due eventi, MOUSEBUTTONDOWN e MOUSEBUTTONUP, invece staranno nella coda, aspettando di essere processati.

La lezione è: scegliete il sistema che è più appropriato per le vostre necessità. Se non avete molto che funziona in un loop – nel senso che state usando un loop “while 1” aspettando gli input, usate get_pressed() o un'altra funzione di stato; la latenza sarà bassa. Nell'altro caso, se ogni tasto premuto è cruciale ma la latenza non così tanto – come l'utente che scrive qualcosa in un box, usa l'evento queue. Qualche tasto premuto potrebbe essere in ritardo, ma li otterrete tutti.

Una nota su event.poll() contro wait() - poll() può sembrare meglio, perché non blocca il programma se non fa nulla, aspettando gli input – wait() invece mette in sospensione il programma finché non riceve un input. Comunque poll() consumerà il 100% della cpu disponibile e riempirà la queue degli eventi con NOEVENTS. Usate set_blocked() per selezionare solo i tipi di eventi che vi interessano – la vostra queue sarà molto più maneggevole.

Colorkey contro Alpha

C'è confusione circa queste due tecniche, e molto dipende dalla terminologia utilizzata.

“Colorkey blitting” riguarda il dire a pygame che tutti i pixel di un certo colore in una certa immagine sono trasparenti invece del qualsiasi colore che dovrebbero essere. Questi pixel trasparenti non sono blittati quando il resto dell'immagine lo è, e quindi non oscurano lo sfondo. Così è come vengono fatti gli sprite che non sono una forma rettangolare. Semplicemente chiamate surface.set_colorkey(color), dove color è una tupla RGB, come (0,0,0). Questa tupla fa in modo che ogni pixel nell'immagine iniziale diventi trasparente invece che nero.

“Alpha” è differente, ed ha 2 flavors. “Image alpha” si applica all'intera immagine, e probabilmente è quello che volete. Propriamente noto come “translucency”, alpha fa in modo che ogni pixel dell'immagine iniziale diventi parzialmente opaco. Per esempio, se settate una superficie alpha a 192 e poi la blittate su un background, ¾ del colore di ogni pixel sarà quello dell'immagine originale, mentre ¼ sarà quello dello sfondo. Alpha si misura da 0 a 255, 0 è completamente trasparente e 255 è completamente opaco. Notate che il blit di Colorkey e di Alpha può essere combinato – produce cioè un immagine che è completamente trasparente in alcuni punti e semi-trasparente in altri.

L'altro flavor di alpha si chiama “per-pixel alpha” ed è molto più complicato. Essenzialmente, ogni pixel nell'immagine originale ha il proprio valore alpha, da 0 a 255. Ogni pixel, quindi, può avere diversa opacità quando blittato in uno sfondo. Questo tipo di alpha non può essere combinato con colorkey blitting, e sovrasta l'effetto di per-image alpha. Per-pixel alpha viene raramente usato nei videogiochi, e per usarlo si deve salvare l'immagine di partenza in un editor grafico con un canale speciale, il canale alpha. È complicato, non usatelo ora.

Fate le cose nel modo di python.

Una nota finale (non la meno importante, solo l'ultima in elenco). Pygame è un buon wrapper, poco pesante, delle SDL, che sono un buon e leggero wrapper, per le chiamate grafiche native del vostro Sistema Operativo

È possibile che se il vostro codice è ancora lento nonostante abbiate fatto tutte le cose dette sopra, il problema sia nel modo in cui vi riferite ai vostri dati in python. Certi modi di scrivere codice saranno comunuqe lenti, non importa quello che farete. Fortunatamente il python è un linguaggio molto pulito – se un pezzo di codice sembra goffo e brutto, modificandolo la sua velocità aumenterà di certo. Leggete Python Performance Tips per aver qualche utile trucco su come aumentare l'efficienza del codice. Però la prematura ottimizzazione del codice è “l'inizio del male”; se non è veloce a sufficienza, non torturate il codice provando di farlo più veloce. Alcune cose non sono adatte :)

Ecco a voi. Adesso sapete praticamente tutto quello che so io su pygame. Adesso, forza scrivete il vostro gioco!


PyGameTutorial/GuidaNiubbi (last edited 2007-08-31 17:14:36 by Flame_Alchemist)