Sviluppare un gioco in Python: Physics engine, prima parte.

Introduzione

Per ora abbiamo gestito collisioni semplici, utilizzando le funzioni che la libreria pygame ci dava a disposizione, ma per un gioco completo e abbastanza appagante, ci vorrebbe qualcosa di più.

Una buona simulazione fisica può rendere un gioco piacevole molto più interessante e avvincente. Infatti bisogna ricordare che ci vogliono i giusti ingredienti per forgiare un gioco di successo, perché senza divertimento la bellezza non conta nulla (per questo giochiamo, per divertirci, giusto?). Più avanti tratteremo anche queste problematiche, ora però torniamo alla questione del giorno.

Sulla rete possiamo trovare molti engine fisici che possono venirci in aiuto. Costruire un motore che gestisce la fisica per il nostro videogioco può diventare una cosa molto ardua e dispendiosa in termini di tempo, per questo è meglio affidarsi a soluzioni già pronte (nessuno vi vieta di scriverne uno tutto vostro).

Tra gli engine disponibili più famosi che si possono utilizzare con python vi sono:

Pyode e pynewton possono gestire le tre dimensioni, mentre pybox2d e pymunk sono solo engine per il 2D. Tra questi ho deciso di utilizzare pymunk per entrare nel mondo della simulazione fisica 2D, molto meno complessa del 3D. Con questo non vuol dire che pybox2d sia da meno, ma ho preferito utilizzare una libreria che non abbia problemi di esecuzione con sistemi windows a 64 bit (sicuramente saranno corretti con versioni future).

Installazione


Per installare pymunk basta procurarsi i sorgenti ed eseguire la procedura per l’installazione di un qualsiasi modulo python (le instruzioni si possono trovare anche all’interno del file zip che potete scaricare dalla sezione downloads), in questo caso:

python setup.py build_chipmunk

python setup.py install

Per gli utenti windows è consigliata l’installazione della versione 32 bit di pymunk per python 2.6.

Fatto questo potete provare se l’installazione è andata a buon fine digitando il comando “import pymunk” in una qualsiasi shell python, che non dovrebbe riportare nessun errore. All’interno dell’archivio che scaricate dal sito ufficiale c’è anche una cartella “examples” dove troverete dei programmi di esempio che potete utilizzare per verificare il corretto funzionamento della libreria e per imparare il suo utilizzo.

Pymunk

Per prima cosa, vedremo un codice di esempio e qualche concetto di base (si tratta della seguente guida):

  • Corpi rigidi : i corpi rigidi perdono tutte le proprietà fisiche che ha un oggetto, come la massa, la posizione, la velocità ecc… Non ha una sua forma.
  • Forme di collisione : assiociando una forma ad un corpo si possono definire le sembianze di quel corpo. Si possono associare più forme allo stesso corpo, per creare uno scheletro più complesso oppure si possono non associare delle forme, se necessario.
  • Vincoli e giunti : si possono congiungere due corpi per vincolare il loro comportamento.
  • Spaces (Spazi) : sono l’unità di base per la simulazione in pymunk (chipmunk). Agli spazi si possono aggiungere corpi, forme, vincoli per poi aggiornare lo spazio nel suo complesso.

Qui potete vedere alcuni esempi dei vincoli presenti in Chipmunk (e quindi in pymunk).

Questi sono i concetti chiavi descritti nel tutorial di pymunk, ma sono anche validi per l’utilizzo della libreria Chipmunk o di qualsia motore fisico. Ecco di seguito il codice che verrà analizzato:

Codice

import pygame,pymunk,sys
from pygame.locals import *
from pygame.color import *

def add_L(space):
	rotation_center_body = pymunk.Body(pymunk.inf,pymunk.inf)
	rotation_center_body.position=(300,100)

	rotation_limit_body = pymunk.Body(pymunk.inf,pymunk.inf)
	rotation_limit_body.position = (200,100)

	body = pymunk.Body(100,10000)
	body.position = (300,100)
	l1 = pymunk.Segment(body, (-150,0),(300.,0.),5.0)
	l2 = pymunk.Segment(body, (-150,0),(-150.,150.),5.0)

	rotation_center_joint = pymunk.PinJoint(body, rotation_center_body, (0,0),(0,0))
	joint_limit = 50
	rotation_limit_joint = pymunk.SlideJoint(body,rotation_limit_body, (-100,0),(0,0),(0),joint_limit)

	l1.elasticity = 0.5
	l2.elasticity = 0.5
	l1.friction = 10
	l2.friction = 10
	space.add(l1,l2,body,rotation_center_joint,rotation_limit_joint)
	return (l1,l2)

def draw_lines(screen, lines):
	for line in lines:
		body = line.body
		pv1 = body.position + line.a.rotated(body.angle)
		pv2 = body.position + line.b.rotated(body.angle)
		p1 = to_pygame(pv1)
		p2 = to_pygame(pv2)
		pygame.draw.lines(screen, THECOLORS["red"],False,[p1,p2],1)

def to_pygame(p):
	return (int(p.x),int(-p.y+480))

def add_ball(space,p):
	mass = 1
	radius = 12.5
	inertia = pymunk.moment_for_circle(mass,0,radius)
	body = pymunk.Body(mass,inertia)
	body.position = p
	shape= pymunk.Circle(body,radius)
	shape.elasticity = 0.95
	shape.friction = 10
	space.add(body,shape)
	return shape

def draw_ball(screen,ball):
	p = int(ball.body.position.x),480-int(ball.body.position.y)
	x,y=p
	angle = int(ball.body.angle)
	imballr = pygame.transform.rotozoom(imball,angle,1)
	screen.blit(imballr,(x-imballr.get_width()/2,y-imballr.get_width()/2))
	pygame.draw.circle(screen, THECOLORS["green"],p, int(ball.radius),2)

def run():
	pygame.init()
	screen = pygame.display.set_mode((640,480),HWSURFACE | DOUBLEBUF,32)

	global imball
	imball = pygame.image.load("8ball.png").convert_alpha()

	clock = pygame.time.Clock()

	pymunk.init_pymunk()
	space = pymunk.Space()
	space.gravity = (0.0,-900.0)

	balls = []
	lines = add_L(space)

	while True:
		for event in pygame.event.get():
			if event.type == QUIT:
				sys.exit()
			elif event.type == KEYDOWN and event.key == K_ESCAPE:
				sys.exit()
			if event.type == MOUSEBUTTONDOWN and pygame.mouse.get_pressed() == (1,0,0):
				x,y = pygame.mouse.get_pos()
				y = 480-y
				ball_shape = add_ball(space,(x,y))
				balls.append(ball_shape)

		screen.fill(THECOLORS["black"])

		balls_to_remove = []
		for ball in balls:
			if ball.body.position.y < 0:
				balls_to_remove.append(ball)
			draw_ball(screen, ball)

		for ball in balls_to_remove:
			space.remove(ball,ball.body)
			balls.remove(ball)

		draw_lines(screen,lines)
		pygame.draw.circle(screen,THECOLORS["blue"],(300,480-100),5)
		pygame.draw.circle(screen,THECOLORS["blue"],(200,480-100),50,2)

		space.step(1/60.)

		pygame.display.update()
		clock.tick(60)
		pygame.display.set_caption("Esempio Fisica. FPS = "+str(clock.get_fps()))

if __name__ == '__main__':
	run()

Analisi

Prima di analizzare il codice, vorrei farvi notare che non contano molto le proprietà fisiche che hanno gli oggetti in se, ma conta piuttosto la resa finale. Quindi non meravigliatevi se troverete una gravità con -900.0 invece di -9.8.

  • Importiamo per prima cosa tutte le librerie necessarie, quindi pygame, pymunk ecc…
  • def add_L(space): questa funzione crea un corpo a forma di ‘L’ che ruota su un perno, limitato da una circonferenza, come si può vedere dalla figura. Rotation_center è il punto centrale dove la ‘L’ ruota, mentre rotation_limit è la circonferenza accanto. Entrambe sono un corpo pymunk statico (nel senso che hanno massa ed inerzia infinita). Body invece è la nostra ‘L’, che infatti è composta da due segmenti. Le coordinate dei segmenti si devono tenere conto dalla posizione di Body, quindi da bosy.position.  Fatto questo, aggiungiamo quindi un vincolo di rotazione tra body (la nostra ‘L’) ed il punto centrale (il nostro rotation_center). Poi decidiamo il limite di movimento del prossimo nodo di tipo SlideJoint, per associarlo quindi alla nostra figura. Settiamo l’elasticità e l’attrito dei due segmenti e li aggiungiamo allo spazio pymunk (che verrà creato nel main principale). Infine ritorniamo le due linee, perché dovranno essere disegnate tramite pygame sullo schermo.
  • def draw_lines(screen, lines): come potete immaginare, questa funzione stampa a video le linee create in precedenza. pv1 e pv2 sono il punto di inizio e di fine (o viceversa) di un segmento. Questi, prima di essere disegnati sullo schermo, devono essere convertiti secondo le coordinate di pygame. Infatti pymunk presenta un piano dove l’asse delle ordinate è orientato verso l’alto, mentre pygame ha l’asse ‘y’ orientato verso il basso (si parla del verso positivo). Una volta fatto questo si disegna tramite pygame.draw.lines la linea sullo schermo.
  • def to_pygame(p): ritorna le coordinate corrette per pygame. In questo caso lo schermo è alto 480 pixel, quindi basta sottrarre a quelli le ordinate di pymunk per avere il corrispettivo giusto.
  • def add_ball(space,p): con questa funzione creiamo una palla di massa 1 e di raggio 12.5. Bisogna settare il momento di inerzia dell’oggetto e poi definire la sua forma (necessaria per le collisioni). In questo caso è semplice perché possiamo utilizzare direttamente le funzioni presenti in pymunk, ma per oggetti più complessi si dovrà optare per altre soluzioni. Chiudiamo aggiungendo l’elasticità e l’attrito per poi inserire l’oggetto nello spazio pymunk e ritornare la forma (perché dovrà essere renderizzata sullo schermo).
  • def draw_ball(screen,ball): convertiamo sul posto le coordinate e renderizziamo l’immagine di una palla numero 8 con un cerchio verde che la circonda.
  • def run(): inizializziamo pygame, carichiamo l’immagine della palla e creiamo un orologio per il tempo di gioco. Inizializziamo pymunk ed un spazio con gravità dove potremo aggiungere i notri oggetti. Creiamo due liste per tenere conto delle figure da renderizzare sullo schermo. Tra gli eventi che controlliamo, abbiamo quello del tasto sinistro del mouse, che ha il compito di creare una palla nel punto dove siamo e di aggiungerla sia alla lista degli oggetti da renderizzare, sia allo spazio pymunk da noi creato.  Il loop principale è molto semplice: si rende lo schermo nero e si crea una lista delle sfere che escono fuori dalla visuale (non vogliamo sprecare troppe risorse calcolando oggetti fisici fuori dallo schermo). Quindi se la coordinata ‘y’ di una sfera è minore di 0 nello spazio pymunk, si può aggiungere a quelle da rimuovere, altrimenti la si renderizza sullo schermo. L’altro ciclo rimuove la sfera dallo spazio e quindi anche dalla lista. Disegnamo poi le due linee (che formeranno la nostra ‘L’) e i due cerchi che rappresentano i nostri vincoli che abbiamo imposto in precedenza. Infine facciamo avanzare di uno step corrispondente a (1/60.) la simulazione nello spazio pymunk e di conseguenza aggiorniamo lo schermo per poi richiamare il tick dell’oggetto clock di pygame a 60 fps. L’ultima riga serve per vedere gli fps sulla barra del titolo della finestra.

Si consiglia di utilizzare un passo per lo step dello spazio pymunk costante e non dipendente dagli fps del gioco, per una migliore resa. Ricordate inoltre che senza una forma gli oggetti non possono collidere.

Conclusioni

Per ora non vi resta  che provare il codice e sperimentare di vostra iniziativa, ma nel prossimo articolo vedremo altri due esempi. Spero di essere stato abbastanza chiaro, anche perché il pezzo non è di difficile comprensione ed è per la maggior parte uguale al tutorial presentato sul sito ufficiale. Anche da questa piccola simulazione possiamo comunque notare come un gioco di qualsiasi livello abbia bisogno di gestire degli eventi “fisici” tra oggetti, in particolare le collisioni. Se non vi siete annoiati troppo, non vi resta che aspettare il prossimo articolo.

Sorgenti

Edit : i sorgenti sono stati aggiornati per permettere all’immagine della palla numero 8 di ruotare insieme alla circonferenza creata con pymunk (nel mondo “fisico”). Anche il codice nell’articolo è stato aggiornato, aggiungendo due linee di codice dopo la riga 54.

Press ESC to close