Introducció als jocs amb pygame¶
Conceptes bàsics: jocs i ús de la biblioteca pygame¶
La biblioteca de subprogrames pygame està pensada per facilitar el desenvolupament de jocs per computador. La biblioteca presenta un conjunt molt extens de funcionalitats i possibilitats repartits entre diversos mòduls, entre ells:
El game-loop o bucle principal d’un joc¶
La majoria dels jocs tenen una estructura com la mostrada a continuació: en primer lloc es fan les inicialitzacions pertinents (creació de la finestra gràfica, dels personatges, inicialització de la puntuació, etc.) i després hi ha una composició iterativa principal anomenada game-loop de la qual se’n surt quan s’acaba el joc, bé perquè l’usuari ha guanyat la partida, bé perquè l’ha perduda.
def joc:
<inicialitza el joc>
while not <final del joc>:
<sondeja i tracta esdeveniments>
<actualitza els elements del joc>
<dibuixa una nova imatge>
<mostra la nova imatge>
<tanca el joc>
En aquest esquema, inicialitza el joc és un seguit d’operacions com
per exemple crear una finestra per visualitzar el joc
(pygame.display.set_mode()
) o preparar i inicialitzar els
continguts del joc necessaris per a entrar-hi.
La iteració principal del joc ha d’estar atenta periòdicament a la
interacció de l’usuari en el joc (el que s’anomenen esdeveniments). Per tant,
sondeja i tracta esdeveniments enquesta al sistema per si han passat
esdeveniments (prémer tecles, botons de la interfície gràfica, botons
o moviments del del ratolí, etc) i en el cas que així hagi estat
actuar en conseqüència. La funció pygame.event.get()
retorna
una llista d’esdeveniments.
La segona part de la iteració principal del joc té a veure amb la representació (gràfica) de l’evolució del joc d’acord al guió, regles del joc i interacció de l’usuari. Típicament actualitza els elements del joc haurà de tenir present:
L’escenari o fons (paisatge, castell, etc) on passa el joc. Pot ser fixe o variar d’acord al guió o regles del joc que es vol dissenyar. La classe
pygame.Surface
i el mòdulpygame.draw
seran útils per a representar el fons.Els objectes gràfics mòbils i canviants (jugadors, naus, bombes, etc) que es mouen respecte l’escenari i responen a unes regles determinades pel guió o regles del joc. La seva posició i/o forma gràfica es canvia per tal de representar-la en la següent imatge que es genera. Tradicionalment, a un objecte gràfic mòbil i canviant se l’anomena sprite (literalment, follet). Vegeu el mòdul
pygame.sprite
.
Els sprites es poden crear a partir d’elements geomètrics que es
dibuixen sobre una imatge (pygame.Surface
) o a partir
d’imatges prèviament creades. Per a dibuixar elements geomètrics en
una imatge disposeu de les funcions pygame.draw.line()
,
pygame.draw.polygon()
, pygame.draw.circle()
,
pygame.draw.ellipse()
, pygame.draw.rect()
… Per a
pintar una àrea, el mètode pygame.Surface.fill()
. Si disposem
de les imatges dels sprites ja fetes, només cal copiar els seus
píxels al lloc adient de la imatge final mitjançant el mètode
pygame.Surface.blit()
. L’actualització de l’estat dels
sprites consisteix tant en canviar la seva mida i posició com en
canviar la imatge associada.
El procés de dibuix de la nova imatge (dibuixa una nova imatge) es
realitza sobre una imatge encara no visible a pantalla. Quan s’acaba
de dibuixar, es mostra la nova imatge a la pantalla
(pygame.display.flip()
).
Exemple d’Animació¶
Heus ací un exemple, adaptat de la introducció:
import pygame
pygame.init()
mida = ample, alt = 320, 240
velocitat = [2, 2]
colorFons = 0, 128, 0
pantalla = pygame.display.set_mode(mida)
pilota = pygame.image.load("pilota.png")
capsaPilota = pilota.get_rect()
final = False
while not final:
for event in pygame.event.get():
if event.type == pygame.QUIT:
final=True
break
capsaPilota = capsaPilota.move(velocitat)
if capsaPilota.left < 0 or capsaPilota.right > ample:
velocitat[0] = -velocitat[0]
if capsaPilota.top < 0 or capsaPilota.bottom > alt:
velocitat[1] = -velocitat[1]
pantalla.fill(colorFons)
pantalla.blit(pilota, capsaPilota)
pygame.display.flip()
pygame.quit()
Copieu el codi en un fitxer anomenat joc1.py
,
proveu-lo i estudieu-lo (podeu trobar el codi comentat en la
introducció abans referida). Baixeu-vos la següent imatge per
a fer les proves.
Si heu fet la prova, haureu notat que la pilota va tant de pressa que els vostres ulls no poden seguir-la. De fet, us sembla veure entre 2 i 4 pilotes. Això es deu a la persistència de la imatge sobre la retina de l’ull.
Per a controlar el refresc de les successives pantalles de l’animació,
podeu temporitzar-ho instanciant un objecte de classe
pygame.time.Clock
(crono=pygame.time.Clock()) i afegint
la crida crono.tick(24) al cos de la iteració principal; això farà
que el programa no faci més de 24 imatges per segon. Vegeu el mètode pygame.time.Clock.tick()
.
Exemples de sondeig i tractament d’esdeveniments¶
Seguint amb l’exemple de la pilota, podríem visualitzar una per una la seqüència d’imatges (quadre a quadre) que donen la sensació de moviment de la pilota. Per això ens proposem generar una imatge, cada cop que premem una tecla del teclat que no sigui Escape. Si premem Escape sortim del programa, a l’igual que ho fem quan premem amb el ratolí el botó de sortida de la finestra del joc.
Consulteu la documentació del mòdul de pygame key
, per les etiquetes de cadascuna de les tecles del teclat, i en la documentació del mòdul event
trobareu els identificadors de tipus d’esdeveniments que capta pygame.
import pygame
pygame.init()
mida = ampla, alt = 320, 240
velocitat = [2, 2]
colorFons = 0, 128, 0
pantalla = pygame.display.set_mode(mida)
pilota = pygame.image.load("pilota.png")
capsaPilota = pilota.get_rect()
final = False
while not final:
for event in pygame.event.get():
if event.type == pygame.QUIT:
final = True
break
elif event.type == pygame.KEYDOWN:
tecla = event.key
if tecla == pygame.K_ESCAPE:
final = True
break
else:
capsaPilota = capsaPilota.move(velocitat)
if capsaPilota.left < 0 or capsaPilota.right > ampla:
velocitat[0] = -velocitat[0]
if capsaPilota.top < 0 or capsaPilota.bottom > alt:
velocitat[1] = -velocitat[1]
pantalla.fill(colorFons)
pantalla.blit(pilota, capsaPilota)
pygame.display.flip()
Copieu el codi en un fitxer anomenat joc3.py
i proveu-ho.
Un altre plantejament seria que que la pilota es mogui d’acord al que indiqui el ratolí dins de la pantalla del joc. Per aconseguir-ho, ens caldrà vigilar a cada pas de la iteració del joc, trobar-nos amb un esdeveniment del tipus pygame.MOUSEMOTION
. Si el trobem, obtindrem les noves coordenades del ratolí en pantalla (pygame.mouse.get_pos()
).
import pygame
pygame.init()
mida = ampla, alt = 320, 240
velocitat = [2, 2]
colorFons = 0, 128, 0
pantalla = pygame.display.set_mode(mida)
pilota = pygame.image.load("pilota.png")
capsaPilota = pilota.get_rect()
crono=pygame.time.Clock()
x, y = 0, 0
final = False
while not final:
for event in pygame.event.get():
if event.type == pygame.QUIT:
final = True
break
elif event.type == pygame.MOUSEMOTION:
x, y = pygame.mouse.get_pos()
capsaPilota.left = x
capsaPilota.top = y
pantalla.fill(colorFons)
pantalla.blit(pilota, capsaPilota)
pygame.display.flip()
crono.tick(24)
Copieu el codi en un fitxer anomenat joc4.py
. El provar-ho notareu que el ratolí apunta a l’extrem superior-esquerra del rectangle que encapsula la pilota. Podeu modificar-lo per que quedi centrat en la pilota. Vegeu
pygame.Rect
on podeu llegir que a més dels atributs pygame.Rect.top
i pygame.Rect.left
, n’hi han d’altres com pygame.Rect.center
.
Exemple de Sprites¶
El mòdul pygame.sprite
conté les classes
pygame.sprite.Sprite
i pygame.sprite.Group
, i
algunes funcions per detectar col·lisions entre sprites. Cal que
cada follet que participi en el joc es defineixi com un objecte d’una
classe derivada de la classe pygame.sprite.Sprite
. En el
següent exemple de codi ampliem el joc amb dues pilotes. Observeu que
és indispensable que la classe Pilota tingui els atributs image i
rect, així com el mètode update(). L’atribut image ha de ser de
classe pygame.Surface
i rect de classe
pygame.Rect
. A més a més, cal afegir tots els sprites en
un grup (en aquest cas, un objecte de la classe
pygame.sprite.Group
). Per actualitzar els sprites n’hi ha
prou amb cridar al mètode pygame.sprite.Group.update()
del
grup; i per visualitzar-los, al mètode
pygame.sprite.Group.draw()
del grup, el qual utilitza els
atributs image i rect de cada sprite.
import pygame
class Pilota(pygame.sprite.Sprite):
def __init__(self, posicio, velocitat):
super().__init__() # constructor de classe Sprite
self.image = pygame.image.load("pilota.png")
self.rect = self.image.get_rect() # rectangle de la imatge
self.rect.left = posicio[0]
self.rect.top = posicio[1]
self.velocitat = list(velocitat)
self.mon = pygame.display.get_surface().get_rect() # Àrea on es mourà
def update(self): # Actualitza posició i la velocitat de la pilota
self.rect.move_ip(self.velocitat)
if self.rect.left < 0 or self.rect.right > self.mon.width:
self.velocitat[0] = -self.velocitat[0]
if self.rect.top < 0 or self.rect.bottom > self.mon.height:
self.velocitat[1] = -self.velocitat[1]
pygame.init()
colorFons = (0, 200, 255)
pantalla = pygame.display.set_mode( (320,240) )
pilota1 = Pilota( (0,0), (2,2) )
pilota2 = Pilota( (100,100) , (1,1) )
pilotes = pygame.sprite.Group() # grup de Sprites
pilotes.add(pilota1)
pilotes.add(pilota2)
crono = pygame.time.Clock()
final = False
while not final:
for event in pygame.event.get():
if event.type == pygame.QUIT:
final = True
pilotes.update() # Actualitza la llista de sprites
pantalla.fill(colorFons) # pinta el fons de la pantalla
pilotes.draw(pantalla) # pinta sprites
pygame.display.flip() # visualitza la nova pantalla
crono.tick(24) # limita a 24 imatges per segon (24 fps)
pygame.quit()
Copieu el codi en un fitxer anomenat joc2.py
, proveu-lo i estudieu-lo.
Un exemple més treballat¶
Acabarem la introducció amb una proposta de disseny d’una funció anomenada joc que amplia les prestacions l’exemple anterior. El nou programa farà que les pilotes tinguin un comportament físic més real, i serà configurable. Configurar com han de ser les pilotes, (radi, imatge, etc) i quin comportament tindran (gravetat, acceleració, rebots entre elles, etc).
A mode d’exemple d’ús, l’anterior programa s’escriuria de la següent manera:
import pygame
from joc import joc
import math
midesP = (320, 240)
sons = ['rock.mid', 'puf.wav']
ipilota = pygame.image.load("pilota.png")
pilotes = []
radi = 55
pilotes.append((ipilota, (0, 0), radi, 200, (math.pi / 4, 2)))
pilotes.append((ipilota, (100, 100), radi, 200, (math.pi / 4, 1)))
regles = ['velocitat', 'límits']
regles2 = []
joc(midesP, pilotes, sons, regles, regles2)
És a dir, la funció joc rep com a paràmetres la mida de la pantalla, una llista de tuples especificant les pilotes, una llista de fitxers de sons, quin comportament han de mostrar les pilotes individualment en el seu món, i quines funcions hem d’aplicar en el cas que les pilotes col·lisionin entre sí.
Un altre objectiu de l’exposició que segueix, és veure com estructurem el disseny per tal d’aconseguir expressivitat, genericitat, i separació de conceptes en el programa final. Els conceptes, seran separats i agrupats en mòduls o classes.
Afegint una mica de física¶
Començarem estudiant com implementar conceptes físics. Aquests, els centralitzarem en un mòdul que anomenarem fisica.py
. Com que treballarem amb vectors, necessitarem també el mòdul vector2D.py
on trobarem funcions i operadors per l’aritmètica de vectors de dos dimensions. Els vectors s’expressaran de forma polar: angle i mòdul.
En fisica.py
introduirem els conceptes de velocitat, acceleració (força), pèrdua d’energia que pot tenir un sprite. Cada concepte l’encapsularem com una classe d’objecte que proporciona al sprite els atributs que calen per fer-ho funcionar, i contindrà un mètode que actualitza els atributs corresponents pel següent quadre o imatge (frame) a representar el fenomen.
Si el sprite es mou, com a mínim ha de tenir una velocitat (píxels/quadre):
class Velocitat:
def __init__(self, follet, vel):
self.follet = follet
self.follet.posicio = list(self.follet.rect.topleft)
self.follet.velocitat = Vector2D(vel[0], vel[1])
def update(self):
self.follet.posicio[0] += self.follet.velocitat.x()
self.follet.posicio[1] += self.follet.velocitat.y()
self.follet.rect.left = self.follet.posicio[0]
self.follet.rect.top = self.follet.posicio[1]
Noteu que guardem la posició en l’atribut posicio fora del rectangle rect. Això ho fem per poder tenir càlculs de tipus i precisió float. Els objectes pygame.Rect
de _pygame guarden la seva posicio i dimensió com a int (coordenades pixel). Encara que intentem assignar valors de tipus float, aquests es truncaran a int. En conseqüència, els següents càlculs perdràn precisió i les pèrdues s’aniran acumulant molt en els següents quadres.
Guardant els resultats a posicio, gaudim de la precisió dels float (que també es limitada), i trunquem el resultat float un cop per quadre.
Si s’aplica una força, hi haurà acceleració. L’acceleració es un canvi de velocitat en el temps que en l’animació de quadre a quadre serà el canvi discret de la velocitat del sprite:
class Acceleracio:
def __init__(self, follet, a):
self.follet = follet
self.follet.acceleracio = Vector2D(a[0], a[1])
def update(self):
self.follet.velocitat = self.follet.velocitat + self.follet.acceleracio
Una força podria ser la gravetat. La constant G la hem decidit empíricament fent proves.
class Gravetat(Acceleracio):
G = (math.pi / 2.0, 1)
def __init__(self, follet):
super().__init__(follet, self.G)
L’energia degut a fregament, etc, es va perdent. Podriem reduir la seva velocitat per un factor menor que 1 en el temps (quadre a quadre):
class Perdues:
def __init__(self, follet, p):
self.follet = follet
follet.perdua = p
def update(self):
self.follet.velocitat = self.follet.velocitat * self.follet.perdua
Acabem la part de comportament individual de les pilotes, afegint en fisica.py
la classe fisica.RebotaLimits
, que calcula el rebots amb les parets de la finestra del joc vistos en els exemples anteriors.
A més, del comportament individual que puguin tenir les pilotes, hi ha les possibles interaccions entre elles. Per exemple, que ensopeguin.
Si dues pilotes ensopeguen, caldrà calcular com afecta a la seva trajectòria i velocitat. A la wikipedia trobareu una explicació detallada de la col·lisió elàstica en que l’energia cinètica total dels dos cossos es conserva. Resumidament, per una pilota amb masa \(m_{1}\) i velocitat \(\vec{u}_{1}\), i una altra pilota amb masa \(m_{2}\) i velocitat \(\vec{u}_{2}\), un cop han impactat, les seves velocitats respectives \(\vec{v}_{1}\) i \(\vec{v}_{2}\) seran:
En el mòdul de fisica.py
disposeu de la funcio rebota(f1, f2) que donat dos sprites que col·lisionen, aplica les fòrmules anteriors per calcular les noves velocitats dels sprites.
Un detall a tenir en compte és que les actualitzacions geomètriques o físiques es fan de forma discreta. Els càlculs els efectuem en un espai discret de píxels, i en un temps marcat pels instants de cada quadre de l’animació (frame to frame).
En el mòn analògic l’ensopegada pot haver estat en un instant comprès entre quadres ja que el temps és continu, mentre que en el programa ho calculem en l’instant que correspon a un quadre sense tenir en compte que ha passat entre el quadre anterior i l’actual.
En particular, si dues pilotes ensopeguen, serà una casualitat que ho facin just en l’instant que representi un quadre. El normal serà que les dues s’encavalquin com si la matèria de les dues pilotes fos traspassable. Caldrà doncs recuperar les posicions que tenien en l’impacte un temps abans i prosseguir. En la funció trobareu la correcció de posicions que atribuirem a un quadre (Oh! Trampa!).
Adoneu-vos que si les velocitats de les dues pilotes són molt grans, podrien creuar-se en el temps que hi ha entre un quadre i l’altre quadre, i el programa no assabentar-se per que en l’altre quadre ja no interseca.
So¶
En so.py
es troba una classe per tractar el so del nostre exemple. Per això usarem els mòduls mixer
i music
. Bàsicament, la classe compta en que hi ha una música de fons que vindrà donat per un fitxer del tipus midi, ogg, o wav. En alguns sistemes també s’accepta el mp3. Estudieu el mòdul i veureu que és força senzill posar alguns sons al nostre joc exemple.
En el nostre exemple farem servir els fitxers rock.mid
i puf.wav
que us podeu baixar.
Redefinint la classe Pilota¶
En el fitxer pilotes.py
redefinirem la classe pilotes.Pilota
en que a l’inici caldrà fixar la posició del seu centre, el seu radi, la seva masa (pels impactes), l’objecte so per els efectes de so, i el seu comportament en forma de llista de classes del mòdul de física. Amb aquest últim paràmetre podrem fixar si volem que reboti a les parets de la finestra, si té velocitat, gravetat, alguna força, i/o pèrdues d’energia.
Noteu que el radi ho fem escalant la imatge de la pilota original usant la funció scale()
del mòdul transform
de pygame.
def __init__(self, imatge, posicio, radi, masa, so, conductes):
super().__init__() # constructor de classe Sprite
# escalem la imatge de la pilota d'acord al seu radi
self.image = pygame.transform.scale(imatge, (2 * radi, 2 * radi))
self.radius = radi
self.rect = self.image.get_rect() # rectangle de la imatge
self.rect.center = posicio
self.so = so
self.masa = masa
self.conductes = []
for f in conductes:
self.conductes.append(f(self))
def update(self): # Actualitza posició pilota
for f in self.conductes:
f.update()
class Pilotes(pygame.sprite.Group):
def __init__(self, so, entrePilotes):
super().__init__() # constructor de classe Sprite
Farem una classe derivada de Group
per tractar les colissions entre pilotes:
def update(self):
pilotes = self.sprites()
for pilota1, pilota2 in itertools.combinations(pilotes, 2):
if pygame.sprite.collide_circle(pilota1, pilota2):
for f in self.entrePilotes:
f(pilota1, pilota2)
super().update()
El paràmetre entrepilotes és una llista de funcions a fer per cada parell de pilotes que trobem. Aquí posarem només la funció rebota()
del mòdul fisica
.
Funció principal¶
En el fitxer joc.py
trobareu la funció principal de l’ampliació joc(midaP, pPilotes, sons, regles, regles2)()
. En una sessió de python, podem importar el mòdul i iniciar el joc amb els paràmetres oportuns. Per exemple, l’exemple de les dues pilotes introduit més amunt.
Un altre exemple amb 9 pilotes disposades amb cercle, és aquest:
import pygame
from joc import joc
import math
midesP = (800,600)
sons = ['rock.mid', 'puf.wav']
ipilota = pygame.image.load("pilota.png")
pilotes = []
refx, refy = midesP[0] // 2, midesP[1] // 2
radi = 10
r = 4 * radi
for a in map(math.radians, range(0, 360, 30)):
x = round(r * math.cos(a) + refx)
y = round(r * math.sin(a) + refy)
pilotes.append((ipilota, (x, y), radi, 200, ((math.pi / 4, 4.0))))
regles = ['velocitat', 'límits']
regles2 = []
joc(midesP, pilotes, sons, regles, regles2)
Es mouran totes en la mateixa direcció amb velocitat fixa. Podeu obervar que en el temps, per errors de precisió, el cercle de pilotes no es manté.
Un altre exemple usant l’anterior disposició però cada pilota té una velocitat fixe centrífuga com si quelcom esclatés des del centre de la disposició i les empenya cap a fora:
import pygame
from joc import joc
import math
midesP = (800, 600)
sons = ['rock.mid', 'puf.wav']
ipilota = pygame.image.load("pilota.png")
pilotes = []
refx, refy = midesP[0] // 2, midesP[1] // 2
radi = 10
r = 4 * radi
for a in map(math.radians, range(0, 360, 30)):
x = round(r * math.cos(a) + refx)
y = round(r * math.sin(a) + refy)
pilotes.append((ipilota, (x, y), radi, 200, (a, 2.0)))
regles = ['velocitat', 'límits']
regles2 = []
joc(midesP, pilotes, sons, regles, regles2)
Podeu ampliar el comportament de les pilotes fent (cal importar el mòdul fisica):
regles = ['velocitat', 'límits', 'perdues']
regles2 = [fisica.rebota]
o afegint la gravetat:
regles = ['gravetat', 'velocitat', 'límits', 'perdues']
A partir d’aquí, si heu entès el programa, podeu fer els canvis que s’us acudeixi. Per exemple, podrieu tractar les pilotes com si fossin planetes i implementar en el mòdul de física, la gravetat entre cossos. Enjoy it.