Games II
In questa lezione, creaeremo alcuni semplici giochi usando le idee introdotte nelle lezioni precenti. Per evitare di duplicare l'implementazione delle basi di questi giochi, abbiamo creato una collezione di classi che useremo nell'implementazione e contenuto nel pacchetto.
GameKit
Per implementare piccolo giochi interattivi, introduciamo una serie di classi il cui codice è contenuto qua sotto. Includiamo la classe vec
per i vettori bidimnesionali.
Il gioco stesso è constituito da una serie di oggetti di tipo Shape
. Questi sono cerchi o rettangoli, secondo la variable iscircle
, con posizione pos
, velocità vel
, dimensione size
e colore color
. tk_id
è l'identificativo per la GUI.
L'insieme di oggetti è mantenuto da World
che ne simula, in modo estremamente approssimato, la fisica. Mentre l'interfaccia utente è mantenuta da TkWindow
import math, time
import Tkinter as tk
class vec(object):
def __init__(self,x,y):
self.x = x
self.y = y
def length(self):
return math.sqrt(self.x*self.x+self.y*self.y)
def normalize(self):
l = self.length()
if l: return vec(self.x/l,self.y/l)
else: return vec(0,0)
def clamp(self,m):
l = self.length()
if l > m: return vec(self.x*m/l,self.y*m/l)
else: return vec(self.x,self.y)
def add(self,v):
return vec( self.x+v.x, self.y+v.y )
def sub(self,v):
return vec( self.x-v.x, self.y-v.y )
def scale(self,s):
return vec( self.x*s, self.y*s )
def dot(self,v):
return self.x*v.x+self.y*v.y
class Shape(object):
def __init__(self,pos,size,vel,iscircle,color,simulated):
self.pos = pos
self.size = size # for circles, size[0] == size[1]
self.vel = vel
self.iscircle = iscircle
self.color = color
self.simulated = simulated
self.tk_id = None
def display(self,canvas):
if not self.tk_id:
if self.iscircle:
self.tk_id = canvas.create_oval(
self.pos.x-self.size.x,
self.pos.y-self.size.y,
self.pos.x+self.size.x,
self.pos.y+self.size.y,
fill=self.color)
else:
self.tk_id = canvas.create_rectangle(
self.pos.x-self.size.x,
self.pos.y-self.size.y,
self.pos.x+self.size.x,
self.pos.y+self.size.y,
fill=self.color)
else:
canvas.coords(
self.tk_id,
self.pos.x-self.size.x,
self.pos.y-self.size.y,
self.pos.x+self.size.x,
self.pos.y+self.size.y)
canvas.itemconfig(self.tk_id,fill=self.color)
class World(object):
def __init__(self,w,h,c):
self.width = w
self.height = h
self.color = c
self.shapes = []
self.tk_id = None
# behaviour
self.drag = 0
self.gravity = 0
# active walls (nswe)
self.walls = (True,True,True,True)
# keeps time of last frame
self.lasttime = None
def update(self):
# update frame time
now = time.time()
dt = (now - self.lasttime) if self.lasttime else 0
self.lasttime = now
# update velocities and positions
for shape in self.shapes:
if not shape.simulated: continue
acc = vec(0,0)
if self.drag: acc = acc.add(shape.vel.scale(-self.drag))
if self.gravity: acc = acc.add(vec(0,-self.gravity))
shape.vel = shape.vel.add(acc.scale(dt))
shape.pos = shape.pos.add(shape.vel.scale(dt))
# handle collisions
for i in range(len(self.shapes)):
for j in range(i+1,len(self.shapes)):
shape1, shape2 = self.shapes[i], self.shapes[j]
if not shape1.simulated and not shape2.simulated: continue
if shape2.simulated and not shape1.simulated: shape1, shape2 = shape2, shape1
if shape1.iscircle and shape2.iscircle and shape2.simulated:
self.handle_collision_circle_circle(shape1,shape2)
elif shape1.iscircle and shape2.iscircle and not shape2.simulated:
self.handle_collision_circle_staticcircle(shape1,shape2)
elif shape1.iscircle and not shape2.iscircle and not shape2.simulated:
self.handle_collision_circle_staticrect(shape1,shape2)
# handle walls
for shape in self.shapes:
if not shape.simulated: continue
self.handle_collision_wall(shape)
def find(self,pos,simulatedonly=True):
for shape in self.shapes:
if simulatedonly and not shape.simulated: continue
if shape.iscircle:
if shape.pos.sub(pos).length() < shape.size.x: return shape
return None
def handle_collision_wall(self,shape):
if shape.pos.y - shape.size.y < 0:
if self.walls[0]: shape.pos.y = shape.size.y; shape.vel.y = -shape.vel.y
if shape.pos.y + shape.size.y > self.height:
if self.walls[1]: shape.pos.y = self.height - shape.size.y; shape.vel.y = -shape.vel.y
if shape.pos.x - shape.size.x < 0:
if self.walls[2]: shape.pos.x = shape.size.x; shape.vel.x = -shape.vel.x
if shape.pos.x + shape.size.x > self.width:
if self.walls[3]: shape.pos.x = self.width - shape.size.x; shape.vel.x = -shape.vel.x
def handle_collision_circle_circle(self,shape1,shape2):
d = shape1.pos.sub(shape2.pos).length()
r = shape1.size.x + shape2.size.x
if d < r:
o = shape1.pos.add(shape2.pos).scale(0.5)
y = shape1.pos.sub(shape2.pos).normalize()
x = vec(y.y,-y.x)
shape1.pos = o.add(y.scale( r*0.5))
shape2.pos = o.add(y.scale(-r*0.5))
shape1.vel = x.scale(shape1.vel.dot(x)).add(y.scale( abs(shape1.vel.add(shape2.vel).dot(y))/2))
shape2.vel = x.scale(shape2.vel.dot(x)).add(y.scale(-abs(shape1.vel.add(shape2.vel).dot(y))/2))
def handle_collision_circle_staticcircle(self,shape1,shape2):
d = shape1.pos.sub(shape2.pos).length()
r = shape1.size.x + shape2.size.x
if d < r:
y = shape1.pos.sub(shape2.pos).normalize()
x = vec(y.y,-y.x)
shape1.pos = shape2.pos.add(y.scale(r))
shape1.vel = x.scale(shape1.vel.dot(x)).add(y.scale(-shape1.vel.dot(y)))
def handle_collision_circle_staticrect(self,shape1,shape2):
if abs(shape1.pos.x - shape2.pos.x) < shape2.size.x + shape1.size.x and abs(shape1.pos.y - shape2.pos.y) < shape2.size.y:
if shape1.vel.x > 0:
shape1.pos.x = shape2.pos.x - shape2.size.x - shape1.size.x
else:
shape1.pos.x = shape2.pos.x + shape2.size.x + shape1.size.x
shape1.vel.x = -shape1.vel.x
if abs(shape1.pos.y - shape2.pos.y) < shape2.size.y + shape1.size.y and abs(shape1.pos.x - shape2.pos.x) < shape2.size.x:
if shape1.vel.y > 0:
shape1.pos.y = shape2.pos.y - shape2.size.y - shape1.size.y
else:
shape1.pos.y = shape2.pos.y + shape2.size.y + shape1.size.y
shape1.vel.y = -shape1.vel.y
def display(self,canvas):
if not self.tk_id:
self.tk_id = canvas.create_rectangle(
0,0,self.width,self.height,
fill=self.color)
for s in self.shapes: s.display(canvas)
class TkWindow(object):
def __init__(self,w,h):
self.win = tk.Tk()
self.win.minsize(w,h)
self.win.resizable(width=False, height=False)
self.win.rowconfigure(0, weight=1)
self.win.columnconfigure(0, weight=1)
self.canvas = tk.Canvas(self.win, bg='gray')
self.canvas.grid(row=0, column=0, sticky='nsew')
# user actions
self.mouse_pressed = False
self.mouse_pos = vec(0,0)
self.mouse_lastpos = vec(0,0)
self.mouse_delta = vec(0,0)
# binding callbacks
self.canvas.bind('<ButtonPress-1>',self.mouse_pressed)
self.canvas.bind('<ButtonRelease-1>',self.mouse_released)
self.canvas.bind('<B1-Motion>',self.mouse_moved)
def mouse_pressed(self,event):
self.mouse_pressed = True
self.mouse_lastpos = vec(event.x,event.y)
self.mouse_pos = vec(event.x,event.y)
self.mouse_delta = self.mouse_pos.sub(mouse.last_pos)
def mouse_moved(self,event):
self.mouse_pressed = True
self.mouse_lastpos = self.mouse_pos
self.mouse_delta = vec(event.x,event.y).sub(self.mouse_pos)
self.mouse_pos = vec(event.x,event.y)
def mouse_released(self,event):
self.mouse_pressed = False
def schedule(self,func):
self.canvas.after(10,func)
def run(self):
self.win.mainloop()
GameKit 2
Mostriamo qui anche una versione di vec
e relativo gamekit
con implementato le operazioni aritmetiche sovrascrivendo gli operatori per semplificare la lettura. In particolare, l'operare +
corrisponde a __add__
, etc. Notate come questa versione è molto più chiara da leggere.
import math, time
import Tkinter as tk
class vec(object):
def __init__(self,x,y):
self.x = x
self.y = y
def length(self):
return math.sqrt(self.x*self.x+self.y*self.y)
def normalize(self):
l = self.length()
if l: return vec(self.x/l,self.y/l)
else: return vec(0,0)
def clamp(self,m):
l = self.length()
if l > m: return vec(self.x*m/l, self.y*m/l)
else: return vec(self.x, self.y)
def __add__(self, v):
return vec(self.x + v.x, self.y + v.y)
def __sub__(self, v):
return vec(self.x - v.x, self.y - v.y)
def __mul__(self, v):
if type(v) == vec:
return self.x*v.x + self.y*v.y
else:
return vec(self.x*v, self.y*v)
def __rmul__(self, s):
return vec(self.x*s, self.y*s)
class Shape(object):
def __init__(self, pos, size, vel, iscircle, color, simulated):
self.pos = pos
self.size = size # for circles, size.x == size.y
self.vel = vel
self.iscircle = iscircle
self.color = color
self.simulated = simulated
self.tk_id = None
def display(self,canvas):
if not self.tk_id:
if self.iscircle:
self.tk_id = canvas.create_oval(
self.pos.x-self.size.x,
self.pos.y-self.size.y,
self.pos.x+self.size.x,
self.pos.y+self.size.y,
fill=self.color)
else:
self.tk_id = canvas.create_rectangle(
self.pos.x-self.size.x,
self.pos.y-self.size.y,
self.pos.x+self.size.x,
self.pos.y+self.size.y,
fill=self.color)
else:
canvas.coords(
self.tk_id,
self.pos.x-self.size.x,
self.pos.y-self.size.y,
self.pos.x+self.size.x,
self.pos.y+self.size.y)
canvas.itemconfig(self.tk_id,fill=self.color)
class World(object):
def __init__(self, w, h, c):
self.width = w
self.height = h
self.color = c
self.shapes = []
self.tk_id = None
# behaviour
self.drag = 0
self.gravity = 0
# active walls (nswe)
self.walls = (True, True, True, True)
# keeps time of last frame
self.lasttime = None
def update(self):
# update frame time
now = time.time()
dt = (now - self.lasttime) if self.lasttime else 0
self.lasttime = now
# update velocities and positions
for shape in self.shapes:
if not shape.simulated: continue
acc = vec(0, 0)
if self.drag: acc += shape.vel*(-self.drag)
if self.gravity: acc += vec(0, -self.gravity)
shape.vel += acc*dt
shape.pos += shape.vel*dt
# handle collisions
for i in range(len(self.shapes)):
for j in range(i+1, len(self.shapes)):
shape1, shape2 = self.shapes[i], self.shapes[j]
if not shape1.simulated and not shape2.simulated: continue
if shape2.simulated and not shape1.simulated:
shape1, shape2 = shape2, shape1
if shape1.iscircle and shape2.iscircle and shape2.simulated:
self.handle_collision_circle_circle(shape1, shape2)
elif shape1.iscircle and shape2.iscircle and not shape2.simulated:
self.handle_collision_circle_staticcircle(shape1, shape2)
elif shape1.iscircle and not shape2.iscircle and not shape2.simulated:
self.handle_collision_circle_staticrect(shape1, shape2)
# handle walls
for shape in self.shapes:
if not shape.simulated: continue
self.handle_collision_wall(shape)
def find(self, pos, simulatedonly=True):
for shape in self.shapes:
if simulatedonly and not shape.simulated: continue
if shape.iscircle:
if (shape.pos - pos).length() < shape.size.x: return shape
return None
def handle_collision_wall(self, shape):
if shape.pos.y - shape.size.y < 0:
if self.walls[0]: shape.pos.y = shape.size.y; shape.vel.y = -shape.vel.y
if shape.pos.y + shape.size.y > self.height:
if self.walls[1]: shape.pos.y = self.height - shape.size.y; shape.vel.y = -shape.vel.y
if shape.pos.x - shape.size.x < 0:
if self.walls[2]: shape.pos.x = shape.size.x; shape.vel.x = -shape.vel.x
if shape.pos.x + shape.size.x > self.width:
if self.walls[3]: shape.pos.x = self.width - shape.size.x; shape.vel.x = -shape.vel.x
def handle_collision_circle_circle(self, shape1, shape2):
d = (shape1.pos - shape2.pos).length()
r = shape1.size.x + shape2.size.x
if d < r:
o = (shape1.pos + shape2.pos)*0.5
y = (shape1.pos - shape2.pos).normalize()
x = vec(y.y, -y.x)
shape1.pos = o + (y*(r*0.5))
shape2.pos = o + (y*(-r*0.5))
shape1.vel = (x*(shape1.vel*x)) + (y*(abs((shape1.vel + shape2.vel)*y)/2))
shape2.vel = (x*(shape2.vel*x)) + (y*(-abs((shape1.vel + shape2.vel)*y)/2))
def handle_collision_circle_staticcircle(self,shape1,shape2):
d = (shape1.pos - shape2.pos).length()
r = shape1.size.x + shape2.size.x
if d < r:
y = (shape1.pos - shape2.pos).normalize()
x = vec(y.y, -y.x)
shape1.pos = shape2.pos + (y*r)
shape1.vel = (x*(shape1.vel*x)) + (y*(-(shape1.vel*y)))
def handle_collision_circle_staticrect(self, shape1, shape2):
if abs(shape1.pos.x - shape2.pos.x) < shape2.size.x + shape1.size.x and abs(shape1.pos.y - shape2.pos.y) < shape2.size.y:
if shape1.vel.x > 0:
shape1.pos.x = shape2.pos.x - shape2.size.x - shape1.size.x
else:
shape1.pos.x = shape2.pos.x + shape2.size.x + shape1.size.x
shape1.vel.x = -shape1.vel.x
if abs(shape1.pos.y - shape2.pos.y) < shape2.size.y + shape1.size.y and abs(shape1.pos.x - shape2.pos.x) < shape2.size.x:
if shape1.vel.y > 0:
shape1.pos.y = shape2.pos.y - shape2.size.y - shape1.size.y
else:
shape1.pos.y = shape2.pos.y + shape2.size.y + shape1.size.y
shape1.vel.y = -shape1.vel.y
def display(self, canvas):
if not self.tk_id:
self.tk_id = canvas.create_rectangle(
0,0,self.width,self.height,
fill=self.color)
for s in self.shapes: s.display(canvas)
class TkWindow(object):
def __init__(self,w,h):
self.win = tk.Tk()
self.win.minsize(w,h)
self.win.resizable(width=False, height=False)
self.win.rowconfigure(0, weight=1)
self.win.columnconfigure(0, weight=1)
self.canvas = tk.Canvas(self.win, bg='gray')
self.canvas.grid(row=0, column=0, sticky='nsew')
# user actions
self.mouse_pressed = False
self.mouse_pos = vec(0,0)
self.mouse_lastpos = vec(0,0)
self.mouse_delta = vec(0,0)
# binding callbacks
self.canvas.bind('<ButtonPress-1>',self.mouse_pressed)
self.canvas.bind('<ButtonRelease-1>',self.mouse_released)
self.canvas.bind('<B1-Motion>',self.mouse_moved)
def mouse_pressed(self,event):
self.mouse_pressed = True
self.mouse_lastpos = vec(event.x, event.y)
self.mouse_pos = vec(event.x, event.y)
self.mouse_delta = self.mouse_pos - mouse.last_pos
def mouse_moved(self,event):
self.mouse_pressed = True
self.mouse_lastpos = self.mouse_pos
self.mouse_delta = vec(event.x, event.y) - self.mouse_pos
self.mouse_pos = vec(event.x, event.y)
def mouse_released(self,event):
self.mouse_pressed = False
def schedule(self,func):
self.canvas.after(10, func)
def run(self):
self.win.mainloop()
Giochi
Ecco due esempi di giochi. Prima una versione solitaria del biliardo.
from gamekit import World, Shape, TkWindow, vec
import random
class GameBilliard(object):
def __init__(self,nballs):
# game world
self.world = World(512,512,'black')
self.world.drag = 0.2
for _ in range(nballs):
b = Shape(
vec(random.random()*self.world.width,
random.random()*self.world.height),
vec(10,10),
# vec((random.random()-0.5)*100,(random.random()-0.5)*100),
vec(0,0),
True,
'white',
True)
self.world.shapes.append(b)
# tk window
self.window = TkWindow(self.world.width,self.world.height)
# game state
self.grabbed = None
def frame(self):
# handle user events
if self.window.mouse_pressed and not self.grabbed:
self.grabbed = self.world.find(self.window.mouse_pos)
if self.grabbed:
if self.window.mouse_pressed:
self.grabbed.pos = self.window.mouse_pos
self.grabbed.vel = vec(0,0)
self.grabbed.simulated = False
self.grabbed.color = 'red'
else:
self.grabbed.vel = self.window.mouse_delta.scale(10)
self.grabbed.simulated = True
self.grabbed.color = 'white'
self.grabbed = None
# run game simulation
self.world.update()
self.world.display(self.window.canvas)
self.window.schedule(self.frame)
def run(self):
self.frame()
self.window.run()
game = GameBilliard(50)
game.run()
E poi una versione solitaria di Pong.
from gamekit import World, Shape, TkWindow, vec
import random
class GamePong(object):
def __init__(self):
# game world
self.world = World(512,512,'black')
self.ball = Shape(
vec(self.world.width/2,self.world.height/2),
vec(10,10),
vec((random.random()-0.5),(random.random()-0.5)).normalize().scale(400),
True,
'white',
True)
self.paddle = Shape(
vec(self.world.width/2,self.world.height-50),
vec(50,10),
vec(0,0),
False,
'white',
False)
self.world.shapes.append(self.ball)
self.world.shapes.append(self.paddle)
# tk window
self.window = TkWindow(self.world.width,self.world.height)
# game state
self.grabbed = None
def frame(self):
# handle user events
if self.window.mouse_pressed:
self.paddle.pos.x = self.window.mouse_pos.x
# run game simulation
self.world.update()
self.world.display(self.window.canvas)
self.window.schedule(self.frame)
def run(self):
self.frame()
self.window.run()
game = GamePong()
game.run()