Fondamenti di Programmazione

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