Classi II
In questa lezione riprenderemo i concetti di classe e oggetto giá visti in precedenza, facendo variazioni sugli esempi di comportamento delle palline. Trovete gli esempi nel pacchetto.
Classi e Oggetti
Per reiterare, le classi definiscono un “modello” che raggruppa sia le variabili, per memorizzare lo stato di un oggetto, che i metodi, per definire il comportamento dell'oggetto. Gli oggetti, o instanze, di una classe sono entitá singole construite basandosi sul modello definito dalla classe. Due oggetti della stessa classe hanno le stesse variabili e gli stessi metodi, ma i valori memorizzati nelle variabili sono diversi. Due classi hanno variabili e metodi diversi.
Il primo esempio di classi che abbiamo introdotto era l'esempio di palline in movimento sulle schermo. In questa lezione, estendiamo questo modello con nuovi esempi. In quell'esempio, avevamo introdutto due classi Brownian
e Bouncing
. Per ogni classe, l'utente poteva creare tanti oggetti, il cui comportamento era definito dalla classe, ma la cui posizione e colore erano diversi, dato che i valori memorizzati nella variabili sono diversi per ogni oggetto. Il valore iniziale di queste variabili é definito nel costruttore __init__
che viene chiamato per creare un oggetto semplicemente usando il nome della classe.
Riprendiamo questa lezione introducendo una versione leggermente modificato dell'esempio precedente che contiene anche ostacoli e la possibilitá di collisione, anche se con una matematica estremamente semplificata e imprecisa.
Tipi Matematici
Possiamo usare le classi anche per specificare tipi matematici come i vettori in 2 dimensioni. Troverete di sotto l'implementazione che possiamo includere nel file vec.py
. Notate che nonostante potessimo sovrascrivere gli operatori aritmetici, cioè definire le operazioni sul vec
usando ad esempio +-*/
, qui usiamo un'implementazione con i metodi per rendere il codice più ovvio da seguire. Questo è un esempio classico dell'uso delle classi per ampliare i tipi matematici definiti dal linguaggio con i tipi definiti dall'utente.
import math
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 )
Mondo e Boids
Per iniziare questa lezione, reimplementiamo l'esempio del moto browniano fatto in predenza usando la classe vec
.
import random
from vec import vec
class Brownian(object):
def __init__(self,pos,r,c):
self.pos = pos
self.vel = vec(0,0)
self.r = r
self.c = c
self.tk_id = 0
def update(self, world):
self.vel = vec(random.randint(-5,+5), random.randint(-5,+5))
self.pos = self.pos.add(self.vel)
def display(self,canvas):
if not self.tk_id:
self.tk_id = canvas.create_oval(
self.pos.x-self.r,self.pos.y-self.r,
self.pos.x+self.r,self.pos.y+self.r,
fill=self.c)
else:
canvas.coords(
self.tk_id,self.pos.x-self.r,self.pos.y-self.r,
self.pos.x+self.r,self.pos.y+self.r)
Introduciamo anche una classe per memorizzare il “mondo” World
che contiene sia le palline che degli ostacoli (per ora definiti come cerchi che non si muovono). In questa classe introduciamo anche funzioni per cacolare le collisioni tra palline e con i muri. Usiamo un algoritmo estremamente semplice, e piuttosto impreciso, ma che funziona a sufficienza per il nostro esempio. Per vedere se due corpi sono il collisione, validiamo se la dustanza tra i proprio centri è minore della somma dei raggi. Se sì, spostiamo indietro la posizione del primo in modo che non si tocchino più.
from vec import vec
class World(object):
def __init__(self,w,h):
self.width = w
self.height = h
self.robots = []
self.obstacles = []
self.tk_id = 0 # tk identifier
def hit_obstacle(self,pos,r):
for o in self.obstacles:
dpos = pos.sub(o.pos)
d = dpos.length()
if d < r+o.r: return o.pos
return None
def update(self):
for r in self.robots: r.update(self)
self.update_collisions()
self.update_bounds()
def update_bounds(self):
for r in self.robots:
if r.pos.x + r.r > self.width: r.pos.x = self.width - r.r; r.vel.x = - r.vel.x
if r.pos.x - r.r < 0: r.pos.x = r.r; r.vel.x = - r.vel.x
if r.pos.y + r.r > self.height: r.pos.y = self.height - r.r; r.vel.y = - r.vel.y
if r.pos.y - r.r < 0: r.pos.y = r.r; r.vel.y = - r.vel.y
def update_collisions(self):
for r in self.robots:
for o in self.obstacles:
self.update_collision(r,o)
for r1 in self.robots:
for r2 in self.robots:
if r1 != r2: self.update_collision(r1,r2)
def update_collision(self,r1,r2):
dpos = r2.pos.sub(r1.pos)
d = dpos.length()
if d < r1.r+r2.r:
if d:
dr = (r1.r+r2.r) / d
r1.pos = r2.pos.add( r1.pos.sub(r2.pos).scale(dr) )
else:
r1.pos = r2.pos.add( vec(d,0) )
def display(self,canvas):
if not self.tk_id:
self.tk_id = canvas.create_rectangle(0,0,self.width,self.height,fill='black')
for r in self.robots: r.display(canvas)
for o in self.obstacles: o.display(canvas)
E questo è il codice per gli ostacoli.
class Obstacle(object):
def __init__(self,pos,r):
self.pos = pos
self.r = r
self.tk_id = 0
def update(self):
pass
def display(self,canvas):
if not self.tk_id:
self.tk_id = canvas.create_oval(self.pos.x-self.r,self.pos.y-self.r,
self.pos.x+self.r,self.pos.y+self.r,
fill='gray')
else:
canvas.coords(self.tk_id,self.pos.x-self.r,self.pos.y-self.r,
self.pos.x+self.r,self.pos.y+self.r)
Infine, mostriamo il codice per creare un semplice esempio.
import Tkinter as tk
import random, math
from vec import vec
from ball import Brownian
from obstacle import Obstacle
from world import World
colors = [ 'white', 'yellow', 'green', 'red' ]
def frame():
world.update()
world.display(canvas)
canvas.after(20, frame)
world = World(512,512)
win = tk.Tk()
win.rowconfigure(0, weight=1)
win.columnconfigure(0, weight=1)
canvas = tk.Canvas(win, bg='gray')
canvas.grid(row=0, column=0, sticky='nsew')
for i in range(25):
world.robots += [ Brownian(
vec(random.randint(1,world.width),random.randint(1,world.height)),
10, random.choice(colors)
) ]
world.obstacles += [ Obstacle(vec(100,100),50) ]
world.obstacles += [ Obstacle(vec(400,100),50) ]
world.obstacles += [ Obstacle(vec(100,400),50) ]
world.obstacles += [ Obstacle(vec(400,400),50) ]
frame()
win.mainloop()
Comportamenti Singoli
Possiamo simulare semplici comportamenti delle palline cambiano la funzione update
. Notate come cambiando la classe, cambiamo il comportamento. Inoltre, nella nostra implementazione, possiamo anche cambiare il comportamento settando dei pesi su varie possibili opzioni. Per questo esempio, usiamo un modello di locomozione dove la pallina ha una velocità massima e dove la nuova velocità è calcolata come variazione della precente. Questo è un modello semplicissimo, ma un pò più “fisico” del precedente.
Mostriamo 4 comportamenti. In follow, la pallina segue un target puntandolo con tutta la propria velocità. In brownian, la pallina accellera a caso (non equivalente al precedente dato che il quello cambiavamo la velocità diretamente). Nel wander, la pallina sterza a caso, ma non cambia la direzione principale. Infine, in obstacle, la pallina cerca di evitare gli ostacoli sterzando con la massina velocità se l'ostacolo è nel percorso futuro.
from vec import vec
import random
class Boid(object):
def __init__(self,pos,vel,r,c,mv,f,b,w,a):
self.pos = pos # position
self.vel = vel # velocity
self.r = r # radius
self.c = c # color
self.tk_id = 0 # tk identifier
# max driving behaviour
self.max_vel = mv # max speed
self.vel = self.vel.clamp(self.max_vel)
# specify behaviour
self.follow = f # behaviour weight --- follow target
self.brownian = b # behaviour weight --- brownian (2 is good)
self.wander = w # behaviour weight --- wander (0.5 is good)
self.wander_speed = 0 # behaviour --- wander speed
self.avoidance = a # behaviour weight --- obstacle avoidance (0.5 is good)
def update(self, world):
# impose max steering
steer = vec(0,0)
# follow target
if self.follow:
d = self.follow.pos.sub(self.pos)
dl = d.length()
nvel = d.normalize().scale(min(self.max_vel,d))
steer = steer.add(nvel.sub(self.vel))
# brownian
if self.brownian > 0:
steer = steer.add(vec(
(random.random()-0.5)*self.brownian,
(random.random()-0.5)*self.brownian))
# wander
if self.wander > 0:
self.wander_speed += (random.random()-0.5)*self.wander
if abs(self.wander_speed) > self.wander:
self.wander_speed *= self.wander / abs(self.wander_speed)
d = self.vel.normalize()
steer = steer.add(vec(-d.y*self.wander_speed,d.x*self.wander_speed))
# obstacle avoidance
if self.avoidance > 0:
# uses the global world
npos = self.pos.add(self.vel)
c = 0
while c < self.avoidance:
npos = self.pos.add(self.vel.scale(c))
opos = world.hit_obstacle(npos,self.r)
if opos:
d = self.vel.normalize()
dd = npos.sub(opos)
if d.x*dd.x+d.y*dd.y > 0:
steer = steer.add(vec(-d.y,d.x).scale(self.max_vel))
else:
steer = steer.add(vec(d.y,-d.x).scale(self.max_vel))
break
else: c += self.max_vel / 10.0
# impose max steering
steer = steer.clamp(self.max_vel)
# update velocity
self.vel = self.vel.add(steer).clamp(self.max_vel)
# update position
self.pos = self.pos.add(self.vel)
def display(self,canvas):
if not self.tk_id:
self.tk_id = canvas.create_oval(self.pos.x-self.r,self.pos.y-self.r,
self.pos.x+self.r,self.pos.y+self.r,
fill=self.c)
else:
canvas.coords(self.tk_id,self.pos.x-self.r,self.pos.y-self.r,
self.pos.x+self.r,self.pos.y+self.r)
Infine, mostriamo il codice per creare un semplice esempio.
import Tkinter as tk
import random, math
from vec import vec
from boid import Boid
from obstacle import Obstacle
from world import World
colors = [ 'white', 'yellow', 'green', 'red' ]
def frame():
world.update()
world.display(canvas)
canvas.after(20, frame)
world = World(512,512)
win = tk.Tk()
win.rowconfigure(0, weight=1)
win.columnconfigure(0, weight=1)
canvas = tk.Canvas(win, bg='white')
canvas.grid(row=0, column=0, sticky='nsew')
# for i in range(10):
# boid = Boid(
# vec(random.random()*world.width,random.random()*world.height),
# vec((random.random()-0.5)*10,(random.random()-0.5)*10),
# 10, colors[0])
# world.robots += [ boid ]
target = Boid(
vec(random.random()*world.width,random.random()*world.height),
vec((random.random()-0.5)*10,(random.random()-0.5)*10),
10, colors[1], 5, None, 0, 0.5, 0)
world.robots += [ target ]
avoider = Boid(
vec(random.random()*world.width,random.random()*world.height),
vec((random.random()-0.5)*10,(random.random()-0.5)*10),
10, colors[2], 5, None, 0, 0, 10)
world.robots += [ avoider ]
for i in range(3):
follower = Boid(
vec(random.random()*world.width,random.random()*world.height),
vec((random.random()-0.5)*10,(random.random()-0.5)*10),
10, colors[0], 3, target, 0, 0, 0)
world.robots += [ follower ]
world.obstacles += [ Obstacle(vec(100,100),50) ]
world.obstacles += [ Obstacle(vec(400,100),50) ]
world.obstacles += [ Obstacle(vec(100,400),50) ]
world.obstacles += [ Obstacle(vec(400,400),50) ]
frame()
win.mainloop()
Comportamenti di Gruppo
Facciamo infine un esempio di palline che si comportano in dipendenza da tutte le altre, come un gruppo di pesci o uccelli. In queto esempio uniamo tre comportamenti. In separation, la pallina si allontana dai vicini sterzando in direzione opposta (e pesata con 1/r
). In cohesion, la pallina si mouve verso il centro di tutte le palline. In alignment, la velocità della pallina si allinea alla velocità media. Invece di considerare tutte le palline nel calcolare le medie, consideriamo solo quelle in un raggio definito. Notate che questo esempio, usato in molti film, non e' ottimo nel nostro caso perchè lo sciame non sa della presenza dei muri.
from vec import vec
import random
class FlockBoid(object):
def __init__(self,pos,vel,r,c,mv,f,fs,fc,fa,fd):
self.pos = pos # position
self.vel = vel # velocity
self.r = r # radius
self.c = c # color
self.tk_id = 0 # tk identifier
# max driving behaviour
self.max_vel = mv # max speed
self.vel = self.vel.clamp(self.max_vel)
# specify behaviour
self.follow = f # behaviour weight --- follow target
self.separation = fs # behaviour weight --- separation
self.cohesion = fc # behaviour weight --- cohesion
self.alignment = fa # behaviour weight --- alignment
self.distance = fd # behaviour weight --- distance
def update(self, world):
# impose max steering
steer = vec(0,0)
# follow target
if self.follow:
d = self.follow.pos.sub(self.pos)
nvel = d.normalize().scale(min(self.max_vel,d.length()))
steer = steer.add(nvel.sub(self.vel))
# flock
visible = []
for r in world.robots:
if r == self: continue
d = self.pos.sub(r.pos).length()
if d < self.distance:
visible.append(r)
separate = vec(0,0)
for r in visible:
d = self.pos.sub(r.pos)
separate = separate.add(d.normalize().scale(1.0/max(d.length(),1)))
steer = steer.add(separate.normalize().scale(self.separation))
avg_pos = vec(0,0)
avg_vel = vec(0,0)
for r in visible:
avg_pos = avg_pos.add(r.pos.scale(1.0/len(visible)))
avg_vel = avg_vel.add(r.vel.scale(1.0/len(visible)))
coalesce = avg_pos.sub(self.pos)
steer = steer.add(coalesce.normalize().scale(self.cohesion))
align = avg_vel
steer = steer.add(align.normalize().scale(self.alignment))
# impose max steering
steer = steer.clamp(self.max_vel)
# update velocity
self.vel = self.vel.add(steer).clamp(self.max_vel)
# update position
self.pos = self.pos.add(self.vel)
def display(self,canvas):
if not self.tk_id:
self.tk_id = canvas.create_oval(self.pos.x-self.r,self.pos.y-self.r,
self.pos.x+self.r,self.pos.y+self.r,
fill=self.c)
else:
canvas.coords(self.tk_id,self.pos.x-self.r,self.pos.y-self.r,
self.pos.x+self.r,self.pos.y+self.r)
Infine, mostriamo il codice per creare un semplice esempio.
import Tkinter as tk
import random, math
from vec import vec
from boid import Boid
from flock import FlockBoid
from obstacle import Obstacle
from world import World
colors = [ 'white', 'yellow', 'green', 'red' ]
def frame():
world.update()
world.display(canvas)
canvas.after(20, frame)
world = World(512,512)
win = tk.Tk()
win.rowconfigure(0, weight=1)
win.columnconfigure(0, weight=1)
canvas = tk.Canvas(win, bg='white')
canvas.grid(row=0, column=0, sticky='nsew')
target = Boid(
vec(random.random()*world.width,random.random()*world.height),
vec((random.random()-0.5)*10,(random.random()-0.5)*10),
10, colors[1], 5, None, 0, 0.5, 0)
world.robots += [ target ]
for i in range(50):
flockboid = FlockBoid(
vec(random.random()*world.width,random.random()*world.height),
vec((random.random()-0.5)*5,(random.random()-0.5)*5),
5, colors[0], 5, None, 1, 2, 2, 200)
world.robots += [ flockboid ]
frame()
win.mainloop()