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

Introduzione

In questi due esempi che vedremo, simuleremo un campo di Air Hockey. Non ci concentreremo sul gioco in se, ma sulla simulazione fisica, quindi quello che riguarda il disco e il giocatore. Anche in questo caso il codice è relativamente semplice e basterà sottolieare alcuni punti per far capire il funzionamento globale.

I due esempi differiscono per i movimenti del giocatore. Infatti non basta spostare la mazza sul tavolo, perché altrimenti questa non imprimerebbe nessuna velocità al disco, serve quindi gestire la cosa trattando la mazza come se fosse un oggetto che riceve impulsi, oppure come un oggetto legato al puntatore del mouse e quindi ai suoi movimenti (di conseguenza acquisterà velocità, necessaria per spostare il disco realisticamente).

In entrambi vi sono le funzioni che abbiamo già visto nel precedente articolo, quindi mi limiterò a commentare le differenze e ad analizzare le cose in più.

Codice Air Hockey

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

def add_mouse(space):
    mass = 10
    inertia = pymunk.moment_for_circle(mass,0,35)
    body = pymunk.Body(mass,inertia)
    body.position=(50,50)
    c = pymunk.Circle(body,35)
    c.elasticity = 0.5
    space.add(body,c)
    return c

def add_L(space):
	body = pymunk.Body(pymunk.inf,pymunk.inf)
	body.position = (0,0)
	l1 = pymunk.Segment(body, (0,0),(400,0.),5.0)
	l2 = pymunk.Segment(body, (0,0),(0,600),5.0)
	l3 = pymunk.Segment(body, (400,0),(400,600),5.0)
	l4 = pymunk.Segment(body, (0,600),(400,600),5.0)
	l1.elasticity = 0.6
	l2.elasticity = 0.6
	l1.friction = 10
	l2.friction = 10
	l3.elasticity = 0.6
	l4.elasticity = 0.6
	l3.friction = 10
	l4.friction = 10
	space.add_static(l1,l2,l3,l4)
	return (l1,l2,l3,l4)

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],5)

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

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

def draw_ball(screen,ball):
	p = int(ball.body.position.x),600-int(ball.body.position.y)
	pygame.draw.circle(screen, THECOLORS["green"],p, int(ball.radius),2)

def run():
	pygame.init()
	pygame.mouse.set_visible(False)
	screen = pygame.display.set_mode((400,600),HWSURFACE | DOUBLEBUF,32)

	clock = pygame.time.Clock()

	pymunk.init_pymunk()
	space = pymunk.Space()
	#space.gravity = (0.0,-90.0)
	space.damping = 0.7
	space.resize_static_hash(1000,10000)
	space.resize_active_hash(1000,10000)

	lines = add_L(space)

	ball_shape = add_ball(space,(200,300))

	mouse_shape = add_mouse(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 == MOUSEMOTION:
				l = Vec2d(mouse_shape.body.position)
				x,y = pygame.mouse.get_pos()
				y = 600-y
				p = Vec2d((x,y))
				p = p-l
				p = p.normalized()
				mouse_shape.body.apply_impulse(p*1000)

		screen.fill(THECOLORS["black"])

		x,y = pygame.mouse.get_pos()
		y = 600-y
		mouse_shape.body.position.x=x
		mouse_shape.body.position.y=y
		p = int(mouse_shape.body.position.x),600-int(mouse_shape.body.position.y)
		pygame.draw.circle(screen, THECOLORS["yellow"],p, int(mouse_shape.radius),2)

		draw_lines(screen,lines)
		pygame.draw.line(screen, THECOLORS["blue"],(2,300),(398,300),2)
		pygame.draw.circle(screen,THECOLORS["blue"],(200,300),75,2)

		draw_ball(screen, ball_shape)

		space.step(1/60.)

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

if __name__ == '__main__':
	run()

Analisi

  • def add_mouse(space): il mouse costituisce la mazza con la quale colpiremo il disco, quindi creiamo un oggetto circolare che lo rappresenti.
  • def add_L(space): questa volta utilizzeremo dei segmenti per delimitare il campo di gioco  e questa funzione ha il compito di creare proprio quest’ultimo. Visto che non dovranno muoversi, possiamo aggiungerli come statici al nostro spazio pymunk.

Le funzioni successivi le avete già incontrate nell’esempio precedente. Vorrei ricordare che la funzione to_pygame viene utilizzata per la conversione delle coordinate. Anche se io non ne faccio utilizzo in questi piccoli esempi è comunque indispensabile integrare questa funzione in un engine di gioco. Pensate infatti di avere molti più oggetti al quale applicare questo calcolo e con risoluzioni di schermo differenti. Chiamando una funzione del genere, con i dovuti accorgimenti (cioè rilevare l’altezza dello schermo), il codice sarà molto più pulito, più chiaro e più maneggevole.

Chiusa questa piccola parentesi possiamo notare qualche piccola differenza nella funzione run (dalla linea 74). Per prima cosa abbiamo tolto la gravità (cioè non l’abbiamo settata, l’ho lasciata come commento) e abbiamo aggiunto il damping, cioè lo smorzamento, e modificato lo static_hash e l’active_hash per una migliore reattività. Naturalmente valora più grandi, rendono più sensibile e realistico il movimento, aumentando le risorse richieste per far girare il gioco, quindi non eccedete troppo perché questo è solo un file di esempio e ci sono solo due corpi da gestire. Abbiamo aggiunto di seguito le linee, la palla e il mouse (la nostra mazza.)

Tra gli eventi possiamo notare che controlliamo solo il movimento del mouse. In poche parole, viene creato un vettore dalla posizione attuale della mazza fino alla posizione del puntatore in quel momento. Normalizzando quel vettore avremo quindi la direzione che la mazza deve percorrere per raggiungere la nuova posizione del puntatore. Ora non dobbiamo spostare semplicemente la mazza, perché altrimenti non acquisterebbe la velocità che dovrebbe essere trasmessa al disco (come abbiamo detto in precedenza), quindi vi applichiamo un impulso che viene generato dal suo centro di gravità nella direzione indicata.

Per ovviare al fatto che la mazza sarebbe solo in balia di questi impulsi quando il mouse si muove, nel loop principale (dalla linea 102) facciamo semplicemente spostare il corpo della mazza nella posizione attuale del puntatore.

In questo modo abbiamo approssimativamente simulato questo gioco in un banale esempio. Sia ben chiaro, non è perfetto, ma in quasi 120 righe di codice (contando anche gli spazi), mi sembra più che accettabile. Se non ci credete, provate a riscrivere lo stesso giochino senza l’utilizzo di pymunk. Ora vediamo l’altro esempio:

Codice Air hockey con dumpingspring

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

def add_mouse(space):
    mass = 10
    inertia = pymunk.moment_for_circle(mass,0,35)
    body = pymunk.Body(mass,inertia)
    body.position=(50,50)
    c = pymunk.Circle(body,35)
    c.elasticity = 0.5
    space.add(body,c)
    return c

def add_L(space):
	body = pymunk.Body(pymunk.inf,pymunk.inf)
	body.position = (0,0)
	l1 = pymunk.Segment(body, (0,0),(400,0.),5.0)
	l2 = pymunk.Segment(body, (0,0),(0,600),5.0)
	l3 = pymunk.Segment(body, (400,0),(400,600),5.0)
	l4 = pymunk.Segment(body, (0,600),(400,600),5.0)
	l1.elasticity = 0.6
	l2.elasticity = 0.6
	l1.friction = 10
	l2.friction = 10
	l3.elasticity = 0.6
	l4.elasticity = 0.6
	l3.friction = 10
	l4.friction = 10
	space.add_static(l1,l2,l3,l4)
	return (l1,l2,l3,l4)

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],5)

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

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

def draw_ball(screen,ball):
	p = int(ball.body.position.x),600-int(ball.body.position.y)
	pygame.draw.circle(screen, THECOLORS["green"],p, int(ball.radius),2)

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

	clock = pygame.time.Clock()

	pymunk.init_pymunk()
	space = pymunk.Space()
	#space.gravity = (0.0,-90.0)
	space.damping = 0.3
	space.resize_static_hash(1000,10000)
	space.resize_active_hash(1000,10000)

	lines = add_L(space)

	ball = add_ball(space,(200,300))

	mouse_shape = add_mouse(space)
	point_shape = pymunk.Body(pymunk.inf,pymunk.inf)

	rest_lenght = mouse_shape.body.position.get_distance(point_shape.position)
	ds = pymunk.DampedSpring(mouse_shape.body, point_shape,(0,0),(0,0),25.,1000,0.009)
	space.add(ds)

	pygame.mouse.set_pos((50,550))
	pygame.mouse.set_visible(False)

	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 == MOUSEMOTION:
				x,y = pygame.mouse.get_pos()
				y= 600-y
				point_shape.position.x=x
				point_shape.position.y=y
				#l = Vec2d(mouse_shape.body.position)
				#x,y = pygame.mouse.get_pos()
				#y = 600-y
				#p = Vec2d((x,y))
				#p = p-l
				#p = p.normalized()
				#mouse_shape.body.apply_impulse(p*1000)

		screen.fill(THECOLORS["black"])

		p = int(mouse_shape.body.position.x),600-int(mouse_shape.body.position.y)
		pygame.draw.circle(screen, THECOLORS["yellow"],p, int(mouse_shape.radius),2)

		draw_lines(screen,lines)
		pygame.draw.line(screen, THECOLORS["blue"],(2,300),(398,300),2)
		pygame.draw.circle(screen,THECOLORS["blue"],(200,300),75,2)

		pygame.draw.circle(screen,THECOLORS["purple"],pygame.mouse.get_pos(),3,3)
		draw_ball(screen, ball)

		space.step(1/60.)

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

if __name__ == '__main__':
	run()

Analisi

Dalla linea 80 possiamo notare come questo approccio sia completamente differente. In questo caso, il puntatore del mouse è indipendente dalla mazza (che rimane mouse_shape), ma i due corpi sono collegati da un DampedSprig, cioè una molla che ha un coefficente di smorzamento. In questo modo, nel loop dei controlli, dobbiamo solo aggiornare la posizione del puntatore (point_shape) perché la mazza (mouse_shape) è legata ad esso.

Con questo piccolo accorgimento però abbiamo cambiato le meccaniche di gioco, serviva piuttosto per fare vedere come si può variare lostile in base a semplici regole. Infatti siete voi stessi che gestite i “compromessi” della simulazione, in base al tipo di gioco e al progetto che volete realizzare.

Conclusioni

Con questo spero di aver fatto capire alcuni concetti chiave nell’utilizzo di librerie fisiche. L’unico modo per imparare ad utilizzarle è cimentarvi con esse, sperimentare varie soluzioni e testare anche i più piccoli cambiamenti. A volte un coefficente di attrito troppo alto potrebbe compromettere lo stile di gioco. Certo, sono ipotesi un pò esagerate, ma dovete comunque avere ben chiara l’idea finale per raggiungere una soluzione soddisfacente. Di seguito rilascio i sorgenti dei due esempi.

Sorgenti

Press ESC to close