Fondamenti di Programmazione

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()