Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Realizzazione e sviluppo di
un mini-gioco per la riabilitazione
della mano
RELATORE
Prof. Nunzio Alberto BORGHESE
TESI DI LAUREA DI
Jacopo Essenziale
CORRELATORE
Matr. 740006
INDICE
1. INTRODUZIONE
2. RIABILITAZIONE
a. RIABILITAZIONE A CASA: PROGETTTO REWIRE
b. IGER: UN GAME ENGINE PER LA RIABILITAZIONE
3. RIABILITAZIONE DELLA MANO
a. STRUMENTI DIGITALI GIA UTILIZZATI NELLA RIABILITAZIONE DELLA MANO
b. OBIETTIVO DEL PROGETTO REALIZZATO
4. STRUMENTI SOFTWARE UTILIZZATI
5. PANDA 3D
a. FUNZIONAMENTO LOOP DI GIOCO
b. COLLISION DETECTION IN PANDA3D
c. MAPPATURA DI INPUT BIDIMENSIONALE NELLO SPAZIO: CAMERA ORTOGRAFICA
d. MAPPATURA DI INPUT BIDIMENSIONALE NELLO SPAZIO: RAY CASTING
6. LEAP MOTION CONTROLLER
a. PROGRAMMARE IL LEAP API
b. PRIMI TEST
c. INTRODURRE NUOVE GESTURES GRABBING
d. IL LEAP MOTION CONTROLLER E LA RIABILITAZIONE
7. REALIZZAZIONE DEL GIOCO: HOT AIR BALLOON
a. SCELTA DELLARCHITETTURA WEB-BASED
b. IL PROGETTO DA REALIZZARE
c. HTML 5 TAG CANVAS E REALIZZAZIONE DEL LOOP DI GIOCO IN JAVASCRIPT
i. DEFINIRE LA ZONA DI GIOCO ALLINTERNO DELLA PAGINA WEB
ii. INIZIALIZZARE E LANCIARE IL GIOCO
d. HOT AIR BALLOON DISEGNARE SUL CANVAS
e. HOT AIR BALLOON IL PRIMO SFONDO E LA MONGOLFIERA
i. CARICAMENTO ASINCRONO E PRELOADING DELLE SPRITES.
f. HOT AIR BALLOON RILEVARE E VALIDARE LINPUT DELLUTENTE
i. RILEVARE LE GESTURES PINCH IN e PINCH OUT
ii. MISURARE LA DISTANZA DELLE DITA SULLO SCHERMO
iii. VALIDARE LINPUT DELLUTENTE
g. HOT AIR BALLOON STRUTTURA DEL GIOCO E GAMEPLAY
h. HOT AIR BALLOON FASI DI GIOCO
i. HOT AIR BALLOON ANIMAZIONI
j. HOT AIR BALLOON GAMEPLAY: MOVIMENTO DEI NEMICI; MONETE E COLLISION
DETECTION
k. HOT AIR BALLOON FEEDBACK E SUGGERIMENTI ALLUTENTE
l. HOT AIR BALLOON SISTEMA DI AUTO-CALIBRAZIONE
8. REALIZZAZIONE DELLA PIATTAFORMA PER I TERAPISTI
i
ii
1 - INTRODUZIONE
La riabilitazione fisica e cognitiva richiede ai pazienti un lavoro intenso e prolungato nel tempo.
Il lavoro che i pazienti devono affrontare durante il periodo di riabilitazione costituito da esercizi
ripetuti che mirano a sollecitare le funzionalit da recuperare. La figura del terapista, in qualit di
istruttore, motivatore, assistente e osservatore del paziente durante lesecuzione dellesercizio
riabilitativo fondamentale. E particolarmente importante che il paziente esegua gli esercizi in
modo corretto per evitare linsorgere di problemi muscolari o scheletrici o acquisisca movimenti non
corretti, situazione che viene indicata come maladaptation (adattamento dannoso).
Purtroppo, oggigiorno, gli oneri relativi al supporto del personale, delle strutture e delle attrezzature
necessarie a mantenere attivi ed efficienti i reparti di riabilitazione allinterno delle strutture
sanitarie, sono quanto mai ingenti, ci costringe tali strutture a cercare di abbattere i costi,
riducendo al minimo necessario la fornitura di tali servizi, rischiando cos di ridurre la qualit del
servizio offerto.
Una possibile soluzione al problema dei costi, che potrebbe addirittura innalzare il livello di qualit
dei servizi riabilitativi offerti dalle strutture sanitarie, quella di decentralizzare il lavoro sul paziente,
permettendo a questultimo di svolgere gran parte degli esercizi riabilitativi a casa, in modo
autonomo, ma sempre sotto la supervisione dei terapisti.
Questo approccio reso oggi possibile, dallampia diffusione, di nuovi strumenti a basso costo di
interazione uomo-macchina (HID Human Interface Devices), che spesso vengono prodotti e
utilizzati in ambito videoludico.
Abbiamo ad esempio:
- Microsoft Kinect
- Wii Balance Board
- Leap Motion Controller
- Tablet e Smartphone con schermi multi-touch
Questi dispositivi sono in grado di tracciare, con sufficiente precisione, molti dei movimenti nello
spazio, o nel piano che compiono gli utenti che li utilizzano. Tali rilevazioni possono essere utilizzate
in ambito riabilitativo per controllare la postura e i movimenti del paziente mentre svolge i propri
esercizi di riabilitazione a casa, fornendo cos la possibilit ai terapisti di supervisionare a distanza il
paziente, e al paziente di svolgere a casa il proprio lavoro di riabilitazione in completa sicurezza.
Inoltre, essendo i dispositivi sopracitati, spesso nati dallindustria videoludica, possibile mappare
allinterno di videogiochi creati ad hoc, gli esercizi di riabilitazione che ogni singolo paziente deve
svolgere, creando cos degli esercizi-giochi (exer-games), che permettano di introdurre una
componente di intrattenimento e divertimento allinterno dellesercizio, rendendo il lavoro di
riabilitazione quotidiano pi stimolante.
Questo ci che viene svolto oggi nellambito del progetto REWIRE (di cui si parler di seguito), ed
ci che si cercato di fare, in piccolo, per quanto riguarda la riabilitazione della mano nel progetto
descritto nei capitoli successivi.
2 - RIABILITAZIONE
giusto, prima di procedere oltre, definire cosa si intende per riabilitazione e medicina fisica o
riabilitativa, poich sar largomento centrale di tutto il lavoro descritto in seguito.
Si tratta di quella branca della medicina volta alla diagnosi, terapia e riabilitazione di quelle disabilit
derivate da malattie invalidanti, che possono compromettere le funzionalit motorie, cognitive o
emozionali del paziente.
La medicina riabilitativa coinvolge una serie di figure professionali tra cui quella del fisioterapista,
che si occupa di valutare la condizione dei singoli pazienti e stabilire un percorso riabilitativo
adatto, costituito, spesso da una serie di esercizi da compiere atti al recupero della funzionalit
compromessa.
La funzione del fisioterapista fondamentale nel processo riabilitativo, esso infatti non si limita a
fornire una serie di esercizi che il paziente pu svolgere in autonomia, ma spesso, deve assisterlo
durante lesecuzione fornendo tempestive correzioni in caso di errore, per impedire il
peggioramento delle condizioni iniziali, e fornendo anche la giusta motivazione, che lo spinga a
persistere nellesercizio e ad affrontare le inevitabili difficolt che affronter durante il suo percorso.
importante tenere ben presente questa triplice funzione del terapista di:
- Ideatore del programma di esercizi adatti al paziente
- Correttore di eventuali errori commessi nellesecuzione dei singoli esercizi
- Motivatore del paziente.
In quanto questi tre elementi sono fondamentali affinch il processo riabilitativo vada a buon fine e
dovranno, pertanto, essere presenti anche nel sistema intelligente che affiancher il paziente
durante la riabilitazione a casa propria, quindi in assenza del terapista.
PATIENT STATION (PS): Si tratta di un sistema, inteso come insieme di hardware / software,
che viene installato a casa dei pazienti. Consiste in sostanza di un insieme di mini-videogiochi
che vengono mostrati sullo schermo di un televisore ai pazienti.
Il sistema di input segue il paradigma Hands Free (o a mani libere), si tratta, in breve di
utilizzare una serie di dispositivi (ad esempio il Microsoft Kinect), per il tracciamento della
posizione del paziente nello spazio, questo gli permette di interagire con lambiente virtuale
senza dover agire su nessun dispositivo fisico quale ad esempio mouse, tastiera, joypads,
joysticks ecc.
Cuore della Patient Station il Game Engine IGER (Intelligent Game Engine for
Rehabilitation), sviluppato presso il Laboratorio di Sistemi Intelligenti Applicati del
Diparitmento di Informatica dellUnviersit degli Studi di Milano, un Game Engine basato su
Panda3D, che oltre a fornire le funzionalit classiche di un Game Engine, affronta una serie
di problematiche dovute alla necessit di monitorare ed adattarsi alle necessit del paziente,
analizzeremo queste problematiche nel dettaglio pi avanti.
-
HOSPITAL STATION (HS): Questa stazione, viene utilizzata dalle strutture e dal personale
sanitario, permette di definire a distanza il programma personalizzato di esercizi che ogni
paziente deve affrontare, inoltre fornisce gli strumenti per monitorare e analizzare i
progressi dei singoli pazienti attraverso lelaborazione dei dati ricevuti dalle singole Patient
Stations.
Rendering Engine (o Renderer): componente che si occupa della generazione dei singoli
frame bidimensionali o tridimensionali a partire da modelli o sprites. il motore sul quale si
basa la generazine di qualsiasi genere di animazione allinterno del gioco.
- Engine Fisico: componente che si occupa di simulare la fisica allinterno del mondo virtuale
ricreato allinterno del videogioco.
- Collision Detection: ogni game engine deve fornire tool adeguati ed efficienti per la
rilevazione di qualsiasi tipo di collisione tra i modelli caricati allinterno del gioco.
- Tools opzionali: quali ad esempio, tool per la gestione dei suoni, le animazioni, lintelligenza
artificiale, input devices differenti e tutto ci che pu essere utile allo sviluppo di un
videogioco.
IGER eredita tutti questi aspetti dal game engine su cui si basa, PANDA3D.
Poich gli esercizi di riabilitazione possono essere molto diversi tra di loro e le abilit e bisogni dei
pazienti possono variare, i videogiochi per la riabilitazione devono potersi adattare alle esigenze dei
3
singoli utenti, e nel frattempo essere in grado di monitorare e archiviare ogni singolo movimento,
mantenendo il pi possibile invariata lesperienza di gioco da un paziente allaltro.
IGER aggiunge alle funzionalit standard di un game engine tradizionale:
-
Tools standard: per la configurazione offline, da parte dei terapisti, dei singoli videogiochi
riabilitativi, permettendo di aggiustare i parametri che pi si ritengono opportuni alle
esigenze di un determinato paziente.
- Strumenti per il graduale adattamento automatico dei parametri di gioco per meglio
rispecchiare levoluzione delle abilit psicomotorie del paziente.
- Strumenti per il monitoraggio in tempo reale dei movimenti dei pazienti.
- Strumenti per lacquisizione e salvataggio dei dati di gioco sia per la valutazione dei progressi
del paziente sia per la motivazione.
Inoltre IGER fornisce un layer di astrazione sul device di Input, denominato IDRA (Input Device for
Rehabilitaion Abstraction layer) questo per potersi adattare al meglio alle esigenze del paziente:
Sar ad esempio pi facile utilizzare un joystick per un paziente affetto da NSU (Neglect) piuttosto
che per un paziente con qualche tipo di paralisi alle braccia. Daltro canto un dispositivo come la
Nintendo Wii Balance Board, non potr essere utilizzata da pazienti con qualche sorta di paralisi alla
parte inferiore del corpo.
Questo layer di astrazione sui dispositivi di Input permetter a qualsiasi paziente di giocare a
qualsiasi gioco della piattaforma, in quanto esso fornisce un interfaccia di input verso il gioco
comune a tutti i dispositivi.
Al momento i dispositivi supportati da IGER sono:
-
Microsoft Kinect.
Sony Playstation 3 Eye camera.
Nintendo Wii Balance Board.
Tyromotion Tymo.
Motycon OpenGo Insoles.
Phantom Omni.
Novint Falcon.
E in parte il Leap Motion Controller.
Il gameplay deve essere adattato alle capacit e allet del bambino, non bisogna proporre
nulla di molto complesso, ed fondamentale, per mantenere acceso linteresse del
bambino, che a ogni azione o movimento svolto in maniera corretta dal paziente
corrisponda un immediata risposta, divertente e stimolante del sistema.
Gli obbiettivi stabiliti dal gioco devono essere quanto pi flessibili possibile e non vincolanti,
il modo in cui il bambino si approccer al gioco spesso imprevedibile, per tanto la
5
valutazione delle prestazioni e dei miglioramenti non potr essere misurata ad esempio
dalla quantit di obbiettivi raggiunti, quanto dallosservazione dei dati ottenuti dalle
misurazioni effettuate durante lintera sessione di gioco
Il feedback fornito al paziente dal sistema sia in caso di movimento scorretto che di
motivazione deve essere studiato in maniera adeguata, ad esempio la figura del terapista
virtuale, usato con gli adulti in REWIRE ha poco effetto su un paziente di quattro anni, avr
sicuramente pi effetto uno smiley sullo schermo, piuttosto che fuochi dartificio, colori e
animazioni.
Innanzi tutto la moltitudine di applicazioni e giochi presenti nei vari store multimediali, offre
sicuramente unampia scelta, questo permette di trovare lapplicazione che pi riesce a
catturare linteresse del bambino e sar quindi pi facile mantenerlo concentrato
sullesercizio da svolgere.
- Lapplicazione installata disponibile per tutti, senza necessit di infrastrutture particolari,
come ad esempio un server ospedaliero per lo storage dei dati ottenuti dallapplicazione, o
sistemi di autenticazione e gestione dei diversi pazienti, questo permette in sostanza ai
genitori dei pazienti di installare sul proprio dispositivo la stessa applicazione provata in
ospedale, e fare giocare il bambino a casa per esercitare il pi possibile la mano da riabilitare
- Inoltre i costi relativi allacquisto di un gioco sullapp store sicuramente irrisorio rispetto a
quello di realizzazione di applicazioni ad hoc.
Vi sono per purtroppo una serie di svantaggi nellutilizzo di queste applicazioni commerciali,
pensate per lintrattenimento in ambito medico/riabilitativo:
-
Spesso questi giochi non sono configurabili, non vi cio la possibilit da parte del terapista,
n di predisporre una tabella di esercizi da svolgere, n di impostare il singolo gioco in modo
da accettare unicamente il tipo di movimento desiderato.
Questi giochi non sono pensati per adattarsi alle capacit fisiche del loro utente, ci significa
che generalmente un interazione errata viene interpretata come un errore, e il giocatore
viene semplicemente penalizzato; in realt, come si visto in precedenza analizzando le
funzionalit di IGER, nei videogiochi per la riabilitazione spesso necessario aggiustare
gradatamente i parametri di gioco in modo da adattarsi alle capacit del singolo paziente.
Spesso utilizzando questi giochi necessaria la presenza fisica del terapista, in quanto il
gioco non pensato per assistere e correggere lutente in caso di movimenti sbagliati, inoltre
non si considera neanche laspetto motivazionale, per cui il paziente, rischia semplicemente
di rimanere frustrato da un insuccesso, e pu rifiutarsi di continuare, oppure pu giocare
controvoglia, aumentando cos il rischio di errore.
Infine non viene fornita ai terapisti un interfaccia per lanalisi dei dati acquisiti durante il
gioco, riguardanti i movimenti effettuati, per cui non possibile monitorare eventuali
progressi o peggioramenti del paziente.
5 - PANDA 3D
Panda 3D un game engine, un framework open source per il rendering 3D e lo sviluppo di
videogiochi. Il nucleo scritto in C++ e le sue funzionalit possono essere estese utilizzando il
linguaggio di scripting Python.
Tra le features offerte ci sono:
-
Renderer 3D che utilizza sia le librerie Mircrosoft DirectX su Microsoft Windows, che le
librerie OpenGL, compatibili con vari sistemi operativi
Librerie per la gestione dellaudio
Un physic engine integrato (oltre a supportarne di pi complessi, come ad esempio lNvidia
Physx engine) che fornisce API per la gestione dei problemi pi comuni nello sviluppo dei
videogiochi, quali quello del collision detection.
Un sistema di effetti particellari dedicato
Un sistema per la gestione delle GUI, per la realizzazione ad esempio di menu di gioco, o
scritte o elementi grafici da mostrare sullo schermo durante la fase di gioco
Una semplice libreria PandaAI per una gestione basilare dellintelligenza artificiale nel
gioco, che fornisce ad esempio strumenti per la ricerca di un oggetto nello spazio di gioco,
levitare gli ostacoli, il trovare e seguire percorsi per spostarsi da un punto a un altro ecc.
La velocit con cui avviene questo ciclo, o meglio lintervallo di tempo trascorso tra due fasi di
disegno indica il frame rate del nostro gioco.
Initialize
Il loader: utilizzato per caricare qualsiasi tipo di gameObject allinterno del nostro gioco, dai
modelli 3d, alle textures, ai suoni, ecc.
- Il render: che si occupa, di tutto ci che grafica allinterno del gioco
- Il taskMgr: per la gestione dei vari task da eseguire allinterno del gioco
- La camera: ovvero la telecamera di default che delimita larea attualmente visibile del gioco
E diversi altri componenti.
Inoltre la classe ShowBase, ci fornisce accesso al metodo run(), che in maniera del tutto trasparente
avvia il loop di gioco.
Tutta la logica che concerne il disegno del singolo frame in Panda3D gestita da un grafo di nodi, la
cui radice loggetto render (o render2D nel caso di giochi bidimensionali); sar sufficiente
impostare un nodo generato dal caricamento (tramite loader) di un qualsiasi modello
tridimensionale come figlio delloggetto render (o come figlio di uno dei suoi figli), affinch il
renderer di panda3D renderizzi loggetto nella scena.
Ci che particolarmente utile in questo schema di gestione dei nodi ad albero, che possibile
impostare parentele tra i diversi oggetti, in tal modo sar ad esempio possibile muovere, scalare o
ruotare un singolo oggetto, non solo in funzione dellorigine della scena visualizzata, ma anche in
funzione del suo genitore.
Ultimo componente degno di nota, utile a descrivere macroscopicamente il funzionamento di un
gioco creato con panda3D, il taskManager.
9
Il TaskManager, implementa in parte il concetto che sta dietro al metodo update, descritto nel
capitolo precedente, si tratta di una struttura dati a cui si possono appendere funzioni, tali funzioni
verranno eseguite a ogni loop e serviranno per i pi disparati scopi, come ad esempio:
- Modificare la posizione della camera di gioco
- Modificare la rotazione del giocatore sulla scena
- Modificare qualsiasi parametro di gioco
- Aggiornare i punteggi
- Oppure semplicemente eseguire controlli per il debugging del gioco
Ecco un esempio di codice per realizzare una semplice animazione, di una sfera che ruota su se stessa
in panda3D.
from direct.showbase.ShowBase import ShowBase
from direct.task import Task
class FirstGame (ShowBase):
def __init__(self):
# Call superclass constructor
ShowBase.__init__(self)
# Set default camera position
base.disableMouse()
self.camera.setPos(0,-10,0)
# Load a model
self.myModel = loader.loadModel("smiley.egg");
# Reparent the model to render so that it will be displayed in the scene
self.myModel.reparentTo(render)
# Attach a task to taskManager to make the spin
self.taskMgr.add(self.spinTheBall, "Spin The Ball Task")
# TASKS
def spinTheBall(self, task):
# Rotate by 20 degrees per second
angleDegrees = task.time * 20.0
self.myModel.setHpr(angleDegrees, 0,0)
return Task.cont
#Instantiate and launch the game
game = FirstGame()
game.run()
Una volta compresa la struttura di base di un gioco realizzato con panda3D si passati alla
realizzazione di semplici giochi di test, nei quali si cercato di mappare un movimento su uno spazio
bidimensionale, come quello del mouse, allinterno della scena di un gioco 3D, scopo di tale test
stato quello di capire come effettuare il mapping dellinput ricevuto da un dispositivo esterno
allinterno del gioco.
Per questo tipo di test, sono stati utilizzati due approcci differenti:
-
Lutilizzo di una camera ortografica invece che proporzionale per inquadrare la scena, tale
approccio consente di mappare, senza distorsioni unarea allinterno di uno spazio
tridimensionale, su un piano.
- Lutilizzo della tecnica del Ray Casting, che permette di rilevare sfruttando il sistema di
collision detection integrato nel game engine, il punto sulla superficie nello spazio
tridimensionale sul quale ci si trova col puntatore del mouse.
Entrambe queste tecniche verranno descritte qui di seguito, ma per fare ci necessario prima
introdurre il sistema di collision detection in Panda3D.
10
CollisionSolids:
o CollisionSphere
o CollisionTube
o CollisionPlane
o CollisionPolygon
o CollisionRay []
Sono tutti i solidi, generalmente invisibili che costituiscono la collision geometry, se
volessimo ad esempio far collidere la palla rotante creata nel capitolo precedente con
qualche altro oggetto, allora dovremmo costruire una CollisionSphere invisibile attorno alla
nostra palla, delle stesse dimensioni della palla stessa, a quel punto su questo oggetto
riusciremmo a rilevare collisioni con altri collider.
11
CollisionHandlers:
Ce ne sono di diversi tipi in Panda3D, il pi semplice il CollisionHandlerQueue, che
sostanzialmente si occupa di rilevare tutte le collisioni avvenute nella scena di gioco in un
dato istante di tempo e inserirle in una coda, consultabile dal programmatore, che potr
estrarre le diverse collisionEntries una per volta e gestirle nel modo che ritiene pi
appropriato.
Il concetto che sta dietro a questa tecnica, quello di sostituire le lenti della camera che inquadra la
scena di gioco, che di default sono lenti a proiezione prospettica: che riprendono lambiente, come
farebbe una telecamera reale, o come farebbe locchio umano; con una lente ortografica, che
elimina di fatto gli effetti prospettici, mantenendo le linee di proiezione perfettamente parallele fra
di loro.
Leffetto ottenuto dallutilizzo di queste lenti, ovviamente, la perdita di percezione visiva della terza
dimensione, per cui ad esempio avvicinandosi o allontanandosi da un oggetto, esso non si
ingrandisce n si rimpicciolisce: questo ovviamente poco intuitivo, ma fornisce un grande
vantaggio, quello di poter scalare la scena inquadrata in modo da mappare in maniera diretta le
coordinate, del dispositivo di input su due dimensioni, nel nostro caso un mouse, allinterno della
scena di gioco.
Per testare questa tecnica stato realizzato un mini-gioco in cui una pallina doveva seguire il
puntatore del mouse allinterno di un percorso delimitato da dei muri laterali, quando la pallina
toccava uno dei muri si bloccava e il gioco terminava.
Analizziamo solo la parte di codice relativa al mapping del device allinterno della nostra scena:
Sappiamo che Panda3D mappa la posizione del mouse con dei valori decimali compresi tra -1 e 1 sia
sullasse delle ascisse che su quella delle ordinate, il nostro scopo dunque quello di montare le
lenti ortografiche, e regolare la dimensione della pellicola fino a quando anche le coordinate della
scena non sono comprese tra -1 e 1, in modo che a certe coordinate del mouse corrisponda il
medesimo punto nella scena.
stato definito un metodo setupCamera, che si occupa proprio di questo:
# This method prepare the camera
def setupCamera(self):
# since this is a 2d game, we use Orthographic lenses, this should avoid
# perspective related issues with the mouse
lens = OrthographicLens()
# we set the camera film size to fit 4/3 aspect ratio, x size is set to
# 2, because we want the coordinate system to be in the [-1, 1] range
lens.setFilmSize(2,1.5)# setFilmSize sets the camera film size in spatial
units
base.camNode.setLens(lens)
# With Orthographic lenses, camera height doesn't really matters
base.camera.setPos(0,0,10)
base.camera.setHpr(0,270,0)
Il metodo che setta la dimensione della pellicola, e quindi definisce la dimensione dellarea ripresa,
setFilmSize.
Una volta definita la dimensione dellarea ripresa si tratter solamente di convertire le coordinate
del mouse, comprese tra -1 e 1 in coordinate spaziali, comprese tra -1 e 1 sullasse delle ascisse e 0.75 e +0.75 sullasse delle ordinate (questo valore per le ordinate dipendente dallaspect ratio
della finestra di gioco,in questo caso 4/3), facciamo questo nel task del gioco che si occupa del
tracciamento del mouse.
La conversione che dobbiamo fare per lasse Y realizzata semplicemente sapendo che laspect ratio
della nostra finestra 4:3, per cui si tratter di prendere le coordinate del mouse ricevute dividerle
per 4 e moltiplicarle per 3, supponiamo ad esempio che il mouse si trovi in coordinate (0,1), allora
la nostra posizione nello spazio di gioco sar 1 / 4 = 0,25 * 3 = 0,75.
13
Ed ecco uno screenshot del gioco finito, da notare come la posizione della pallina corrisponda alle
attuali coordinate del puntatore del mouse:
prospettica delle lenti ortografiche, che non permette di avere percezione della profondit e quindi
della distanza tra la camera e i vari oggetti.
La tecnica del ray casting, ci viene in soccorso nel momento in cui abbiamo necessit di rilevare il la
posizione del mouse, o di altri dispositivi allinterno di una scena tridimensionale.
Il procedimento consiste nel rilevare la posizione del mouse nel suo sistema di coordinate (-1,1) e
proiettare ortogonalmente alla superficie di gioco un raggio (un collisionRay in panda3D) dalla
telecamera: il punto in cui avviene la collisione col terreno, sar il punto su cui si trova il mouse nella
scena.
stato realizzato, anche in questo caso, un gioco simile a quello precedente, ma, questa volta,
utilizzando lenti prospettiche e inquadratura non esattamente ortogonale alla superficie.
Analizziamo anche in questo caso le parti di codice utili al fine di comprendere il funzionamento di
questa tecnica:
Innanzi tutto, quello che dobbiamo fare preparare il collisionRay che andr proiettato sulla
superficie di gioco, si tratta di aggiungere semplice geometria di collisione:
15
Da notare, lultima istruzione, in cui viene impostato il nodo relativo al nuovo collisionRay come figlio
del nodo della telecamera: questo viene fatto perch proprio da essa che faremo partire il nostro
raggio.
A questo punto la classe collisionRay, ci consente comodamente di settare la posizione e
lorientamento del nostro raggio tramite un comodo metodo setFromLens():
Tale metodo setta lorigine del collisionRay su cui viene chiamato nel punto corrispondente alle
coordinate nel range [-1,1] sul piano focale minimo (ovvero sul piano pi vicino allosservatore, che
verr messo a fuoco dalla camera) e lo proietter, perpendicolarmente a questo piano, per una
lunghezza virtualmente infinita.
A questo punto, se riusciremo a rilevare una collisione tra il raggio e la nostra superficie, allora il
punto di collisione sul terreno, sar il punto del gioco su cui il giocatore ha cliccato, potremmo quindi
rilevare levento e gestirlo di conseguenza.
# This tasks checks the mouse status, and eventually sets the tracked WayPoints
def mouseMonitor(self, task):
# We check if the mouse is within the game window
if(base.mouseWatcherNode.hasMouse() == True):
mpos = base.mouseWatcherNode.getMouse()
# Shoot a ray from the camera towards the surface
self.pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())
base.cTrav.traverse(render)
if self.cHan.getNumEntries() > 0:
# This is so that we get the closest object
self.cHan.sortEntries()
pickedObj = self.cHan.getEntry(0).getIntoNodePath()
if(pickedObj.getName() == "Ground Collision Node"):
pickedObjPoint = self.cHan.getEntry(0).getSurfacePoint(render)
# etc. etc. [. . .]
Come possibile osservare dal codice, anche in questo caso, si utilizza un semplice il
CollisionHandlerQueue per la gestione delle collisioni: si proietta il raggio invisibile verso la superficie
con setFromLens, si chiama il metodo traverse() di collisionTraverser per rilevare le collisioni, se sono
state rilevate delle collisioni con il collisionNode della superficie, allora il punto di collisione ovvero:
16
pickedObjPoint = self.cHan.getEntry(0).getSurfacePoint(render)
il punto su cui lutente ha cliccato, secondo il sistema di coordinate del nostro gioco.
Una volta presa analizzate le possibili tecniche per la gestione di input da diversi device in Panda3D,
il lavoro s concentrato sullo studio delle potenzialit e la possibilit di integrare allinterno del
game engine, un dispositivo di input differente, il Leap Motion Controller.
17
on_init(): questa funzione viene chiamata quando il controller a cui il nostro listener
collegato, viene inizializzato: sar opportuno inserire qua tutta la logica di inizializzazione
della nostra classe
- on_connect(): questa funzione viene richiamata quando il dispositivo si connesso con il
software del leap motion ed pronto a inviare frame
- on_disconnect(): questa funzione esattamente opposta alla precedente, serve ad esempio
a gestire il caso in cui il dispostivo venga accidentalmente scollegato dal computer.
- on_exit(): questa funzione viene chiamata quando il nostro listener viene scollegato dal
controller attivo, qua implementeremo tutta la logica di deallocazione della nostra classe.
- on_frame(): questa la funzione pi importante, richiamata dallevento sollevato quando
viene ricevuto un nuovo frame dal dispositivo, allinterno di questa funzione dovremmo
compiere tutte quelle operazioni necessarie ad analizzare il singolo frame ricevuto, e ad
estrapolare tutte le informazioni necessarie, alla nostra applicazione finale, nel nostro caso,
il gioco costruito con panda3D, affinch essa risponda correttamente agli input dellutente.
Oltre alla classe Listener, la libreria proposta dal produttore del dispositivo, ci fornisce una serie di
classi che ci permettono di astrarre sui dati ricevuti dal dispositivo e di lavorare in comodit su aspetti
quali ad esempio, la posizione della normale del palmo della mano, il numero di dita rilevate, la
posizione della punta di un singolo dito, ecc.
Analizziamo quelle fondamentali:
-
18
La classe HAND: fornisce informazioni su una singola mano rilevata dal dispositivo, da
informazioni riguardo, la posizione del palmo, la direzione della normale, se si sta muovendo
e se s in quale direzione e con che velocit, quante dita della mano sono state rilevate ecc.
- La classe POINTABLE: fornisce informazioni analoghe a quelle della classe HAND, ma per il
singolo dito della mano, o per il singolo tool rilevato. Le classi FINGER e TOOL estendono la
classe POINTABLE.
Le classi disponibili nella libreria, sono molte di pi, ma queste sono quelle fondamentali.
bene inoltre spendere qualche parola su un altro elemento fondamentale dellhand tracking: le
gestures.
Per gesture si intende un tipo di movimento molto comune, ricorrente o che ha un intenzione
particolare che pu essere compiuto dallutente.
Il software del Leap in grado di riconoscere alcune di queste gestures, quali ad esempio la rotazione
della punta del dito formando un cerchio: Gesture.Circle, oppure la traslazione di un dito sul piano
orizzontale o verticale: Gesture.Swipe, o il movimento che simula la pressione di un pulsante
Gesture.KeyTap o Gesture.ScreenTap, per ognuna di queste gesture possibile definire una
serie di parametri per la validazione del tipo di movimento da rilevare, quali ad esempio velocit,
ampiezza e direzione del movimento per personalizzare il tipo di risposta del dispositivo in funzione
della nostra applicazione.
Al momento le librerie del Leap non forniscono strumenti ufficiali per la realizzazione di gestures
personalizzate, nonostante ci, in seguito verr descritto un test realizzato in panda3D per il
rilevamento della gesture Grabbing ovvero della presa di un oggetto col Leap Motion Controller.
Prima per descriviamo il primo test realizzato con il Leap Motion Controller:
19
Come stato detto in precedenza, la callback on_frame viene richiamato dal software di tracking
fornito col leap motion controller, ogni volta che il dispositivo trasferisce un frame al computer:
per prima cosa utilizziamo il metodo frame della classe Controller per recuperare listanza del frame
corrente.
A questo punto con un semplice controllo sulla lista dimensione della lista di oggetti Hand nel frame
corrente decidiamo se stampare la scritta Metti la mano sopra il leap!, o meno.
Nel caso in cui la mano sia stata rilevata dal dispositivo, accediamo allattributo hands della classe
frame, che sostanzialmente una lista di mani, e estraiamo il primo elemento, ovvero la prima mano
rilevata.
Ricaviamo direzione e normale del palmo, e ricaviamo attraverso dei semplici attributi gli angoli di
inclinazione della mano (pitch, roll, yaw) che convertiamo da radianti a gradi, fatto ci
stampiamo le stringhe pi appropriate.
Ecco un esempio di output del programma:
20
Realizzato questo primo test, si cercato di utilizzare il leap motion controller allinterno di panda3D:
stato realizzato in Blender un semplice modello tridimensionale di un aeroplano, che, una volta
importato in panda3D, stato programmato per muoversi seguendo i dati rilevati a posizione e
inclinazione della mano sul leap motion controller.
Rispetto al test precedente, la differenza dovuta al fatto che questa volta non bastato limitarsi a
stampare i valori di inclinazione della mano ottenuti a ogni frame, ma si dovuto mantenere in
memoria i dati rilevati in un dato frame, fornendo i metodi necessari al recupero degli stessi
allinterno di panda3D.
Una delle difficolt riscontrate nellutilizzo del leap allinterno del loop di gioco stata quella di
sincronizzare linput ricevuto dal dispositivo con la frequenza di aggiornamento del gioco stesso. Il
problema dovuto al fatto che il leap, generalmente, genera frame a velocit diversa rispetto a
quella necessaria al completamento del ciclo di aggiornamento del gioco: per tanto, se ad esempio,
si cerca di accedere a un parametro impostato allinterno del leapListener in due punti del codice
nella nostra procedura di aggiornamento di gioco, si rischia di ottenere valori diversi: perch pu
accadere che nel tempo che abbiamo impiegato per arrivare alla seconda istruzione di accesso ai
dati di movimento della mano, il leap abbia generato un nuovo frame, e il dato precedente stato
sovrascritto.
Si pu affrontare tale problema in due modi:
-
Sincronizzando il frame rate del gioco e del leap motion controller, cosa non banale, in
quanto bisognerebbe richiedere al gioco un frame rate costante, cosa che penalizzerebbe
calcolatori poco potenti che dovranno eseguire il gioco.
- Organizzando il codice del gioco in modo da richiedere il valore estratto dal leap una sola
volta per ciclo, e conservare quel valore fino a loop completato, eliminando cos qualsiasi
problema di concorrenza alla fonte.
stata adottata questa seconda strategia, analizziamo le parti fondamentali del codice in questo
secondo test:
21
Innanzi tutto dovremo scrivere un listener personalizzato in grado di ottenere per ogni frame i gradi
di rotazione della mano intorno ai tre assi, inoltre avremo bisogno della posizione normalizzata del
palmo della mano rispetto allarea scansionata dal dispositivo lungo lasse Z, questo ci permetter
di settare la velocit dellaereo in funzione della posizione della mano: pi la mano vicina al bordo
superiore dellarea scansionata, pi la velocit sar alta, pi si avviciner al bordo inferiore, pi la
velocit sar bassa.
Il nostro listener estender ovviamente la classe Leap.Listener, concentriamoci su due dei metodi da
implementare metodi:
Il metodo on_init() , come abbiamo visto prima, il luogo in cui bisogna implementare tutta la logica
di inizializzazione del nostro listener: poich dovremo poter accedere dal gioco ai dati sulla posizione
della mano, dovremo dichiarare e inizializzare dei campi ad accesso pubblico che conterranno i dati
rilevati a ogni frame.
def on_init(self, controller):
# Questo contiene il frame precedente, serve per rilevare le traslazioni
# della mano
self.prevFrame = None
self.handPitch = None
self.handRoll = None
self.handYaw = None
self.translationStatus = None
self.reversed_normalized_palm_position = None
print("Listener inizializzato...")
Come possibile osservare, i dati ottenuti riguardano beccheggio (pitch), rollio (roll) e
imbardata, (yaw) della mano.
Concentriamoci ora sul metodo pi importante del listener, on_frame(): come nel test precedente
dovremo accedere ai dati riguardanti la posizione rilevata della mano sopra il dispositivo e
aggiornare le i campi impostati dal metodo di inizializzazione.
I dati relativi alla rotazione della mano, vengono convertiti, anche in questo caso da radianti a gradi
per moltiplicando il loro valore per la costante Leap.RAD_TO_DEG = (180/) con lunico scopo di
semplificarne lutilizzo allinterno di panda3D.
Degno di nota anche il metodo normalized_point() della classe InteractionBox (classe che
rappresenta lattuale area scansionata dal dispositivo), tale metodo richiamato con argomento
hand.palm_position restituisce la posizione normalizzata della mano nello spazio in un sistema di
coordinate ([0,1],[0,1],[0,1]) dove 0 il punto allestremit inferiore dallarea nel range attuale del
dispositivo e 1 il punto allestremit superiore per ognuno dei 3 punti cardinali dello spazio.
22
Costruito il nostro listener personalizzato, vediamo ora come utilizzare i dati ottenuti dal listener per
far muovere laeroplano in panda3D. Innanzi tutto in fase di inizializzazione del gioco necessario
istanziare il listener, facciamo ci nel costruttore della classe World, il cui scopo quello di creare il
nostro mondo di gioco, chiamando la funzione controllerSetup cos implementata:
def controllerSetup(self):
# Istanzio il listener e il controller
self.leapListener = MyLeapListener()
self.leap = Leap.Controller()
# Assegno il listener al controller
self.leap.add_listener(self.leapListener)
A questo punto accedendo alla variabile self.leapListener avremo accesso a tutti i campi pubblici
riguardanti linclinazione e la posizione della mano nello spazio impostati nel punto precedente.
Copia di questo oggetto viene quindi passata al costruttore della classe Player, il cui scopo quello
di visualizzare e gestire il movimento del giocatore.
23
Come possibile osservare dal codice del Move Player Task creato nella classe Player, la gestione
dellinput affrontata in maniera piuttosto semplice:
Per prima cosa si accede allinputListener e si salvano in variabili locali i valori di inclinazione della
mano rilevati dal dispositivo, in questo modo in caso il listener cambiasse tali valori perch il
dispositivo ha generato un nuovo frame, avremmo comunque dei dati consistenti per il nostro ciclo
di aggiornamento della posizione dellaeroplano.
24
A questo punto tramite semplici operazioni matematiche si convertono i dati ricevuti dal dispositivo
in dati utilizzabili allinterno del gioco, per fare un esempio, possibile osservare come i gradi di
rollio e imbardata dellaeroplano arrivino dal dispositivo con segno opposto a quello utilizzato
allinterno di panda3D, in questo caso tali valori vengono semplicemente moltiplicati per -1 prima di
essere utilizzati per il movimento del giocatore.
In realt questi dati, una volta convertiti potrebbero essere utilizzati direttamente come parametri
del metodo setHpr(H,P,R) della classe NodePath di panda3D per far corrispondere con un mapping
diretto alla posizione della mano lesatta posizione dellaeroplano.
Il problema di questo approccio che i movimenti dellaereo risulterebbero troppo repentini per
essere realistici in caso di movimenti molto rapidi della mano dellutente; il codice scritto qua sopra
cerca di ovviare al problema in maniera molto semplice: lattuale posizione della mano non la
posizione attuale dellaereo, ma la posizione in cui laereo deve arrivare.
Come possibile osservare, laereo ha delle velocit di rotazioni impostate per ogni asse: il valore
ricevuto in input langolo a cui laereo deve arrivare (ad esempio targetRoll), se la rotazione attuale
dellaereo inferiore o superiore rispetto allangolo obiettivo allora bisogna ruotare laereo verso
tale angolo della sua velocit.
In questo modo si ottiene un movimento molto pi fluido e naturale.
particolare se si vogliono sviluppare giochi per la riabilitazione della mano, dove, a seconda della
disabilit da riabilitare, necessario rilevare movimenti diversi.
Si voluto perci esplorare la possibilit di rilevare movimenti non preimpostati, e il movimento
scelto per questi test un movimento molto comune, quello di Grabbing o presa di un oggetto.
Innanzi tutto bene spendere due parole sul perch rilevare questo movimento non un operazione
cos banale con il leap motion controller, per fare ci ci avvaliamo di uno strumento molto comodo
fornito con il software di gestione del dispositivo il visualizer:
Si tratta di un software che si interfaccia col dispositivo e ci fornisce una rappresentazione
tridimensionale di ci che il dispositivo sta rilevando nello spazio intorno a se in tempo reale.
Come possibile osservare dallimmagine, nel caso della presa, il dispositivo non riesce a rilevare la
presenza delle dita nel momento in cui esse si avvicinano troppo fra di loro: questo dovuto al fatto
che il dispositivo osserva lambiente da un'unica posizione: dal basso, ci significa che nel momento
in cui le dita cominciano a essere troppo vicine fra loro e sovrapposte, dal suo punto di osservazione,
al palmo, esso non pi in grado di rilevarle e di conseguenza la lista fingers della classe hand del
frame corrente indica 0 dita presenti.
Il problema che a questo punto ci si pone come rilevare un evento come il grabbing, basato
strettamente sulla distanza delle dita (che deve essere a ogni frame sempre minore) con un
dispositivo in cui le dita superato un certo angolo spariscono?
Ovviamente non si pu rilevare con certezza assoluta, ma si pu pensare alla seguente osservazione
statistica:
Se per un certo numero di frame, il dispositivo ha rilevato 5 dita, e magari per ognuno di essi, le
punte delle dita si stavano avvicinando e improvvisamente a un certo frame, tutte le dita sono
scomparse (e rimangono assenti anche nei frame successivi), allora c una buona probabilit (e si
tratta di una probabilit, non di una certezza) che sia stata effettuata una gesture di grabbing.
Scopo del test effettuato, era dunque scrivere una sotto-classe del Leap-Listener in grado di
segnalare, utilizzando questa tecnica di monitoring delle dita rilevate frame per frame nel tempo,
leventualit di una possibile gesture di presa, e il conseguente rilascio.
Per prima cosa bisogna identificare un modo per poter segnalare allutente leffettivo rilevamento
della gesture di grabbing, si potrebbe studiare un sistema ad eventi per cui a ogni gesture valida
rilevata si potrebbe sollevare un evento che potremmo gestire allinterno della nostra applicazione.
26
Un alternativa meno complessa, vista la semplicit del nostro test, quella di utilizzare un campo
pubblico del nostro listener (self.grab) di tipo booleano, che viene settata a true quando lutente ha
effettuato una presa e a false quando non lha effettuata oppure lha rilasciata.
Ci serviamo inoltre di una lista di interi che utilizzeremo per mantenere i dati relativi al numero di
dita rilevate in un certo numero di frame trascorsi.
Definiremo quindi un metodo catch_grab(hand) che verr chiamato allinterno del metodo
on_frame per rilevare se sulla mano in argomento stata rilevata la gesture di grabbing, e in caso
affermativo setter la variabile che abbiamo definito prima a true.
Vediamo come si comporta il metodo catch_grab(hand):
def catch_grab(self, hand):
# If we have at least one element in our finger_story list
if(not self.__finger_count_story is None):
# Let's sort the finger list, we just need to get the maximum finger
# number in the last 5 frames
self.__finger_count_story.sort()
max_finger =
self.__finger_count_story[len(self.__finger_count_story) - 1]
if(max_finger >= len(hand.fingers)):
# If the maximum finger number from the last 5 frames is 5, and
# now we got a 0, than this is a grab
if(max_finger == 5 and len(hand.fingers) == 0):
self.grab = True
print("GRAB")
# If grab is set to true, but we have five fingers in our
# current frame, than we should release the grab
if(self.grab == True and len(hand.fingers) == 5):
self.grab = False
print("GRAB RELEASED")
# This if-else statement simply updates the list
if(len(self.__finger_count_story) < 5):
self.__finger_count_story.append(len(hand.fingers))
else:
self.__finger_count_story.pop(0)
self.__finger_count_story.append(len(hand.fingers))
# If we didn't have any element in our finger story list,
# simply add one
else:
self.__finger_count_story = [len(hand.fingers)]
In questo caso la nostra lista: __finger_count_story contiene fino a 5 element0,i ovvero il numero
delle dita rilevate negli ultimi cinque frame, calcoliamo il numero massimo di dita rilevate in questi
frame: questo viene fatto per eliminare alcune rilevazioni fasulle dovute a eventuali elementi di
disturbo nellambiente scansionato dal leap, che possono per qualche frame far rilevare meno dita
di quelle che sono realmente sul dispositivo, in questo modo consideriamo il numero massimo di
dita rilevate in un dato frame con un valore attendibile del numero di dita che fino al frame scorso
erano effettivamente sul dispositivo.
A questo punto, come possibile osservare dal codice si tratta, semplicemente di confrontare il
numero ottenuto prendendo il massimo dalla lista, con il numero di dita rilevate nel frame corrente
e in particolare abbiamo che:
SE avevamo 5 dita fino al frame scorso e il numero di dita rilevate in questo frame 0 allora lutente
sta effettuando una presa.
Inoltre:
27
SE avevamo gi rilevato un evento di grabbing, quindi self.grab settato a True e ora rileviamo di
nuovo 5 dita sopra il dispositivo, allora la presa stata rilasciata.
A questo punto se in un ipotetico gioco Panda3D, in cui vogliamo rilevare la presa di un oggetto,
andiamo a controllare lo stato della variabile inputListener.grab, possiamo sapere se lutente sta
cercando di prendere qualcosa o meno.
Vediamo un esempio di esecuzione in un semplice gioco scritto in panda3D in cui utilizzando le
tecniche di raycasting viste in precedenza rileviamo su una superficie la posizione della mano, se
chiudiamo la mano scatenando levento di grabbing, allora una pallina sulla superficie comincer a
seguire la nostra mano:
28
Le dita impiantate sulla mano dei giovani pazienti, sono spesso troppo piccole per essere
rilevate con precisione dal dispositivo: spesso, il software di visualizzazione del leap motion
controller con cui abbiamo effettuato alcuni test, mostra unicamente il palmo della mano,
ci rende praticamente impossibile realizzare qualsiasi tipo di esercizio che coinvolga le dita
della mano.
Il Leap Motion Controller certamente uno strumento di facile utilizzo, molto immediato: si
mette la mano sopra il dispositivo e succede qualcosa sullo schermo; per richiesta una
certa coordinazione mano-occhio che non da dare per scontata per utenti con meno di 5
anni: necessario infatti operare con la mano sopra al dispositivo, mentre allo stesso tempo
si osserva uno schermo che non si trova sotto al dispositivo, questo obbliga lutente a
operare sul leap senza guardarlo, il che porta spesso i bambini a spostarsi al di fuori del
raggio dazione del dispositivo rendendo non fluido linput del programma e difficile, e poco
preciso il rilevamento dei dati sul movimento effettuato.
Questo fa s che un sistema basato su leap non sia cos facile da istallare e utilizzare in pratica.
Inoltre:
- Un problema di attrezzature: il Leap Motion Controller non funziona da solo, ha bisogno
almeno di un PC con sufficiente capacit hardware da gestire sia il software del Leap, che il
videogioco riabilitativo che lo utilizza. Inoltre, essendo lo strumento da realizzare studiato
per la riabilitazione a casa, necessaria una fase di configurazione e installazione dei
software relativi al Leap e al gioco, operazione che non sicuramente alla portata di
chiunque e che richiederebbe del personale dedicato, in grado di fornire il minimo di
assistenza necessaria a chi deve configurare lambiente.
- Un problema di costi: a differenza di molti altri dispositivi, come smartphone o tablet,
difficilmente un paziente possieder a casa un Leap Motion Controller, esso dovrebbe quindi
o essere fornito dalla struttura sanitaria o acquistato direttamente dal paziente, generando
un problema di costi, che si voluto evitare, quanto meno nella prima fase di
sperimentazione.
Il Leap Motion Controller rimane comunque uno strumento assolutamente valido per la
riabilitazione della mano in pazienti pi grandi, o per la riabilitazione cognitiva: ad oggi, infatti, il
dispositivo stato gi parzialmente integrato allinterno del Game Engine IGER, per quanto riguarda
il posizionamento del palmo e il tracciamento della posizione di un singolo dito o di un tool e andr
ad aggiungersi alla lista dei dispositivi supportati dalla piattaforma REWIRE.
29
Abbiamo innanzi tutto potuto notare come i giovani pazienti abbiano gi sviluppato un
altissima dimestichezza nellutilizzo di questi dispositivi con applicazioni commerciali,
questo semplifica di molto la fase di apprendimento nellutilizzo dellapplicazione che verr
realizzata.
- Spesso i pazienti, o le loro famiglie, sono gi in possesso di uno di questi dispositivi, vista la
loro diffusione, ci permette di abbattere i costi.
- Inoltre i dispositivi multi-touch permettono di rilevare pi dita contemporaneamente sullo
schermo, il rilevamento molto preciso, e difficilmente le dita scompariranno senza che
lutente le sollevi realmente dal dispositivo.
Si cercato, come vedremo nel prossimo capitolo, di sviluppare unapplicazione che fosse
compatibile con il maggior numero di dispositivi, pertanto, lintero gioco stato sviluppato come
applicazione web in HTML5 e Javascript.
30
Le capacit delle librerie grafiche per web per quanto molto avanzate, sono ancora lontane
dallessere equiparabili a quelle disponibili per i singoli dispositivi, per tanto sar difficile realizzare
videogiochi particolarmente complessi per browser web.
Ad oggi esistono una serie di difficolt per quanto riguarda la riproduzione audio in videogiochi
basati sul web che vengono utilizzati su dispositivi mobili.
Inoltre lintera piattaforma sviluppata: gioco + pannello di configurazione + pannello di analisi ha
bisogno di funzionare su un webserver centralizzato, questo deve essere installato, gestito e
mantenuto direttamente dalla clinica che lo utilizza, tale gestione richiede personale competente e
quindi un potenziale costo per la struttura.
Detto ci, si comunque ritenuto opportuno utilizzare, almeno in questa fase preliminare questa
architettura, per rendere disponibile ai clinici e ai pazienti nel pi breve lasso di tempo possibile un
prototipo di piattaforma da testare e valutare per sviluppi futuri.
31
applicazioni per la gestione di fotografie, per la scalatura delle immagini: allontanando le dita si
zoomma ovvero si ingrandisce limmagine, avvicinandole si restringe.
Abbiamo quindi pensato di realizzare un semplice gioco, la cui protagonista una mongolfiera che
sale quando le dita di avvicinano, e scende quando si allontanano: scopo del gioco fare decollare
la mongolfiera, farla volare in un cielo pieno di uccellini da evitare e monetine da prendere, e quindi
farla atterrare dolcemente a destinazione.
Insieme al gioco stata sviluppata un interfaccia per la configurazione dei parametri relativi al
singolo paziente, e per lanalisi delle singole sessioni di gioco e della storia delle sessioni effettuate
Nei capitoli che seguiranno verranno descritte tutte le fasi di sviluppo e le problematiche affrontate
nello sviluppo di questo videogioco, e dellintera piattaforma per i terapisti.
32
Come possibile notare, sufficiente scrivere del testo allinterno del tag canvas per gestire
leventuale accesso allapplicazione da parte di un client dotato di browser non in grado di gestire il
tag canvas.
A questo punto possiamo preparare la classe principale del nostro gioco, il cui scopo sar quello di
inizializzare il gioco, e avviare il loop di update e draw che ci permetter di animarlo!
7.c ii - INIZIALIZZARE E LANCIARE IL GIOCO
Questa fase particolarmente importante, attraverso il costruttore della nostra classe game,
recuperiamo lelemento canvas definito allinterno della nostra pagina HTML attraverso il suo id,
recuperiamo loggetto context, che utilizzeremo allinterno del gioco per disegnare fisicamente le
nostre sprites, scriveremo la vera e propria logica di inizializzazione del gioco, per cui il caricamento
delle singole sprites, immagini, inizializzeremo tutte le variabili di gioco, e instanzieremo tutti i nostri
game object.
Definiremo i metodi per laggiornamento e il disegno dei singoli frame, solo a questo punto
attraverso un metodo run, che la nostra classe dovr fornire, avvieremo il loop di gioco:
/* CONSTANTS */
var UPDATE_FREQ = 15;
function Game(canvasID){
/* Game Variables */
this.canvas = document.getElementById(canvasID);
this.ctx = this.canvas.getContext('2d');
/* Public Methods */
this.update = function(self){
// GAME UPDATE LOGIC HERE
console.log("update");
self.draw(self);
}
this.draw = function(self){
// FRAME DRAW LOGIC HERE
console.log("draw");
}
this.run = function(){
// This frequency should give us a framerate slightly higher
// than 60, fps:
// by increasing this value we reduce the frame-rate making
//the game slower but easier to run
// on slower devices
var self = this;
setInterval(function(){self.update(self)}, UPDATE_FREQ);
}
}
33
Elemento degno di nota allinterno del codice qui sopra la frequenza in millisecondi passata al
metodo setInterval(), essa ci permette, in generale, di regolare il frame-rate del nostro gioco:
Per come stato settato nellesempio qui sopra, e per come stato impostato il sistema, il metodo
update viene richiamato ogni 15 millisecondi, garantendo un frame rate di un po pi di 60 frame
per secondo.
Ovviamente incrementando questo valore, si riduce il numero di frame al secondo, rallentando il
gioco ma rendendolo giocabile anche su dispositivi meno performanti.
Di seguito la semplice formula per il calcolo dei millisecondi necessari a ottenere il frame-rate
desiderato:
1000 =
Per cui ad esempio, per ottenere un frame rate di 25 frame per secondo, la frequenza di
aggiornamento sar di:
1000 25 = 40
A questo punto ben chiaro che sar sufficiente istanziare in una variabile loggetto game e
richiamare il metodo run() per lanciare il gioco.
<script type="text/javascript">
var game = new Game("myCanvas");
game.run();
</script>
In poche righe di codice si cos ricreato in ambiente web, il loop di gioco, vedremo ora nei prossimi
capitoli come a partire da questo semplice schema si sia realizzato il gioco HotAirBalloon,
analizzando man mano, tutti i problemi che sono stati affrontati nello sviluppo.
34
fillRect() disegna un rettangolo pieno con lo stile specificato nella propriet fillStyle
strokeRect() disegna un rettangolo vuoto, con il bordo nello stile specificato nella
propriet strokeStyle
- In modo analogo funzionano i metodi: fillArc() strokeArc(), fillText()
Vi sono poi una serie di metodi e attributi per modificare direttamente il contesto:
METODI DI TRASFORMAZIONE
- scale(): scala lintero contesto ai valori passsati come parametri
- rotate(): ruota lintero contesto allinterno del canvas
- translate(): trasla il contesto, ovvero sposta in nuove coordinate il punto di origine 0,0
Attenzione che metodi come questi non vanno a operare direttamente sulloggetto da disegnare,
ma sullintero contesto, questo significa che, ad esempio, dopo avere utilizzato scale, tutti gli
elementi che verranno disegnati successivamente utilizzeranno la nuova scalatura impostata.
Per ovviare a questo problema, sono stati implementati altri due metodi che permettono di salvare
e ripristinare il contesto precedente, ripristinando, ad esempio la scalatura originale.
Ora che conosciamo gli strumenti basilari per disegnare allinterno del nostro canvas, bene
spendere qualche parola sul sistema di coordinate utilizzato per posizionare i disegni allinterno di
questo contenitore:
[IMMAGINE]
Come possibile osservare il punto di origine della nostra tela si trova nellangolo superiore
sinistro, la distanza da questo punto misurata in pixel: sia sullasse X che sullasse Y ci fornisce un
metodo univoco per posizionare i nostri oggetti sulla scena.
Ad esempio se volessimo disegnare un pallino qualcosa esattamente nellangolo superiore destro di
un canvas di 800x600px, allora posizioneremo il nostro pallino nel punto: (800,0).
Creare un oggetto di tipo immagine, vuoto allinterno del DOM html, questo in javascript si
pu fare sfruttando la classe standard Image
Caricare effettivamente limmagine da file, che risiede sul server, e impostarla come
sorgente, dellimmagine appena creata
Scalare limmagine per entrare comodamente allinterno del nostro contenitore
35
Allinterno del costruttore della nostra classe game, creiamo limmagine e le assegniamo la
sorgente.
Allinterno del metodo Draw a questo punto dobbiamo fare in modo che a ogni frame, venga
cancellato il contenuto della scena al frame precedente, e ridisegnato il contenuto della scena
relativa al frame corrente:
36
this.draw = function(self){
// FRAME DRAW LOGIC HERE
self.ctx.clearRect(0,0,window.innerWidth, window.innerHeight);
self.ctx.beginPath();
self.ctx.drawImage(self.takeoffbg ,0,0,
window.innerWidth, window.innerHeight);
}
Come possibile osservare, il metodo clearRect() ci serve per pulire la scena: esso infatti elimina
qualsiasi contenuto grafico presente nellarea definita dal rettangolo passato come argomento.
Il metodo beginPath() un metodo di servizio che permette di comunicare al context linizio di un
nuovo tratto da disegnare.
Il metodo drawImage(), infine, serve per disegnare la nostra immagine, prende come parametro
limmagine da disegnare, le coordinate da cui iniziare il disegno, e la larghezza e altezza desiderata.
Il tutto cos scritto, sembra funzionare a dovere e produce, quanto meno in locale, quanto mostrato
nellimmagine seguente:
C per purtroppo un problema nascosto in quanto scritto, dovuto alla modalit in cui vengono
caricate le immagini allinterno di una pagina web.
7.e i - CARICAMENTO ASINCRONO E PRELOADING DELLE SPRITES.
In genere il caricamento di immagini attraverso javascript avviene in maniera asincrona: questo
significa che quando noi comunichiamo di recuperare limmagine sprites/bg.jpg dal server, viene
lanciato il caricamento del file, quindi il download dellimmagine dal server alla macchina client, ma
lesecuzione del codice, non viene interrotta, questo vuol dire che spesso ci si ritrover a eseguire
listruzione successiva a quella di caricamento, prima che il client sia realmente in possesso
dellimmagine richiesta.
37
ASYNC
LOAD IMAGE
ACCESS IMG
DOWNLOAD
ERROR
NEXT ISTR
DATA
READY
Definiremo una variabile: takeOffBgReady, booleana, che sar impostata di default a false,
e che indicher quando il nostro sfondo sar pronto per essere disegnato.
Assegneremo una funzione come callback allattributo onload del nostro sfondo, il cui unico
scopo sar settare a true la variabile definita prima.
38
Il risultato sar come quello visto in precedenza, ma adesso saremo protetti da eventuali problemi
dovuti a immagini non pronte.
A questo punto non ci resta che caricare unulteriore sprite per la mongolfiera e posizionarla nella
parte centrale inferiore della nostra scena in modo che sia pronta al decollo.
Il disegno della mongolfiera potrebbe essere affrontato in maniera analoga a quello dello sfondo,
ma in questo caso stiamo preparando un oggetto che verr fatto muovere sullo schermo dal
giocatore, chiamiamo questo oggetto per comodit GameObject, sar bene quindi pensare a una
nuova classe: Player, che fornisca propriet e metodi adatti a modificare posizione, scalatura ecc.
in maniera comoda allinterno del metodo update del nostro gioco, oltre che a disegnarlo
semplicemente
In particolare avremo bisogno di due propriet, o campi, pubblici che indichino la posizione del
nostro oggetto sul canvas, possiamo chiamarli pos_x e pos_y, facile intuire come variando nel
metodo update del nostro loop di gioco questi valori, e usando queste propriet allinterno del
metodo ctx.drawImage, si possa facilmente muovere a piacere la mongolfiera sullo schermo.
Vediamo la classe un po pi nel dettaglio:
39
function Player(x,y){
var balloonReady = false;
var balloon = new Image();
balloon.src = "sprites/balloon.png";
balloon.onload = function(){
balloonReady = true;
}
// Public properties
this.pos_x = x;
this.pos_y = y;
this.scale = .5;
// Public methods
this.draw = function(ctx){
// Draw only if sprite is ready
if(balloonReady === true){
// SAVING OLD CONTEXT
ctx.save();
// New draw
ctx.beginPath();
ctx.drawImage(balloon, this.pos_x, this.pos_y,
balloon.width * this.scale, balloon.height * this.scale);
// RESTORING OLD CONTEXT
ctx.restore();
}
}
}
Come possibile osservare, la sprite del giocatore viene gestita in maniera analoga a quella dello
sfondo, viene impostata una variabile balloonReady e inizializzata a false, viene richiesto il
caricamento dellimmagine, e solo a caricamento completato tale variabile viene settata a true.
La classe giocatore, cos come labbiamo definita, prende due parametri, ovvero le coordinate iniziali
su cui disegnare la nostra mongolfiera, fornisce inoltre:
-
Tre propriet pubbliche, pos_x, pos_y per modificare la posizione della mongolfiera in
runtime, e scale, per modificare in runtime anche la dimensione della mongolfiera.
- Un metodo per il disegno della mongolfiera, che prende come parametro la variabile
contenente il graphic context corrente: salva, il contesto attuale, disegna la nostra
mongolfiera, nella posizione desiderata e della dimensione corretta, dopo di che ripristina il
contesto.
Lutilizzo di questa classe particolarmente semplice, inseriremo nel costruttore del nostro gioco,
una variabile player che ospiter unistanza della nostra classe Player, e allinterno del nostro
metodo draw, chiameremo il metodo draw della classe Player, per disegnare il nostro giocatore.
this.player = new Player(0,0);
40
this.draw = function(self){
// FRAME DRAW LOGIC HERE
self.ctx.clearRect(0,0,window.innerWidth, window.innerHeight);
self.ctx.beginPath();
// Draw the background
if(takeoffBgReady === true){
self.ctx.drawImage(self.takeoffbg ,0,0,
window.innerWidth, window.innerHeight);
}
// Draw Player by calling its own draw method!
self.player.draw(self.ctx);
}
A questo punto la mongolfiera verr disegnata a partire dallangolo superiore sinistro del canvas, noi
vorremmo per posizionarla sul terreno al centro dello schermo, in modo che sia pronta a decollare.
Per fare ci dovrebbe essere sufficiente impostare le variabili pos_x e pos_y come segue:
player.pos_x = window.innerWidth / 2 - player.getWidth() / 2;
player.pos_y = window.innerHeight - player.getHeight() - 30;
Ci si propone per nuovamente un problema dovuto al caricamento asincrono della sprite della
mongolfiera.
Sapendo che i metodi getWidth() e getHeight() restituiscono la dimensione effettiva della nostra
sprite sullo schermo secondo la formula:
=
E
=
Nel caso in cui posizionassimo le istruzioni scritte sopra, subito dopo listruzione di creazione del
giocatore:
this.player = new Player(0,0);
Potremmo trovarci nella situazione in cui limmagine non sia ancora stata scaricata completamente
dal client, e quindi il valore SPRITE WIDTH non sia ancora stato settato, per tanto il valore restituito
dai nostri due metodi sarebbe 0, e la mongolfiera non verrebbe posizionata correttamente.
Bisogna quindi aspettare che limmagine sia pronta, prima di settare la posizione iniziale del
giocatore, il problema che si pone a questo punto quanto aspettare?
Potremmo monitorare, allinterno di un ciclo, una variabile che indica lo stato del giocatore
(POLLING), oppure utilizzare il sistema di eventi di javascript e lanciare un nuovo evento
personalizzato dalla callback che viene richiamata onload dello sprite, che abbiamo visto in
precedenza, in cui comunichiamo a un eventuale listener, la possibilit di utilizzare in sicurezza i
metodi delloggetto player.
stata adottata questa seconda opzione:
41
Per prima cosa necessario modificare come segue la funzione di callback chiamata al caricamento
completo dellimmagine:
var self = this;
balloon.onload = function(){
balloonReady = true;
var playerReadyEvent = new CustomEvent(
"playerReady",
{
detail: {
message: "Player is Ready",
player: self
},
bubbles: true,
cancelable: true
}
);
document.dispatchEvent(playerReadyEvent);
}
A questo punto, allinterno del costruttore della classe listener, sar sufficiente predisporre un
listener appropriato, che, al rilevamento dellevento playerReady chiamer una funzione che si
occuper di riposizionare la mongolfiera sullo schermo.
// Wait for the player to be ready, than set its position:
document.addEventListener("playerReady", function(e){
var player = e.detail.player;
player.pos_x = window.innerWidth / 2 - player.getWidth() / 2;
player.pos_y = window.innerHeight - player.getHeight() - 30;
}, false);
Abbiamo quindi descritto, come allinterno del gioco stato gestito il problema del preloading delle
sprites, e del caricamento asincrono dei contenuti, per qualsiasi altro elemento utilizzato allinterno
del gioco stato utilizzato il medesimo approccio.
Al momento abbiamo caricato la mongolfiera del giocatore, e labbiamo posizionata nella posizione
iniziale, ecco il risultato:
42
Quello che verr affrontato ora uno dei temi centrali dellintero progetto, la gestione dellinput
dellutente attraverso lo schermo touch screen del dispositivo utilizzato.
Touchstart, scatenato nel momento in cui lutente tocca la superficie e genera almeno un
punto di contatto touch point, tali punti sono elencati allinterno delloggetto che
rappresenta levento generato.
Touchmove, scatenato quando un utente sposta, trascinandolo un touch point sulla
superficie.
43
Touchend, scatentato ogni qualvolta, lutente rimuove un touch point dalla superficie (ad
esempio solleva un dito)
- Touchcancel, da non confondere con touchend, scatenato quando uno dei punti di
contatto esce dal DOM della pagina: ad esempio il dito si spostato dalla pagina alla barra
degli strumenti del browser, oppure comparso un pop-up sullo schermo
- Touchenter, scatenato quando uno dei punti di contatto entra in un elemento specifico del
DOM
- Touchleave, scatenato quando uno dei punti di contatto esce da un elemento specifico del
DOM.
Quello che bisogner fare innanzi tutto allinterno del nostro gioco, al fine di rilevare le dita sullo
schermo, sar quindi associare gli opportuni handlers per gli eventi touchstart, touchmove e
touchend rilevati sul nostro canvas, contare il numero di dita rilevato e accedere alla loro posizione.
Recuperati questi dati, li passeremo al nostro gioco, che li utilizzer almeno in questa fase iniziale,
per mostrare dei pallini colorati in corrispondenza delle dita dellutente.
Cosa fondamentale, sar prevenire allinterno dei vari handlers definiti, eventuali comportamenti
che alcuni client possono avere a seguito del rilevamento di determinate gestures: ad esempio la
gesture di pinch che dovremo largamente utilizzare allinterno di questo gioco, su iPad, scatena
lingrandimento o lallargamento della pagina del browser visualizzata, mantenere questo
comportamento, renderebbe ovviamente il gioco inuilizzabile.
Fortunatamente javascript fornisce una comoda funzione per inibire questi comportamenti:
event.preventDefault();
Tralasciando la classe Finger, che si occupa di creare proceduralmente dei cerchi con il metodo
ctx.drawArc, e di disegnarli in maniera del tutto analoga al giocatore e allo schermo nellarea di
gioco, vediamo come rilevare gli eventi descritti sopra, e come gestire i dati ricevuti.
Per prima cosa bisogna verificare che il dispositivo corrente supporti gli eventi touch, per fare ci
sufficiente settare una variabile in questo modo:
var touchable = 'createTouch' in document;
Questa istruzione controlla lesistenza della stringa createTouch allinterno delloggetto della pagina,
restituendo true, se tale stringa presente, false altrimenti.
Tale stringa presente solo in dispositivi che supportano eventi touch, per tanto, sar sufficiente
effettuare un controllo su questa variabile per stabilire se possiamo o meno proseguire:
44
Se ci troviamo su un dispositivo che supporta gli eventi touch, allora associamo un listener per gli
eventi di inizio, fine e movimento di un punto di contatto con lo schermo, ci assicuriamo di prevenire
comportamenti indesiderati con il metodo preventDefault(), e in caso di movimento salviamo la lista
e.touches allinterno di una variabile accessibile dalla nostra classe game.
e.touches una lista di punti di contatto o come vengono identificati in javascript: Touch, ogni
touch un oggetto che contiene un identificatore univoco, il target su cui stato scatenato levento:
nel nostro caso sar un puntatore al canvas, e una serie di coordinate prese da diversi punti di
riferimento:
- clientX,clientY: sono le coordinate misurate dallangolo superiore sinistro dellintera finestra del
browser
- pageX, pageY: sono le coordinate misurate dallangolo superiore sinistro della pagina visualizzata
- screenX, screenY: la posizione misurate dallangolo superiore sinistro dellintero schermo del
client.
Allinterno del progetto, le uniche coordinate utilizzate sono state screenX e screenY in quanto esse
sono le uniche che variano solo in caso di aggiunta di nuovi monitor, o cambio della risoluzione dello
schermo, il che rende il sistema di riferimento che esse generano, sempre consistente e affidabile.
Nel caso specifico dellevento touchend, il comportamento dellhandler semplicemente quello di
rimuovere lintero contenuto della lista touches.
A questo punto siamo in grado di accedere ai punti di contatto delle dita sullo schermo anche dal
nostro gioco, per tanto non ci resta che creare due istanze delloggetto Finger, che rappresenter
come dicevamo prima, un dito sullo schermo:
// Fingers
this.fing1 = new Finger();
this.fing2 = new Finger();
A questo punto allinterno del metodo update, non dovremmo fare altro che contare il numero di
dita rilevate sullo schermo, se esse sono due, sono sufficienti a rilevare la gesture di pinching, per
tanto possiamo fornire un feedback adeguato allutente, posizioniamo le dita nel punto di contatto
rilevato allinterno del metodo update del gioco:
45
// There can be a pinch only if there are two fingers on the screen
if(touches.length === 2){
self.fing1.show = true;
self.fing2.show = true;
self.fing1.pos_x = touches[0].screenX;
self.fing1.pos_y = touches[0].screenY;
self.fing2.pos_x = touches[1].screenX;
self.fing2.pos_y = touches[1].screenY;
}
else{
self.fing1.show = false;
self.fing2.show = false;
}
Come possibile intuire la variabile show, serve a stabilire se necessario disegnare o meno il pallino
sullo schermo, il risultato sar semplicemente la comparsa di pallini in corrispondenza delle dita
quando ci sono almeno due dita sullo schermo.
46
I valori di min / max finger distance, ovvero le distanze obiettivo che i pazienti dovranno
raggiungere con le loro dita, vengono stabilite in due modi:
- In fase di setup del gioco dal terapista tramite unopportuna interfaccia.
- Tramite un sistema di auto-calibrazione attivo nella fase iniziale del gioco.
Entrambi i metodi verranno discussi dettagliatamente in seguito.
Per quanto riguarda la gestione degli eventi pinch, pinch-out e pinching si deciso di utilizzare
una libreria di terze parti, specializzata a rilevare molte delle gestures tipiche operabili su dispositivi
touch-enabled, quali: tap, swipe, rotate, drag e ovviamente pinch.
La libreria in questione si chiama QuoJs, e per quello che riguarda il progetto della mongolfiera
in grado di rilevare e scatenare i seguenti eventi:
-
eventi pinch o pinch-out, scatenati a movimento completato, salvare i dati relativi al movimento
effettuato per poter preparare successivamente le statistiche relative, stabilire se il movimento
effettuato valido o meno, ovvero se esso ampio a sufficienza rispetto ai parametri impostati in
fase di calibrazione, e infine fare reagire il gioco di conseguenza mostrando un feedback negativo in
caso di errore, o facendo muovere la mongolfiera in caso di successo.
Vediamo nel dettaglio come tutto ci stato implementato:
Per prima cosa definiamo nel costruttore della classe principale del gioco tre variabili booleane:
var pinched = false;
var pinching = false;
var pinchout = false;
Queste variabili verranno settate a true dai vari handler relativi agli eventi corrispondenti; in questo
modo sar sufficiente controllare lo stato di queste variabili dal metodo update del nostro gioco per
sapere quale caso dovremmo gestire a ogni iterazione del game loop.
Ora catturiamo gli eventi che vengono scatenati dalla libreria QuoJs, precedentemente importata
nel file index.html, posizioneremo tali handler allinterno del controllo if(touchable) descritto
precedentemente, infatti non ha senso assegnare degli handler per degli eventi che non possono
essere scatenati dal client corrente:
/* Handlers: Using the QuoJS Library to detect gestures */
$$(this.canvas).on("pinch", function(e){
e.preventDefault();
pinched = true;
});
$$(this.canvas).on("pinching", function(e){
e.preventDefault();
pinching = true;
});
$$(this.canvas).on("pinchOut", function(e){
e.preventDefault();
pinchout = true;
});
Come possibile osservare gli handler si limitano a prevenire il comportamento di default associato
alla gesture rilevata: nel nostro caso lingrandimento o il rimpicciolimento della pagina, con il
metodo preventDefault(), e a impostare le variabili definite in precedenza a true:
N.B. Allinterno del metodo update, una volta gestito levento, dovremmo ricordarci di reimpostare
a false queste variabili per evitare di gestire i singoli casi pi di una volta!
A questo punto siamo in grado di predisporre il nostro metodo update per gestire i singoli eventi:
-
Verificheremo lo stato della variabile pinching, se essa true significa che lutente ha le dita
sullo schermo e sta effettuando il movimento, in questo caso dovremo solo monitorare la
distanza delle dita ed eseguire qualche animazione sulla mongolfiera
Se invece lo stato della variabile pinched a true, significa che lutente ha appena rilasciato
le dita dal dispositivo, e che ha completato il pinch, in questo caso dovremmo misurare la
distanza compiuta dalle dita, preparare il dato per essere inviato al server e infine muovere
di conseguenza la mongolfiera.
Se infine la variabile pinchout a essere true, bisogner comportarsi in maniera analoga ma
muovere la mongolfiera nella direzione opposta.
48
Come possibile intuire, il gioco non ancora cambiato nel suo aspetto, ma ora quando vengono
compiuti i movimenti richiesti, su console vengono stampate le scritte Pinching, Pinched e
Pinched-Out.
Prima di far muovere la mongolfiera ci rimane un problema da risolvere: quello della misurazione
della distanza tra le dita in cm.
7.f ii - MISURARE LA DISTANZA DELLE DITA SULLO SCHERMO
Essendo HotAirBalloon un gioco per la riabilitazione, abbiamo necessit di inviare, a fine sessione, al
terapista informazioni riguardo le prestazioni del paziente.
In un gioco in cui lunico movimento richiesto quello di avvicinamento e allontanamento delle dita,
la capacit di misurare in tempo reale la distanza fisica tra le dita in qualsiasi istante
fondamentale.
Vi per una sostanziale differenza, in un applicazione di questo tipo, tra distanza fisica e
virtuale tra due punti:
49
Per distanza fisica intendiamo la distanza effettiva reale tra le dita che stanno toccando la
superficie dello schermo.
- Per distanza virtuale intendiamo, invece, la distanza, in pixel, che viene calcolata tra i due
punti identificati allinterno del canvas.
facile intuire come mentre la distanza fisica sia una distanza assoluta tra due punti nello spazio
reale, la distanza virtuale dipenda strettamente dal dispositivo utilizzato:
Posizionando due dita alla medesima distanza fisica su due schermi a risoluzione differente
otterremo due distanze in pixel completamente diverse, in particolare il dispositivo a risoluzione
maggiore generer una distanza virtuale tra i due punti pi ampia di quello a risoluzione minore.
Dato fondamentale per la conversione tra distanza virtuale e distanza fisica, quello fornito dai DPI
(Dots Per Inch) dello schermo del dispositivo.
Come suggerito dal nome, questo valore ci comunica quanti punti, o meglio pixels sono racchiusi
nella distanza di un pollice (2.54 cm) sul dispositivo corrente.
Conoscendo questo dato, la formula per la conversione da distanza virtuale a distanza fisica tra
due punti immediata:
() = 2.54
Il problema che ci si pone a questo punto : come recuperare i DPI dello schermo del client in
unapplicazione web utilizzando javascript?
Purtroppo, non esiste una funzione comoda da poter richiamare per ottenere questo dato:
Una soluzione parziale ci fornita dalla possibilit di definire in CSS propriet quali la larghezza di un
elemento, come un <div>, uno <span> o unimmagine: <img>, in cm tramite lattributo width:
div{
width: 1cm;
}
Sapendo poi che la funzione jquery width(); restituisce la larghezza dellelemento selezionato in
pixel, sarebbe sufficiente scrivere una funzione getScreenDPI(); che crei proceduralmente un div
nascosto di dimensione 1cm o un 1in, legga il numero di pixel restituiti dalla funzione width() sul
nuovo div, elimini il div appena creato e restituisca il valore.
In questo modo potremmo sapere quanti pixel largo un div di 1inch, o di 1 cm, sostanzialmente
avremmo ottenuto il valore dei dpi dello schermo.
function getScreenDPI(){
var dpi = 0;
// Create an empty div 1 inch wide
var tmpDiv = document.createElement("div");
tmpDiv.style.display = "none";
tmpDiv.style.width = "1in";
// Get the div width in pixel
dpi = $(tmpDiv).width();
// Remove the div
$(tmpDiv).remove();
return dpi;
}
50
Provando questa funzione su schermi con DPI noti, si noter per che il valore restituito, non
corrisponder agli effettivi DPI dello schermo, generalmente la funzione ci fornir come valore o 72
o 96 DPI per qualsiasi dipositivo che utilizzeremo.
Stando alle specifiche fornite dal produttore, un tablet come liPad di prima generazione dotato di
uno schermo a 132 DPI e nuovi schermi come quelli di cui sono dati gli iPad di ultima generazione
ne hanno molti di pi!
Non esistendo ad oggi uno standard per definire le dimensioni di un oggetto html in cm o in pollici,
i browser, tendono a mentire sui reali DPI dello schermo, generalmente impostano un valore pari
a 96 DPI per i dispositivi con schermo standard e utilizzano un moltiplicatore per variare questo
valore, su schermi ad alto numero di DPI, come gli schermi retina dei nuovi iPad.
Questo significa, che in realt in HTML, quando si definisce la dimensione di un oggetto in cm, non
detto che ci che verr creato, sar effettivamente della dimensione richiesta!
In questo gioco, per, fondamentale poter misurare con accuratezza la distanza fisica delle dita del
paziente, per altro, si cercato di ovviare a questo problema, aggiungendo un campo, nella
schermata di configurazione del gioco utilizzata dal terapista che vedremo in seguito, in cui
possibile scegliere il dispositivo del paziente da una lista di dispositivi supportati (a ogni dispositivo
associato il corrispondente valore di DPI dello schermo, preso dalla scheda tecnica fornita dal
produttore).
Quando il file di configurazione verr letto dal gioco, in fase di avvio, verr impostato il valore DPI
corretto per il dispositivo utilizzato in un campo dedicato della nostra classe principale; lintero
processo di trasferimento dei dati di gioco dal pannello di configurazione al gioco verr trattato nel
capitolo 8.
Ora che siamo in possesso di questa importante informazione possiamo scrivere una funzione che
misuri la distanza attuale tra le dita del paziente, in funzione dellultimo valore registrato prima dello
scatenarsi dellevento pinched (ovvero levento che stabilisce quando un paziente ha staccato le
dita dallo schermo), potremo validare linput secondo i valori richiesti dal terapista.
7.f iii - VALIDARE LINPUT DELLUTENTE
Per prima cosa scriviamo una funzione che ci permetta di calcolare comodamente la distanza tra due
punti e la converta in centimetri, questo ci permetter di poter confrontare la distanza ottenuta con
la distanza richiesta dal terapista.
var INCH2CM = 2.54;
function pointDistance(x1,y1,x2,y2 dpi){
var d1 = Math.pow((x1 - x2),2);
var d2 = Math.pow((y1 - y2),2);
// This calculates the distance between two points in inches
// and convert it to centimetres
var dist = Math.sqrt(d1 + d2)/dpi * INCH2CM;
}
Ora allinterno del metodo update, se ci sono due dita sullo schermo, e la variabile pinching a true,
ovvero c una gesture in corso, dobbiamo verificare che il pinch non sia gi stato iniziato in un loop
precedente, se non lo stato: ovvero pinchStarted ancora a false, dobbiamo registrare la distanza
iniziale del pinch corrente:
this.update = function(self){
[...]
currPinchDistance = pointDistance(touches[0].screenX, touches[0].screenY,
touches[1].screenX, touches[1].screenY, dpi);
// Check if user is pinching
if(pinching === true){
if(pinchStarted === false){
currPinchStartDistance = currPinchDistance;
pinchStarted = true;
}
pinching = false;
}
[...]
}
Ricordiamo che touches una lista contenente tutti i punti di contatto con lo schermo nel frame
attuale!
Ora ci chiediamo: quando finisce una gesture?
Quando viene scatenato o un evento pinch o un evento pinchout.
Ora abbiamo quindi bisogno di altre due variabili:
var pinchEnded = true;
var currPinchEndDistance = 0;
52
Anche in questo caso, la prima serve a capire se abbiamo gi gestito o meno levento di pinch
concluso e la seconda serve a conservare temporaneamente la distanza di chiusura delle dita
registrata.
A questo punto sia allinterno dellif dedicato al controllo dello stato della variabile pinched, che
in quello dedicato al controllo della variabile pinchout, dovremo verificare lo stato della variabile
pinchEnded, se essa true, allora dovremo registrare il valore attuale di currPinchDistance in
currPinchEndDistance, impostare a false la variabile pinchEnded e reimpostare la variabile
pinchStarted per preparare il sistema a leggere il pinch successivo.
Vediamo il tutto nel caso pinched, per il caso pinchout il codice del tutto analogo:
this.update = function(self){
// There can be a pinch only if there are two fingers on the screen
if(touches.length === 2){
[...]
}
else{
[...]
// fingers are not on the screen anymore!
if(pinched === true){
// Pinch is complete: record finger distance and movement data
// than make the player climb
if(pinchEnded === false){
currPinchEndDistance = currPinchDistance;
pinchEnded = true;
pinchStarted = false;
currPinchDistance = 0;
}
pinched = false;
}
[...]
}
A questo punto abbiamo salvato in variabili temporanee i valori di apertura e chiusura del pinch
registrato: non ci resta che salvarli in un contenitore, un vettore, sia che essi siano ritenuti validi
(ovvero allinterno dei parametri impostati dal terapista), sia che non lo siano, per essere inviati a
fine sessione al server con lo scopo di generare le opportune statistiche da mostrare al terapista.
Dopo di che potremo confrontarli con i valori richiesti dallesercizio per stabilire se fare muovere la
mongolfiera, oppure tenerla ferma generando un opportuno feedback allutente.
53
54
Questa la fase principale del gioco, ha durata variabile da 1 a 5 minuti, la lunghezza dellesercizio
impostabile dal terapista in fase di configurazione.
Lutente in questa fase si ritrova in una scena diversa, lo sfondo un cielo che scorre dando la
sensazione che la mongolfiera si stia spostando orizzontalmente.
Eseguendo dei movimenti di pinch-in la mongolfiera sale, mentre eseguendo movimenti di pinchout la mongolfiera scender, lo scopo di questa fase duplice: evitare degli uccellini che volano
incontro alla mongolfiera e raccogliere delle monetine che casualmente compariranno sullo
schermo.
Il punteggio viene assegnato in base al tempo di volo trascorso senza nessun impatto con un uccello,
ogni impatto porta a un decurtazione di 5 punti dal punteggio, mentre la collezione di una moneta
un bonus di 50 punti.
Durante questa fase, il sistema registra tutti i pinch effettuati in vettori dedicati, come mostrato nel
capitolo precedente, viene inoltre salvato il numero di pinch effettuati e la percentuale di pinch validi
e invalidi. I dati non verranno inviati al server fino al completamento dellesercizio e il conseguente
inizio della fase 3.
FASE 3 ATTERRAGGIO E INVIO DEI DATI DI SESSIONE AL SERVER
Questultima fase parte allo scadere del tempo di gioco impostato, la mongolfiera scende da sola
verso la parte inferiore dello schermo, la scena cambier nuovamente e scopo dellutente sar quello
di cercare di fare atterrare dolcemente la mongolfiera al suolo effettuando dei pinch per rallentarne
la caduta.
I tipi di atterraggi rilevati sono 3:
- Atterraggio duro: non vengono attribuiti punti bonus
- Atterraggio morbido: vengono assegnati 100 punti bonus
- Atterraggio molto morbido: vengono assegnati 200 punti bonus
A seguito dellatterraggio viene mostrato per qualche secondo il punteggio iniziale e viene
automaticamente rilanciato il gioco dalla fase 1 per una seconda sessione.
La fase di atterraggio, in realt una fase che maschera linvio asincrono dei dati di sessione rilevati
durante la fase 2 al server. Durante latterraggio il sistema infatti richiama una procedura il cui scopo
preparare i dati per linvio ed eseguire un HTTP Request verso una procedura server side, che si
occuper di salvare su file i dati della sessione.
Ora che abbiamo chiaramente delineato la struttura finale del gioco, possiamo ad analizzare gli
elementi chiave dello sviluppo che non sono ancora stati trattati, ovvero:
-
55
takeoff
animation
flying
animation
landing
56
this.update = function(self){
if(animation === true){
// Handle animations here
animation = false;
}
else{
if(touches.length === 2){
[...]
}
else{
[...]
if(pinched === true){
console.log("Pinched");
if(gamestatus === "takeoff" || gamestatus === "landing"){
// Handle pinch in takeoff or landing status
}
if(gamestatus === "flying"){
// Handle pinch in flying status
}
pinched = false;
}
if(pinchout === true){
[...]
}
}
// Status transition: takeoff -> flying
if(player.pos_y < 0 && gamestatus === "takeoff"){
gamestatus = "flying";
animation = true;
}
// Status transition: flying -> landing
if(self.eta.getTime() === 0){
gamestatus = "landing";
animation = true
}
self.draw(self);
}
}
this.draw = function(self){
self.ctx.clearRect(0,0,window.innerWidth, window.innerHeight);
self.ctx.beginPath();
switch(gamestatus){
case "takeoff":
// Draw all elements from takeoff mode
if(takeoffBgReady === true){
self.ctx.drawImage(self.takeoffbg ,0,0,
window.innerWidth, window.innerHeight);
}
break;
case "flying":
// Draw all elements from flying mode
break;
case "landing":
// Draw all elements from landing mode
break;
}
[...]
}
57
Ovviamente controllando lo stato delle variabili gamestatus e animation allinterno dei metodi
update e draw della classe di gioco, possiamo decidere come fare comportare lapplicazione.
Come possibile osservare dal codice abbiamo inserito un macro-controllo allinterno del metodo
update, nel quale controlliamo, prima di qualsiasi altra cosa se al momento attiva un animazione
o meno.
possibile osservare, come ad esempio, in caso di animazione venga ignorato completamente
linput dellutent e come, nel caso di pinching, vengano distinti casi diversi a seconda dello stato di
gioco, per generare comportamenti diversi a seconda della situazione
Per quanto riguarda il metodo draw, stato aggiunto uno switch per il controllo del gamestatus,
allinterno dei singoli case vengono disegnati solo i componenti da mostrare nel singolo stato di
gioco, mentre fuori dallo switch sono presenti solo le istruzioni per disegnare elementi comuni a
tutti gli stati, come ad esempio il giocatore.
58
Lanimazione, a questo punto teoricamente molto semplice e consiste nel disegnare a ogni frame
una porzione diversa della nostra spritesheet in modo tale che al primo frame disegnato, venga
disegnato il primo riquadro della spritesheet, al secondo frame il secondo, al terzo il terzo e cos via
fino allultimo, per poi ricominciare dallinizio, con una frequenza di aggiornamento
sufficientemente rapida, si dar lillusione allutente che il protagonista della nostra animazione stia
muovendosi, quando in realt stiamo semplicemente sostituendo la sua immagine molto
velocemente.
Vediamo quindi come a partire da uno Spritesheet di questo tipo si sia realizzata lanimazione degli
uccellini allinterno di HotAirBalloon:
Per prima cosa necessario definire una classe che rappresenti un uccellino sullo schermo, la
chiamiamo Bird, questa classe avr tutte le propriet della classe Player, quindi posizione,
scalatura, altezza, larghezza ecc. le variazioni principali riguarderanno il metodo draw:
Innanzi tutto analizziamo i due overload possibili del metodo drawImage della classe context
associata al nostro canvas:
-
Il primo, quello che utilizziamo nel caso di disegni semplici, in cui vogliamo disegnare
lintera immagine in una posizione stabilita allinterno del canvas:
ctx.drawImage(img, x, y, width, height);
Prende come parametri limmagine da disegnare, e il rettangolo entro cui limmagine dovr
essere disegnata, se limmagine ha dimensioni superiori o inferiori alla dimensione del
rettangolo specificato, allora verr deformata per rientrare in quelle dimensioni.
-
Il secondo overload quello che serve per realizzare spritesheet animations allinterno di un
canvas html:
ctx.drawImage(img, sx, sy, swidth, sheight, x, y, width, height);
In questo caso il metodo prende come primo parametro limmagine da disegnare, nel nostro
caso lintera spritesheet, sx e sy sono le coordinate da cui iniziare il disegno nellimmagine
sorgente, in parole povere le coordinate del riquadro dello spritesheet da disegnare al frame
corrente, swidth e sheight, le dimensioni del singolo riquadro, mentre x, y, width e height
stabiliscono posizione e dimensione del riquardo da disegnare sul canvas.
Come possibile osservare, con questo secondo overload del metodo drawImage possibile
disegnare singole porzioni di immagini, vediamo come utilizzarlo nel caso specifico degli uccellini.
59
Innanzi tutto bene conoscere alcune propriet dello spritesheet generato con il programma di
photo editing, in particolare vogliamo conoscere:
- Le dimensioni dellintero spritesheet
- Le dimensioni di un singolo riquardo allinterno dello spritesheet
- Il numero di riquadri per riga
- Il numero di righe e di colonne nel singolo spritesheet
- La direzione in cui la sprite va letta (da sinistra verso destra o da destra verso sinistra)
Salviamo queste informazioni allinterno di opportune variabili della classe Bird come mostrato di
seguito:
function Bird(spritesheet, width, height,
slides_row, slides_col, total_slides, sprite_dir){
[...]
// Calculate single slide width and height
var sprite_width = width / slides_row;
var sprite_height = height / slides_col;
var slide_counter = 1;
[...]
// Define two different draw methods depending on the direction
// from which the sprite should be read.
if(sprite_dir === "r"){
var curr_slide = 0;
var curr_col = 0;
this.draw = function(ctx){
[...]
}
}
else{
var curr_slide = slides_row - 1;
var curr_col = 0;
this.draw = function(ctx){
[...]
}
}
}
Come possibile osservare, vengono distinti due casi, se la spritesheet va letta da sinistra verso
destra -> sprite_dir === r allora definiamo un metodo draw, altrimenti ne definiamo un altro, i
metodi in realt sono speculari, ci che cambia la direzione del ciclo con cui si scorrono le singole
slides.
Per comodit osserviamo solo il primo caso, il secondo speculare:
-
E la dimensione dello sprite sul canvas sar data ancora dalle variabili sprite_width e
sprite_height per il fattore scale della classe Bird che ci permetter di ridimensionare
facilmente gli uccellini sullo schermo.
- Una volta disegnato il frame corrente, dovremmo preparare le variabili per disegnare il
prossimo frame:
o Incrementeremo il numero della slide corrente
o Verificheremo di non aver superato il numero di slide per riga, in caso affermativo
resetteremo il contatore a 0
o Verificheremo di non aver superato il numero di slide per colonna, anche in questo
caso resetteremo il contatore a 0
o Infine incrementeremo il numero di slide disegnate, e verificheremo di non aver
superato il numero di slide totali nella spritesheet, anche in questo caso dovremo
resettare tutte le variabili per riprendere a disegnare dallangolo superiore sinistro
della spritesheet.
Ecco, per completezza il codice del metodo draw della classe Bird:
this.draw = function(ctx){
ctx.save();
// Draw current slide
ctx.drawImage(this.spritesheet,
sprite_width * curr_slide, sprite_height * curr_col,
sprite_width, sprite_height,
sprite_width * scale, sprite_height * scale
);
// Move the counter forward
curr_slide += 1;
// Check if we reached the end of the row
if(curr_slide === slides_row){
curr_slide = 0;
curr_col += 1;
// Check if we reached the end of the column
if(curr_col === slides_col){
curr_col = 0;
}
}
slide_counter += 1;
// Check if we reached the end of the spritesheet
// We need to do this if the last row has not enough slides
if(slide_counter > total_slides){
slide_counter = 1;
curr_slide = 0;
curr_col = 0;
}
ctx.restore();
}
In modo analogo si generata anche lanimazione delle monetine e delle esplosioni sullo schermo.
Ecco una sequenza di screenshot tratta da una sessione di gioco per mostrare leffetto ottenuto.
61
Come possibile osservare dallimmagine, i riquadri colorati rappresentano il nostro canvas, quello
che si fatto stato disegnare una porzione dellimmagine grande quanto il canvas attuale, a partire
da una posizione (lungo lasse X) diversa e incrementale a ogni frame, dando cos lillusione di
movimento.
Questo obbiettivo facilmente raggiungibile utilizzando una variabile di posizione sullasse X, che
chiameremo:
var frame_pos_x = 0;
E lo stesso overload del metodo drawImage utilizzato nelle spritesheet animations; eseguendo
quindi a ogni frame il seguente codice, otterremo il risultato voluto:
ctx.drawImage(this.sprite,
frame_pos_x, 20, ctx.canvas.width, ctx.canvas.height,
0,0,ctx.canvas.width, ctx.canvas.height);
frame_pos_x = frame.pos_x + 10;
62
63
this.draw = function(ctx){
ctx.save();
if(reachingEnd === true){
/* The first part of the background is the last visible *
* part of the sprite */
var first_part_width = this.sprite.width - frame_pos_x;
var remaining_part = ctx.canvas.width - first_part_width;
ctx.drawImage(this.sprite,
frame_pos_x, 20, first_part_width, ctx.canvas.height,
0,0,first_part_width, ctx.canvas.height);
/* The last part is the first part of the sprite */
ctx.drawImage(this.sprite,
0, 20, remaining_part, ctx.canvas.height,
first_part_width, 0, remaining_part,
ctx.canvas.height);
}
else{
ctx.drawImage(this.sprite,
frame_pos_x, 20, ctx.canvas.width, ctx.canvas.height,
0,0,ctx.canvas.width, ctx.canvas.height);
}
if (this.loop === true){
frame_pos_x = frame.pos_x + 10;
}
/* If nex portion of image excedes the source sprite width,
*
* we need to draw both the end and the beginning of the sprite */
if(frame_pos_x + ctx.canvas.width > this.sprite.width){
reachingEnd = true;
}
/* If we reached the end of the sprite, we can restart *
* from the beginning */
if(frame_pos_x >= this.sprite.width){
frame_pos_x = 0;
reachingEnd = false;
}
ctx.restore();
}
Come possibile osservare dal codice, nel caso in cui reachingEnd settato a true, ovvero quando
la dimensione del riquadro da disegnare eccede la larghezza dellimmagine sorgente, effettuiamo
due chiamate al metodo drawImage, consecutive, la prima per disegnare la prima parte
dellimmagine, la seconda per la seconda parte.
Ovviamente quando la posizione frame_pos_x, ovvero la posizione del punto da cui iniziamo a
disegnare (in parole povere, langolo superiore sinistro del disegno) avr raggiunto o superato la
larghezza dellimmagine sorgente, potremo ricominciare ad eseguire una sola draw, ripartendo dal
punto con ascisse 0. Questo ci che viene controllato dallultimo if della funzione.
Degno di nota il controllo della propriet della classe loop: come possibile osservare, la posizione
del nostro box di riferimento viene incrementata, unicamente quando questa propriet settata a
true, ci limita lavvio o la fermata dellanimazione al semplice assegnamento di un valore diverso a
questa propriet.
64
birds = [];
max_birds_on_screen = 0; // Start with one bird
max_bird_number = 5; // Max 5 birds at the end of the exercise
bird_increase_frequency = 60000; // 1min
/* Preload birds */
for(var i = 0; i < max_bird_number; i++){
var bird = new Bird(
["sprites/red.png", "sprites/blue.png",
"sprites/green.png", "sprites/yellow.png",
"sprites/purple.png"],
918,506,5,3,14,"l");
birds.push(bird);
}
Come possibile osservare, viene definito un numero massimo di nemici possibili, il numero di
nemici sullo schermo e la frequenza con cui si devono aggiungere nemici. Viene definito un semplice
array in cui mantenere le istanze degli oggetti bird, quindi allinterno di un ciclo si precaricano gli
uccellini, in modo da poterli utilizzare quando necessario, senza doverli reistanziare.
65
Da notare come il cotruttore della classe Bird, prenda come primo parametro un vettore di stringhe,
ogni stringa rappresenta il percorso a una spritesheet di colore diverso, questo viene fatto, per poter
permettere di cambiare colore delluccellino a runtime, passando da una spritesheet ad unaltra.
Ora che abbiamo preparato i nostri uccellini, dobbiamo modificare il metodo update per farli
muovere.
this.update = function(){
[...]
for(var i = 0; i < max_birds_on_screen; i++){
var bird = birds[i];
if(bird.alive === true){
// This moves the bird to the left
bird.update("l");
// Check if bird is off screen
if(bird.pos_x <= bird.width * -1){
bird.alive = false;
}
// Check for collision
if(boxCollided(player, bird)){
bird.alive = false;
// Explosion animation
[...]
// Update points & feedback user...
[...]
}
}
else{
/* Respawn logic */
// Hide the bird beyond the right side of the screen
bird.pos_x = self.canvas.width + 100;
bird.pos_y = Math.floor(Math.random() * (self.canvas.height bird.width));
// Randomize size
bird.scale = Math.random() / 2 + 0.3;
// Randomize color
bird.spritesheet.src = bird.spritesheets[Math.random() *
bird.spritesheets.length << 0];
// Randomize bird speed
bird.speed_x = Math.floor(Math.random() * 10) + 5;
}
}
[...]
// Also remember to increase birds on screen number every
bird_increase_frequency seconds
setInterval(function(){
if(max_birds_on_screen < max_bird_number){
max_birds_on_screen++;
}
}, bird_increase_frequency);
[...]
}
Come possibile osservare dal codice, la prima cosa che facciamo ciclare sul vettore birds da 0 al
numero massimo di uccellini mostrabili sullo schermo al frame corrente (N.B. Non al numero di
uccellini nellarray, altrimenti ne avremmo il numero massimo fin dal primo frame, e partiremmo
dalla difficolt massima),
66
Per ogni bird, verifichiamo innanzi tutto la propriet alive, se vivo allora dovremmo farlo
muovere, se morto dovremmo riposizionarlo per ricomparire sullo schermo.
Analizziamo i casi uno per volta.
-
Alive a true, innanzi tutto spostiamolo sullo schermo in funzione della sua velocit
utilizzando il metodo update della classe Bird, che altro non fa che spostare, nella direzione
indicata dal parametro, nel nostro caso l: ovvero da destra verso sinistra luccellino in
posizione bird.pos_x bird.speed_x.
Una volta spostato verifichiamo due cose:
o Eventuali collisioni con il giocatore, trattandosi di sprites relativamente piccole, e
non essendo necessario, a questo livello un particolare livello di precisione si
deciso di utilizzare una funzione, boxCollided, che ritorna true, se i rettangoli
costruiti attorno al giocatore e alluccellino si sono intersecati, false altrimenti:
In caso di esito positivo, ci limitiamo a settare a false, la propriet alive del bird
corrente, a lanciare unanimazione di esplosione nella stessa posizione in cui si
rilevata la collisione e a decurtare un certo numero di punti al giocatore
o Verifichiamo la posizione del bird corrente, se essa minore di 0 significa
semplicemente che la sprite ha superato il bordo sinistro dello schermo, e non
quindi pi visibile allutente.
Anche questa volta settiamo a false la propriet alive, ma non eseguiamo alcuna
animazione ne azione particolare, questeventualit significa semplicemente che il
giocatore riuscito ad evitare luccellino.
Alive a false, pu succedere in vari casi:
o Ci troviamo alla prima esecuzione del metodo update dopo essere passati dalla fase
di decollo alla fase di volo.
o Lutente ha colpito luccellino allupdate precedente
o Lutente ha superato luccellino allupdate precedente
o trascorso un tempo superiore a bird_increase_frequency ed stato abilitato un
altro uccellino allinterno del vettore birds.
Qualunque sia la causa per la quale ci troviamo in questa condizione, lunica cosa da
effettuare il respawn delluccellino, ci significa riposizionarlo dietro al bordo destro
dello schermo, scegliere a caso una nuova posizione, scalatura, colore e velocit, e infine
settare a true nuovamente la propriet alive.
Ultima operazione da svolgere allinterno del metodo update, dopo il controllo della
propriet alive di tutti gli uccellini nel vettore, quella di eseguire a intervalli predefiniti una
funzione che abilita, incrementando il valore di max_birds_on_screen, il numero di uccellini
contemporaneamente visibili sullo schermo.
Questo viene fatto dalla funzione passata come parametro a setInterval, nellultima parte
del codice mostrato sopra.
Altri elementi importanti del gioco sono le monetine bonus: queste monetine compaiono
casualmente nella scena e provengono dalla stessa direzione da cui compaiono gli uccellini. Il
giocatore dovr cercare di toccare le monetine con la propria mongolfiera per ottenere 50 punti
bonus.
Scopo di queste monetine quello di stimolare lutente a muovere la mongolfiera invece che tenerla
il pi possibile ferma in un posto dove passano meno uccellini.
Il modo in cui vengono gestite le monetine simile a quello degli uccellini, ma molto pi semplice.
67
Innanzi tutto non abbiamo bisogno di un vettore di monetine, ma di una soltanto: per come stato
pensato il gioco, non potranno mai esserci due monetine contemporaneamente nella scena,
dovremmo per trovare un criterio, per stabilire quando e se disegnare una monetina sullo schermo.
var COIN_X_OFFSET = 100;
var COIN_SPRITE_HEIGHT = 40;
var FRAME_PER_MINUTE = 1500;
// Pick up a random frame to show the coin
var coinShowTime = Math.floor(Math.random() * FRAME_PER_MINUTE);
var frameCounter = 0;
var coin = new Coin(canvas.width + COIN_X_OFFSET, 0);
this.update = function(){
[...]
// Check if we should draw the coin
if(frameCounter === coinShowTime){
// Check if coin is not already onscreen
if(coin.show === false){
// Randomly choose a position and draw the coin
coin.pos_x = canvas.width + COIN_X_OFFSET;
coin.pos_y = Math.random() *
(canvas.height - COIN_SPRITE_HEIGHT) << 0;
coin.show = true;
// Pick up another random frame to show the coin
coinShowTime = Math.floor(Math.random() * FRAME_PER_MINUTE);
}
}
// Increase frame counter every frame
frameCounter++;
// If we passed FRAME_PER_MINUTE reset the counter
if(frameCounter > FRAME_PER_MINUTE){
frameCounter = 0;
}
[...]
}
Si scelto, come possibile osservare, un criterio del tutto casuale per mostrare una monetina sullo
schermo.
Il nostro scopo mostrare almeno una moneta al minuto: innanzitutto impostiamo nella costante
FRAME_PER_MINUTE, il numero di volte in cui viene chiamato il metodo update in un minuto di
gioco.
Nellesempio mostrato abbiamo un frame rate di 25 FPS (Frame al Secondo) per tanto in un minuto,
il metodo update verr chiamato:
2560 = 1500
Generiamo un numero casuale nellintervallo [0, FRAME_PER_MINUTE].
Il numero generato corrisponder al frame esatto in cui dovremo fare comparire la monetina sullo
schermo, lo salviamo nella variabile coinShowTime; non ci resta che contare il numero di update
effettuati, per fare ci, utilizziamo la variabile frameCounter, che conter i frame mostrati
dallinizio alla fine del minuto di gioco corrente, (superato il valore di FRAME_PER_MINUTE viene
resettato a 0).
68
La monetina viene quindi mostrata quando la variabile frameCounter ha lo stesso valore generato
nella variabile coinShowTime.
Una volta mostrata la monetina, viene impostato un nuovo valore per la variabile coinShowTime
generando casualmente un nuovo valore compreso tra [0,FRAME_PER_MINUTE].
Leffetto sar quello di avere la garanzia (100% di probabilit) che almeno una moneta compaia sullo
schermo in un minuto, senza che per sia prevedibile listante esatto in cui accadr.
Come intuire dalla posizione iniziale delle dita quale tipo di pinch vuole effettuare con le
dita.
- Come indicare nella maniera meno invasiva possibile il punto dove fermarsi.
Per il primo problema, stato effettuato un controllo particolarmente elementare:
-
Se le dita al momento dellinizio del pinch si trovano a una distanza superiore o uguale alla
distanza di apertura massima delle dita richiesta dal terapista, allora assumiamo che lutente
stia per eseguire un pinch-in
Se le dita si trovano a una distanza inferiore o uguale alla distanza di chiusura delle dita
richiesta dal terapista, allora assumiamo che lutente voglia effettuare un pinch-out.
Infine se le dita partono da una posizione compresa tra i due valori impostati dal terapista,
allora sicuramente il pinch risultante sar invalido, pertanto comunichiamo fin da subito
lerrore al paziente, colorando i puntatori delle dita (i cerchi che vengono disegnati in
corrispondenza di essi) di rosso.
this.update = function(){
[...]
if(gamestatus === "flying"){
if(currPinchStartDistance >= fingerRequiredStartDistance){
/* Then user is probably trying to do a pinch-in */
// TODO: draw green marks
}
else if(currPinchStartDistance <= fingerRequiredTargetDistance){
/* Then user is probably trying to do a pinch-out */
// TODO: draw blue marks
}
else{
/* If we are here than finger position is invalid already */
// TODO: notify user (RED FINGERS)
}
}
[...]
}
Per risolvere invece il secondo problema si deciso di posizionare delle barrette colorate
perpendicolari al segmento che unisce i due punti di contatto con tra la superficie e le dita sullo
schermo.
Il colore delle barrette impostato a seconda del tipo di pinch che si vuole effettuare:
- Verde per il pinch-in
- Blu per il pinch-out
Il colore dei cerchi in corrispondenza delle dita inizialmente giallo: se lutente, trascinando le dita,
riesce a superare le barrette, allora i cerchi cambieranno colore assumendo lo stesso della barretta
superata e, staccando le dita dallo schermo si otterr un pinch valido.
Il problema pi grosso stato per quello del posizionamento delle barrette in funzione della
posizione delle dita allinizio del movimento.
Vediamo il codice utilizzato per risolvere questo problema nel caso del pinch-in, per il pinch-out
esattamente speculare:
70
Come possibile osservare, per prima cosa, calcoliamo la differenza tra la posizione in cui sono
partite le dita nel pinch corrente, e dove il terapista vuole che arrivi: noi vogliamo che il movimento
sia il pi simmetrico possibile, per cui le barrette di riferimento, che dora in poi chiameremo
markers, andranno posti a una distanza di diff / 2 dal punto in cui iniziato il movimento.
Ora che sappiamo quanto i marker distano dalla posizione inziale, non ci resta che calcolare il
coefficiente angolare del segmento che unisce i due punti iniziali delle dita, per sapere esattamente
in quale punto dello schermo disegnare i marker.
Per fare questo calcoliamo la differenza tra i punti X della posizione delle dita, e la salviamo in deltaX
e la differenza tra i punti Y delle dita e la salviamo in deltaY.
Fatto ci, il coefficiente angolare, in gradi ci sar dato dalla formula:
tan1 (
180
)
= ( ) + (
) cos( /180)
2
= ( ) + (
) sin( /180)
2
71
diff/2
f2
y
72
Si fanno effettuare alcuni pinch al paziente liberamente, senza verificare cio che raggiunga
determinate misure preimpostate.
- Si registra la distanza delle dita allinizio e alla fine del movimento.
- Si conta il numero di pinch effettuato in questa fase.
- I valori minimi da raggiungere nei pinch della fase di volo vengono, quindi settati come la
media campionaria dei valori rilevati per ogni pinch effettuato in fase di decollo, pi o meno
un certo valore di tolleranza impostato dal terapista in fase di configurazione.
In caso di utilizzo del sistema di auto-calibrazione, a fine sessione, verranno inviati al terapista anche
informazioni quali gli effettivi valori di finger distance ottenuti dal sistema e la cardinalit del
campione, in modo che egli possa valutare la qualit delle rilevazioni effettuate dal sistema.
73
La cardinalit del campione viene stabilita in funzione della forza di gravit esercitata in fase di
decollo sulla mongolfiera: pi forte, pi sar difficile far superare il bordo superiore dello
schermo alla mongolfiera per passare alla fase di volo e pi pinch saranno richiesti al paziente.
Vediamo come tutto ci stato implementato:
Per prima cosa, allinterno del metodo update verifichiamo di essere nella fase 1 di gioco e che sia
stato effettivamente richiesto lutilizzo del sistema di auto-calibrazione, questo pu essere fatto
controllando semplicemente lo stato delle variabili gamestatus e autocalibration:
if(gamestatus === "takeoff"){
if(autocalibration === true){
[...]
}
}
Abbiamo ora bisogno di due variabili che puntino a due vettori il cui unico scopo sar mantenere le
distanze di inizio e chiusura dei pinch effettuati:
var startDistArray = [];
var endDistaArray = [];
Per comodit, in questa fase, consideriamo unicamente i pinch-in e non i pinch-out, infatti, scopo
del giocatore in questa fase unicamente quello di fare salire la mongolfiera oltre il bordo superiore
dello schermo, inutile accettare comandi per accelerare la discesa.
Quello che dobbiamo fare, a questo punto per ogni pinch effettutato correttamente inserire i valori
di pinchStartDistance e pinchEndDistance nei vettori definiti sopra. Per cui avremo:
if(gamestatus === "takeoff"){
if(autocalibration === true){
/* Only pinch-in are registered */
if(currPinchStartDistance > currPinchEndDistance){
startDistArray.push(currPinchStartDistance);
endDistaArray.push(currPinchEndDistance);
}
}
}
A questo punto non ci resta che attendere leffettivo superamento della fase di decollo per calcolare
e impostare i valori adatti per le variabili fingerRequiredStartDistance e fingerRequiredStopDistance
normalmente impostate dai terapisti.
Per cui quando si passa alla fase di volo, verifichiamo se autocalibration abilitato e calcoliamo la
media dei pinch effettuati, fatto ci non ci resta che convertire il valore di tolleranza su pinch non
eseguito correttamente dal valore espresso in percentuale dal terapista a un valore espresso in
millimetri.
La formula per fare ci la seguente:
= | |
Dove tollerancePercentage un valore compreso tra 0 e 1.
A questo punto i valori di pinch richiesti saranno dati da:
74
=
E
= +
Ecco quanto descritto sopra, espresso in codice in questo estratto del metodo update di gioco:
if(gamestatus === "flying"){
[...]
if(autocalibration === true){
var sum = 0;
for(var i=0; i < startDistArray.length; i++){
sum += startDistArray[i];
}
// Calculate average start distance
avgStart = sum / startDistArray.length;
sum = 0;
for(var i=0; i < endDistArray.length; i++){
sum += endDistArray[i];
}
avgEnd = sum / endDistArray.length;
// Number of pinches caught during calibration
calibrationPinchNumber = endDistArray.length;
// Calculate tollerance using parameters set by the therapist
tollerance = Math.abs(avgStart - avgEnd) * pinchErrorTollerance;
// Then set required distance variables
fingerRequiredStartDistance = avgStart - tollerance;
fingerRequiredTargetDistance = avgEnd + tollerance;
}
[...]
}
Ed infine ecco un estratto dalla pagina di statistiche della sessione presentata al terapista, che
mostra i dati generati dal sistema di auto-calibrazione:
75
Lo storico delle sessioni di gioco effettuate con, media per ogni sessione della posizione di
inizio e fine di ogni singolo pinch, e ovviamente, un dato riguardante il numero di sessioni
giocate.
Informazioni riguardanti la configurazione iniziale, che riportasse, per ogni sessione di gioco,
i dati relativi alla configurazione effettuata dal terapista riguardo alla distanza delle dita in
fase di pinching oppure ai dati generati dal sistema di auto-calibrazione del gioco.
Il numero o la percentuale di pinch ritenuti validi o invalidi in una singola sessione di
gioco.
76
E il dettaglio della distanza delle dita allinizio e fine di ogni movimento di pinching,
effettuato allinterno della singola sessione selezionata.
Essendo stato lintero gioco sviluppato con tecnologie web-based, si deciso di implementare
lintera piattaforma per i terapisti utilizzando le stesse tecnologie utilizzate per il gioco (HTML 5 e
Javascript).
In questo modo la piattaforma risieder sullo stesso server che ospiter il gioco utilizzato dai
pazienti, e sar accessibile via web da qualsiasi terminale con accesso a internet (di cui lospedale
per il quale stato creato lintero progetto, era gi provvisto). Vedremo poi nel paragrafo finale di
questo capitolo come sia stato ristretto laccesso alla hospital station unicamente al personale
autorizzato.
Ora addentriamoci pi nei dettagli, iniziando a vedere come stata realizzata la schermata di
configurazione del gioco e dellesercizio.
Come possibile osservare, il pannello di configurazione si presenta come una normalissima pagina
web, essa divisa sostanzialmente in 3 parti:
-
Tralasciando la parte relativa alla selezione e configurazione dei pazienti che verr analizzata meglio
nel paragrafo finale del capitolo, vediamo come stato strutturato il form di input della
configurazione, come funzionano i pulsanti e slider presenti in questa pagina e come i dati acquisiti
vengano preparati per linvio al gioco.
Per quanto riguarda il paziente corrente, ci basta sapere in questa fase, che il suo nominativo
univoco mantenuto nella sessione corrente, allinterno della variabile globale:
window.patient = "PXXX";
Per quanto riguarda, invece laspetto grafico dellintera pagina stato utilizzato il framework
bootstrap sviluppato da Twitter Inc. lazienda che sviluppa e mantiene lomonimo social network.
Si tratta sostanzialmente di una collezione di classi CSS e qualche modulo javascript utile da applicare
ai comuni elementi che caratterizzano una normale pagina web, quali bottoni, selettori, form, ecc.
con lo scopo di ottenere unaspetto grafico piacevole e coerente anche in assenza di web-designer
esperti che si occupino dellaspetto grafico della propria pagina.
Per quanto riguarda invece gli slider visibili nello screenshot qu sopra, stato utilizzato un plugin di
bootstrap open-source rilasciato da uno sviluppatore indipendente denominato bootstrapslider.js
Scopo di tale slider quello di semplificare limpostazione della misura delle dita durante il
movimento del pinch, o della percentuale di errore consentita al terapista:
Se analizziamo ad esempio nel dettaglio lo slider relativo alla finger distance, possiamo osservare
come spostando lo slider di sinistra si regoli la distanza richiesta dal terapista per quanto riguarda la
chiusura minima del pinch e regolando quello di destra si imposti, in maniera intuitiva la distanza
massima richiesta in apertura.
Tutti i valori impostati tramite slider o selettori vanno a riempire automaticamente i campi di un
form nascosto.
Andiamo, per, a osservare nel dettaglio come vengono trattati i dati impostati in questa schermata,
per fare ci dobbiamo seguire ci che succede alla pressione dei pulsanti pi importanti di questa
schermata, quelli situati nella parte inferiore destra:
-
Restore Defaults - Alla pressione di questo pulsante sia gli slider che i campi del form
nascosto vengono reimpostati ai valori predefiniti.
- Save Settings - Questo pulsante prepara i dati contenuti nel form ad essere inviati al server
per poter essere sfruttati dal gioco.
Per il primo pulsante limplementazione molto semplice, al rilevamento dellevento click (o tap
nel caso di dispositivi dotati di schermo touchscreen) sul pulsante, non si fa altro che accedere alla
propriet value di ogni singolo elemento del form e ripristinare il valore di default,
Il plugin bootstrap-slider utilizzato fornisce un metodo setValue molto comodo per eseguire questa
operazione, ecco un estratto dal codice per quanto rigurada proprio lo slider relativo alla distanza
tra le dita:
78
function restoreDefaults(){
[...]
// RESET SLIDER POSITION
$('#sl1').slider("setValue", [5,10]);
// ALSO RESET LABELS TEXT
$('#values_sl1').html("<p><b>5.0</b> cm
<i class=\"icon-resize-horizontal\"></i> <b>10.0</b> cm</p>");
[...]
}
Stessa cosa, ovviamente non verr descritta nel dettaglio, accade per tutti gli altri elementi presenti
nel form.
Da notare come la funzione restoreDefaults() non salvi il ripristino ai valori di default sul server, ma
effettui il cambiamento unicamente lato client: sar necessaria la pressione del pulsante Save
Settings per confermare le modifiche; questo rende possibile un annullamento delle modifiche
effettuate tramite il semplice reload della pagina e limita quindi il rischio di commettere errori da
parte dellutente.
Ben pi interessante ci che accade alla pressione del pulsante Save Settings, in questo caso
dovremmo preparare i dati per essere inviati al server ed effetture un opportuna HTTP request per
linvio vero e proprio.
bene spendere qualche parola nel descrivere lo standard utilizzato per la gestione dei file di
configurazione e delle sessioni di gioco allinterno della piattaforma.
Si pensato di utilizzare dei file JSON (JavaScript Object Notation), creati a runtime dalle varie
applicazioni che costituiscono la piattaforma, per il trasferimento dei dati dalla piattaforma per i
terapisti al gioco e viceversa.
Un file JSON sostanzialmente un file contenente dati strutturati secondo la notazione utilizzata
dagli oggetti javascript, eccone un esempio:
{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"title": "S",
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A meta-markup language, used to create markup
languages such as DocBook.",
"GlossSeeAlso": ["GML", "XML"]
},
"GlossSee": "markup"
}
}
}
}
}
79
Il grosso vantaggio derivato dallutilizzo di questa sintassi allinterno di unapplicazione come quella
realizzata in questo progetto dovuto principalmente alla possibilit di accedere a tutti i campi che
caratterizzano il file JSON, senza bisogno di generare o utilizzare un parser gi pronto, in quanto
assegnando lintero contenuto di un file JSON a una variabile, potremmo accedere a tutti i campi del
file come faremmo con un normale oggetto javascript.
Nel caso specifico della configurazione dellesercizio il file che vorremo andare a generare estraendo
i dati dal nostro form sar qualcosa di simile a questo:
{
"device_dpi":132,
"override_autocalibration":0,
"fingerRequiredTargetDistance":5,
"fingerRequiredStartDistance":10,
"pinchRequiredFreq":1,
"gameTime":1,
"tollerance":10
}
Il nostro primo obiettivo sar quindi quello di ottenere tutte le informazioni dal form HTML presente
nella pagina:
80
function saveConfig(){
// If no patient is selected
if(window.patient === undefined || window.patient === ""
|| window.patient === "none"){
return;
}
// Prepare variables
var device_dpi = parseInt($('#device_dpi').val());
var override_autocalib = document.getElementById('override_auto').checked;
override_autocalib = (override_autocalib === true) ? 1 : 0;
var minFingerDist = parseFloat($('#sl1').slider('getValue').val()
.split(',')[0]).toFixed(1);
var maxFingerDist = parseFloat($('#sl1').slider('getValue').val()
.split(',')[1]).toFixed(1);
var frequency = parseFloat($('#sl2').slider('getValue').val()).toFixed(2);
var gameTime = parseInt($('#sl3').slider('getValue').val());
var errorTollerance = parseInt($('#sl4').slider('getValue').val());
/* Set default values if necessary */
if(minFingerDist === 'NaN') {
minFingerDist = 5.0;
minFingerDist.toFixed(1)
};
if(maxFingerDist === 'NaN') {
maxFingerDist = 10.0;
maxFingerDist.toFixed(1)
};
if(frequency === 'NaN') { frequency = 1.0; frequency.toFixed(2); };
if(gameTime === 'NaN' || gameTime === "") { gameTime = 1; };
if(errorTollerance === 'NaN') { errorTollerance = 10; };
/* Send post request to generate the config file */
[...]
}
Come possibile osservare, si fa uso intenso della libreria jQuery per selezionare i vari elementi del
form; tutti gli elementi vengono presi e salvati semplicemente in opportune variabili.
8.b i - INVIO DELLA CONFIGURAZIONE DAL SERVER AL GIOCO
A questo punto non ci resta che inviare i dati al server che si occuper di generare il file di
configurazione che verr letto dal gioco, in fase di inizializzazione, prima di mostrare linterfaccia
allutente.
Per fare ci effettuiamo in fondo alla funzione saveConfig() una POST request sul server richiedendo
lesecuzione di un semplice script PHP il cui unico scopo sar creare il file config.json in una
directory dedicata allutente:
81
$.post("../HotAirBalloon/callbacks/generateConfig.php", {
patient : window.patient,
dpi : device_dpi,
override : override_autocalib,
minFinger : minFingerDist,
maxFinger: maxFingerDist,
frequency: frequency,
gameTime: gameTime,
tollerance: errorTollerance
}).done(function(){
$('#ok').fadeIn(500);
setTimeout(function(){$('#ok').fadeOut(500);},3000);
}).fail(function(){
$('#error').fadeIn(500);
setTimeout(function(){$('#error').fadeOut(500);},3000);
});
Per effettuare la POST request al server stata utilizzata la funzione $.post(callback, post_data) della
libreria jquery:
una semplice funzione che prende come parametro il path dello script lato server da richiamare e
gli argomenti da passare a tale script, in questo caso trasparente al programmatore listanziazione
delloggetto XMLHttpRequest, la preparazione dei dati e lesecuzione della chiamata SEND, secondo
lo standard definito per il protocollo HTTP.
Vediamo il codice della callback chiamata da questa funzione:
<?php
/* Retrieve POST data */
$data = array(
"device_dpi" => $_POST['dpi'],
"override_autocalibration" => $_POST['override'],
"fingerRequiredTargetDistance" => $_POST['minFinger'],
"fingerRequiredStartDistance" => $_POST['maxFinger'],
"pinchRequiredFreq" => $_POST['frequency'],
"gameTime" => $_POST['gameTime'],
"tollerance" => $_POST['tollerance']
);
/* Open config file and write config */
$fp = fopen('../config/'.$_POST['patient'].'/config.json', w);
print($fp);
fwrite($fp, json_encode($data));
fclose($fp);
?>
Come possibile osservare, il codice PHP non fa altro che preparare in un array il contenuto della
variabile $_POST, ovvero i dati ricevuti dal form.
Dopo di che crea un file nella directory dedicata al paziente denominato config.json in modalit di
scrittura.
A questo punto utilizzando la funzione fwrite scrive sul file il contenuto dellarray convertito
automaticamente in formato json dalla funzione della libreria standard php json_encode.
82
Il file a questo punto stato generato, da notare come non sia prevista una politica di
aggiornamento del file particolare: a ogni esecuzione dello script PHP il file viene semplicemente
riscritto da zero.
Quello che rimane da osservare, a questo punto, osservare come il gioco recuperi a sua volta il file
di configurazione generato poco fa:
Il codice per fare ci viene eseguito in fase di inizializzazione della classe game, a seguito del login
dellutente, che analizzeremo nella parte finale del capitolo, avremo accesso anche nella classe game
alla variabile globale window.patient, contenente lid relativo al paziente corrente.
Una volta in possesso di questo dato, per recuperare i dati relativi allultima configurazione salvata,
non ci resta che accedere al file config.json nella cartella del paziente ovvero:
/path/to/game/root/config/patient/config.json
Possiamo recuperare il file utilizzando, anche in questo caso, una comoda funzione della libreria
jQuery, la funzione getJSON(path), il cui scopo recuperare, tramite unHTTP request di tipo GET
il file JSON presente sul server e specificato come argomento.
window.patient = patient;
$.getJSON("config/"+window.patient+"/config.json", function(data){
therapistConfig = data;
/* since ajax loads data asynchronously and we want to pass
* therapistConfig to the game, we
* need to pack the whole game starting logic in a function
* and call that function only once
* we have all the required data
launchGame("myCanvas", therapistConfig);
});
*
*
*
*
*/
I dati ottenuti dalla funzione getJSON, essendo in formato JSON sono gi racchiusi allinterno di un
oggetto javascript valido, che possiamo passare tranquillamente al launcher del gioco, il quale
istanzier loggetto della classe game.js che al suo interno avr istruzioni di questo tipo:
[...]
this.devicePPI = therapistConfig.device_dpi;
83
Come possibile osservare, si accede ai singoli campi ottenuti dal file json come si accederebbe ai
campi di un normale oggetto javascript (Dot-Notation, oppure con parentesi quadre).
La stessa tecnica per il recupero del file json, viene utilizzata allinterno del pannello di
configurazione stesso quando, tramite lopportuno menu a tendina, il terapista seleziona un
paziente dalla lista, viene recuperato, se esiste, lultimo file di configurazione generato per quel
paziente, e automaticamente vengono impostati, per comodit tutti gli slider ai valori impostati in
precedenza per quel paziente.
Questo viene fatto per agevolare il lavoro del terapista che deve unicamente effettuare una semplice
modifica, magari su un unico parametro della configurazione, senza dover reimpostare tutti i valori
da zero.
84
Inviare un aggiornamento al server per ogni pinch effettuato (questo richiede una
connessione costante col server per effettuare lo streaming dei dati, pu essere pesante per
il server e genera problemi in caso di invio asincrono delle informazioni: pu accadere ad
esempio che arrivi al server il secondo aggiornamento, mentre il primo ancora in corso).
- Inviare un unico pacchetto di informazioni a fine sessione (nel momento di transizione tra la
fase di volo e la fase di atterraggio della mongolfiera).
Vista la semplicit della piattaforma in questo stadio iniziale di sviluppo e la quantit esigua delle
informazioni da inviare, si preferito adottare questo secondo metodo.
Rimane da rispondere alla domanda, come trasferiamo le informazioni?
Il metodo che si deciso di utilizzare del tutto analogo a quello utilizzato per trasferire il file di
configurazione generato dal terapista dal server al gioco:
Per ogni paziente, si generata unulteriore cartella sul server, denominata stats la quale contiene
una collezione di file JSON (uno per ogni sessione giocata) in cui vengono mantenuti tutti i dati
necessari a generare la pagina di statistiche.
Vediamo come:
-
Per prima cosa verifichiamo, allinterno del metodo update, di aver concluso la fase di volo
il timer di gioco, contenuto nella variabile self.eta deve essere 0, quando ci troviamo in
questo caso dobbiamo chiamare una funzione che prepari i dati necessari alla generazione
del file json e passi in modalit volo:
[...]
if(self.eta.getTime() === 0){
/* The interesting part of the game is finished... while the
* user plays the landing mode we pack all the interesting data
* and send them to the server, so that the therapist can
* analyze the results
*
*
*
*/
sendSessionStats(self);
/* Switch to landing mode */
self.gamestatus = 'landing';
}
[...]
A questo punto non ci resta che analizzare la funzione sendSessionStats() che come
possiamo osservare dal codice prende come parametro listanza corrente delloggetto
game potendo cos accedere a tutti i suoi campi pubblici.
86
function sendSessionStats(game){
$.post("callbacks/generateStats.php", {
patient : window.patient,
calibNumber : game.calibrationPinchNumber,
avgStart : game.avgStart,
avgEnd : game.avgEnd,
tollerance : game.therapistConfig.tollerance,
pinchOkNumber : game.validPinchNumber,
// Number of valid pinches
pinchBadNumber : game.invalidPinchNumber,
// Number of invalid pinches
startPinches : game.pinchStartArray,
// Array of pinches start
// distance
stopPinches : game.pinchStopArray
// Array of pinches stop
// distance
});
}
Anche in questo caso, possibile vedere come come la generazione dellopportuno file json venga
demandata a uno script PHP che viene eseguito server-side:
<?php
/* Retrieve POST data */
$data = array(
"calibNumber" => $_POST['calibNumber'],
"avgStart" => $_POST['avgStart'],
"avgEnd" => $_POST['avgEnd'],
"tollerance" => $_POST['tollerance'],
"pinchOkNumber" => $_POST['pinchOkNumber'],
"pinchBadNumber" => $_POST['pinchBadNumber'],
"startPinches" => $_POST['startPinches'],
"stopPinches" => $_POST['stopPinches']);
/* Open config file and write config */
$fp = fopen('../stats/'.$_POST['patient'].'/latest_session.json', w);
print($fp);
fwrite($fp, json_encode($data));
fclose($fp);
$fp = fopen('../stats/'.$_POST['patient'].'/'.time().'.json', w);
print($fp);
fwrite($fp, json_encode($data));
fclose($fp);
?>
Da notare come lo script, in questo caso generi non uno ma due file, uno denominato
latest_session.json e il secondo TIMESTAMP.json dove per TIMESTAMP si intende la stringa
rappresentante listante di tempo corrente ritornato dalla funzione time() di PHP.
Il primo file singolo e viene aggiornato a ogni sessione: contiene i dati relativi allultima sessione
giocata, mentre il secondo ha un nome sempre diverso ad ogni esecuzione dello script, e serve per
mantenere in memoria, sul server i dati relativi a tutte le sessioni giocate.
Passiamo quindi alla costruzione della pagina di statistiche:
Per prima cosa la pagina deve importare le librerie esterne fornite da google per poter accedere alle
API di costruzione dei grafici e inizializzarle.
87
Come possibile osservare dallultima istruzione, viene richiesta lesecuzione della funzione
drawChart una volta caricate, da remoto, le librerie di google. La funzione drawChart non esiste
ancora, bisogna crearla essa non far altro che accedere ai dati generati durante la sessione di
gioco e li elaborer creando i vari grafici, vediamo per esempio, come stato creato il grafico dello
storico delle sessioni, ovvero quel grafico che mostra media di apertura e chiusura delle dita per ogni
sessione giocata dal paziente selezionato:
-
Per prima cosa otteniamo le medie dei pinch eseguiti in ogni sessione registrata nella cartella
stats dellutente corrente:
La funzione eseguita in seguito a una chiamata andata a buon fine sulla callback si aspetta, anche in
questo caso un elemento di tipo json che bisogner restituire:
Vediamo, dunque, anche cosa viene eseguito lato server analizzando lo script getPatientHistory:
-
88
<?php
$patient = $_GET['patient'];
$stat_path = "../stats/".$patient."/";
$sessions = scandir($stat_path);
$result = array();
// Scan all file in the patient directory
foreach ($sessions as $s) {
// Skip unnecessary files
if($s == "." || $s == ".." || $s == ".DS_Store" || $s ==
'latest_session.json'){
continue;
}
// For each session calculate avg finger distance and save it to $result
$json = file_get_contents($stat_path.$s);
$arr = json_decode($json, true);
$timestamp = explode('.', $s)[0];
$sum_open = 0;
$sum_closed = 0;
$sum_dist = 0;
for($i = 0; $i < count($arr['startPinches']); $i++){
$sum_open += floatval($arr['startPinches'][$i]);
$sum_closed += floatval($arr['stopPinches'][$i]);
$sum_dist += (floatval($arr['startPinches'][$i])
floatval($arr['stopPinches'][$i]));
}
$average_start_pinch = $sum_open / count($arr['startPinches']);
$average_stop_pinch = $sum_closed / count($arr['stopPinches']);
$average_dist_pinch = $sum_dist / count($arr['startPinches']);
array_push($result, array("timestamp" => $timestamp, "avg_start" =>
$average_start_pinch, "avg_stop" => $average_stop_pinch, "avg_dist" =>
$average_dist_pinch));
}
// Echo the json_encoded version of the $result array
echo json_encode($result);
?>
Il div history_chart conterr il grafico e il range filter di controllo i due componenti verranno
distribuiti sui div pi interni:
- Chart0 per il grafico vero e proprio
- Control0 per il range filter.
Secondo le API di google chart la coppia grafico + strumenti di controllo si chiama dashboard per
questo la nostra funzione dovr predisporne una come segue:
89
A questo punto generiamo il sistema di controllo del range da visualizzare, si tratta di un semplice
grafico a linea con due cursori trascinabili col mouse per limitare la zona da visualizzare nel grafico
principale:
Fatto ci predisponiamo il grafico principale, si tratta di un semplice grafico a colonne, ogni elemento
del grafico ha associata la data della sessione il range di apertura massima delle dita e il range di
chiusura minima:
Ecco il codice:
90
Ora, sempre sfruttando le API di google e i dati restituiti dal nostro script PHP che abbiamo eseguito
server-side, prepariamo i dati da visualizzare nel grafico:
var prepared_data = [['Session',
'Average session fingers open (in cm)',
'Average session fingers closed (in cm)']];
for(var i = 0; i < data.length; i++){
var curr_element = data[i];
var tmpArray = [
// Converting UNIX time to javascript date
new Date(parseInt(curr_element.timestamp)*1000),
parseFloat(parseFloat(curr_element.avg_start).toFixed(2)),
parseFloat(parseFloat(curr_element.avg_stop).toFixed(2))];
prepared_data.push(tmpArray);
}
var historyData = google.visualization.arrayToDataTable(prepared_data);
Non ci resta che collegare grafico e rangeFilter assieme e disegnare il grafico corrispondente ai dati
preparati:
dashboard.bind(control,chart);
dashboard.draw(historyData);
stato mostrato come stato disegnato il grafico pi complesso della pagina, ovviamente la
funzione drawChart si comporter in modo analogo per generare tutti gli altri grafici presenti:
-
Eseguir uno script PHP server-side per il recupero dei dati dai differenti file JSON presenti
Li trasformer in dati utilizzabili dalla piattaforma Google Chart
Disegner il grafico pi opportuno per il tipo di dati da mostrare al terapista.
91
I pazienti: ogni paziente ha due cartelle dedicate sul server, una per il mantenimento dei file
di configurazione e unaltra per mantenere tutti i file JSON relativi alle sessioni giocate nella
sua storia. Ogni paziente deve poter essere identificato univocamente, e deve poter
accedere al gioco tramite una password, in modo da non poter giocare con la configurazione
di un altro paziente il cui nome noto.
- I terapisti: questi utenti hanno accesso alla Hospital Station, quindi allelenco dei pazienti,
alle loro configurazioni e alle statistiche di ogni paziente.
Iniziamo con lautenticazione dei pazienti.
Trattandosi lintero progetto, una piattaforma sperimentale e non un prodotto finito, si deciso di
mantenere il massimo grado di riservatezza dei pazienti nel modo pi semplice possibile, ovvero
limitando al minimo necessario il mantenimento di dati sensibili sul server.
Per fare ci si deciso di assegnare a ogni paziente un identificativo anonimo: PXXX dove al posto di
XXX viene sostituito dal sistema un numero a tre cifre univoco e incrementale. Sar cura
dellospedale associare a ogni identificativo il nome del paziente corrispettivo come meglio crede.
Un paziente pu essere creato da un terapista in qualsiasi momento, attraverso il pannello di
configurazione del gioco cliccando sul pulsante ADD NEW PATIENT.
Verr generato lid univoco del paziente e mostrato il seguente form da compilare con la password
scelta dallutente e la conferma di questultima.
92
Alla pressione del tasto di conferma verr validato il form controllando leffettiva compilazione e
uguaglianza dei campi password e confirm password dopo di che, verr richiamata uno script PHP
lato server che creer lutente vero e proprio, vediamo come:
$('#newpatient-form').submit(function(){
var patient = $("#newuser_patientId").html();
var pass = $("#inputPassword").val();
var confirm = $("#inputConfirmPassword").val();
if(pass.length === 0){
// All fields are required
[...]
return false;
}
if(confirm.length === 0){
[...]
return false;
}
if(pass != confirm){
// Password mismatch
[...]
return false;
}
/* POST call to create user */
$.post("../HotAirBalloon/callbacks/createNewUser.php", {user: patient,
passwd: pass})
.done(function(data){
var template = '<option value="{val}">{name}</option>';
var html = template.replace("{val}",
patient).replace("{name}",patient);
$('#patient_select:first-child').after(html);
$('#patient_select').val(patient);
$('#newpatient-modal').modal("hide");
window.patient = patient;
});
});
La sequenza di if iniziale controlla leffettiva compilazione dei campi obbligatori del form, una volta
superati i controlli viene eseguita una richiesta HTTP di tipo POST al server richiedendo lesecuzione
dello script PHP createNewUser.php, il cui compito sar creare le cartelle atte ad ospitare i file di
configurazione e i dati di sessione per il nuovo utente. Inoltre dovr aggiungere le credenziali di
accesso scelte in un file che consulteremo in seguito per loggare lutente nel gioco.
<?php
$patient = $_POST['user'];
$pass = sha1($_POST['passwd']);
/* Adding patient to users file: using 'a' instead of 'w' because we want *
the file pointer to point at the end of the file and not at the
*
beginning since we want to append data
*/
$fp = fopen("../users", 'a');
$string_to_append = $patient . " " . $pass . "\n";
fwrite($fp, $string_to_append);
fclose($fp);
mkdir("../config/".$patient);
mkdir("../stats/".$patient);
?>
93
Tralasciando la creazione delle directory dellutente che risolta con la banale chiamata alla
funzione php mkdir(path), parliamo un secondo del file users che come abbiamo detto, mantiene
al suo interno lelenco dei pazienti registrati con le loro password:
Si tratta di un semplice file di testo dove per ogni paziente viene aggiunta una riga (da notare con la
funzione fopen venga chiamata con modalit a, ovvero append).
Ogni riga contiene a sinistra il nome dellutente secondo la convenzione adottata dallintero sistema
PXXX seguito da uno spazio e la password elaborata utilizzando la funzione crittografica di hash
sha1.
Caratteristica di questa famiglia di funzioni di hash quella di essere in grado di generare una stringa
di dimensioni fisse e univoca a partire da una stringa di lunghezza variabile. Caratteristica particolare
di questo processo quello di non essere reversibile, non per tanto possibile risalire al messaggio
originale conoscendo unicamente la stringa restituita dalla funzione di hash.
Il file risultante sar qualcosa di simile a questo:
# LOGIN PASSWORD
P000 89e495e7941cf9e40e6980d14a16bf023ccd4c91
P001 89e495e7941cf9e40e6980d14a16bf023ccd4c91
P002 9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684
P003 9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684
Ora che sappiamo come i pazienti vengono aggiunti al sistema, vediamo come si possono autenticare
per utilizzare il gioco.
Prima dellavvio della sessione di gioco stato aggiunto un form da compilare per autenticare
lutente.
94
A questo punto, alla pressione del tasto Sign in dovr essere autenticato il paziente, settata la
variabile window.patient e avviato il gioco come descritto in precedenza.
Alla pressione del tasto Sign In, viene innanzi tutto verificata leffettiva immissione dei dati in tutti
i campi del form e leffettiva validit del nome utente inserito (tramite lespressione regolare
/P\d\d\d/i, verifichiamo che il nome utente sia composto da una P seguita da 3 cifre).
95
Una volta eseguiti questi controlli preliminari, si passa nuovamente al codice eseguito lato server:
viene chiamato uno script PHP userLogin.php a cui vengono passati via POST i parametri
rappresentanti nome utente e password dellutente che sta tentando di effettuare il login.
da notare, innanzi tutto, come la password venga immediatamente passata alla funzione sha1, se
essa sar identica alla password impostata in fase di creazione dellutente, allora la stringa restituita
dalla funzione sar la medesima.
A questo punto lo script apre il file users, che abbiamo mostrato in precedenza, in modalit di sola
lettura.
Viene letto il file riga per riga allinterno del ciclo while; essendo ogni riga strutturata nella stessa
maniera della precedente: nome_paziente \spazio password \nuova_riga, particolarmente
comodo usare la funzione fscanf per leggere da file.
La funzione restituir un vettore di stringhe, in cui il primo elemento sar il nome del paziente nella
riga corrente, e il secondo la password.
Non ci resta che confrontare i valori estratti dal file con quelli ricevuti via POST dal client, nel caso in
cui trovassimo una corrispondenza, non dovremmo fare altro che restituire la stringa LOGGED.
Tornando al client, non appena esso riceve la risposta dallo script userLogin, esso verifica di aver
ricevuto la stringa LOGGED come risposta:
-
Per quanto rigurada questa tipologia di utenti, che a differenza dei pazienti, ha permessi diversi,
accesso a elementi diversi della piattaforma, e sono generalmente in numero costante ( pi raro
che si debba aggiungere un terapista alla piattaforma, piuttosto che un paziente), si deciso di
sfruttare una delle funzionalit integrate nel webserver utilizzato per ospitare la piattaforma
(Apache 2) per quanto riguarda lautenticazione dei terapisti.
Stiamo parlando del modulo auth_basic e dellapplicazione htpasswd.
Il concetto che sta alla base dellutilizzo di questo modulo quello di:
-
Spostare tutti i file che costituiscono larea ad accesso riservato in una cartella dedicata
allinterno del webserver.
- Definire le credenziali degli utenti che potranno accedere a questarea
- Comunicare al webserver di restringere laccesso ai soli utenti indicati in precedenza.
Nel nostro caso i file da spostare nella directory dedicata ai terapisti, sono quelli che mostrano il
pannello di configurazione e quello di statistiche, stata perci creata sul server una directory
therapist in cui sono stati spostate le due pagine e i file da cui esse dipendono.
A questo punto stato abilitato il modulo auth_basic e riavviato il webserver:
sudo a2enmod auth_basic
sudo apachectl restart
Per creare il file contenente lelenco dei terapisti accettati dal sistema stato utilizzato il comando
htpasswd: esso funziona in maniera del tutto analoga al comando passwd dei sistemi operativi Unix
based, ecco un estratto dal manuale ufficiale di apache che ne spiega lutilizzo:
Il file degli utenti, per questioni di sicurezza stato salvato sul server in una posizione non
raggiungibile da remoto: in /etc/apache2/users ovvero nella cartella in cui risiedono i file di
configurazione del webserver, ecco il comando per laggiunta del primo terapista alla piattaforma:
sudo htpasswd -c users nomeutente
Con lopzione c specifichiamo la necessit di creare il file users, tale opzione verr omessa per la
creazione di tutti i terapisti successivi al primo.
A questo punto non ci resta che salvare un file nascosto allinterno della directory riservata ai
terapisti nominato .htaccess che verr letto dal webserver prima di mostrare il contenuto richiesto
dal client, in questo file esprimiamo la necessit di autenticare gli utenti per visualizzare il contenuto
della cartella:
AuthUserFile /etc/apache2/users
AuthType Basic
AuthName "RESTRICTED ACCESS! Use therapist credential to login ..."
Require valid-user
97
A questo punto il webserver richieder allutente di autenticarsi prima di mostrare il contenuto della
directory.
Ecco la schermata proposta dal sistema:
98
9 - CONCLUSIONI
Con la descrizione della piattaforma per i terapisti, abbiamo concluso la descrizione di tutti gli
elementi che costituiscono il progetto realizzato per la riabilitazione della mano.
Il gioco HotAirBalloon e la piattaforma realizzata sono, ovviamente, ancora a uno stadio preliminare
di sviluppo, ma lintero pacchetto stato installato su un WebServer virtuale predisposto su una
macchina dedicata presso il laboratorio AISLab del dipartimento di Informatica dellUniversit degli
Studi di Milano e reso disponibile al reparto di chirurgia e riabilitazione della mano dellospedale San
Giuseppe con cui abbiamo collaborato, per ulteriori test su alcuni dei pazienti volontari ritenuti pi
adatti dal personale clinico.
A seguito del rilascio della piattaforma, si sono tenuti ulteriori incontri in cui si cercato di
individuare le problematiche relative allutilizzo della piattaforma e ai possibili miglioramenti.
Tuttora si stanno effettuando modifiche alla piattaforma per renderla il pi utilizzabile possibile.
99
Questo tipo di approccio, dovrebbe facilitare la capacit di acquisire dati su pazienti molto giovani
che non sono ancora in grado di giocare con videogiochi strutturati pensati per tablet o console,
come quelli in cura presso il reparto con cui abbiamo potuto lavorare.
NUOVE TECNOLOGIE
Oltre a questi elementi, altri spunti per sviluppi futuri del progetto ci vengono offerti da nuovi device
e tecnologie rese disponibili in questi mesi, due fra tutte:
-
Microsoft Kinect 2, secondo quanto dichiarato dal produttore, e secondo quanto si potuto
osservare in applicazioni commerciali gi rilasciate con luscita di Microsoft XBOX One
sembra essere in grado di tracciare con un alto livello di precisione le mani degli utenti
davanti al dispositivo. Questo permetterebbe di sviluppare giochi pi complessi, che
comprendano movimenti pi articolati e che si possano giocare insieme ad altri.
100
Tutti questi elementi potranno migliorare la qualit dei rilevamenti delle mani dei pazienti
permettendo di realizzare e analizzare esercizi con movimenti sempre pi complessi.
101
10 BIBLIOGRAFIA
1. J. Schell J. The Art of Game Design: Book of Lenses. Elsevier, 2008.
2. Mainetti R, Sedda A, Ronchetti M, Bottini G, Borghese NA. (2013) Duckneglect: videogames based neglect rehabilitation. Technology and Health Care 21 97111 97. DOI
10.3233/THC-120712 IOS Press.
3. NA Borghese, M Pirovano, PL Lanzi, S Wuest and ED de Bruin (2013), Computational
Intelligence and Game Design for effective home-based stroke at Home Rehabilitation.
Games for Health Journal. April 2013, Vol. 2, No. 2: 81-88.
4. M.Pirovano, P.L. Lanzi, R.Mainetti and N.A. Borghese (2013), IGER: A Game Engine
Specifically Tailored to Rehabilitation, Games for Health, Proc. of 3rd Conf. on Gaming
and Playful Interaction in Health Care, B. Schouten, S. Fedtke, T. Bekker, M. Schijven, A.
Gekker Eds., Springer Vieweg.
5. Michele Pirovano, Iuri Frosio, Carl Yuheng Ren, Pier Luca Lanzi, David Murray, N. Alberto
Borghese (2013), Robust Silhouette Extraction from Kinect data, Proc. ICIAP2013,
Springer-Verlag.
6. M. Pirovano, R. Mainetti, G. Baud-Bovy, P.L. Lanzi, N.A. Borghese (2012), Self-Adaptive
Games for Rehabilitation at Home, Proc. IEEE Conference on Computational Intelligence
and Games, 978-1-4673-1194-6/12/$31.00 2012 IEEE pp. 179-186.
ARTICOLI
7. Nunzio Alberto Borghese, Michele Pirovano, Renato Mainetti, Pier Luca Lanzi Seline West
and Eling D. de Bruin - Artificial Intelligence and Game Design for effective at Home
Rehabilitation.
8. N. Alberto Borghese, Renato Mainetti, Michele Pirovano, Pier Luca Lanzi - Artificial
Intelligence and Game Design for effective at Home Rehabilitation.
102
RINGRAZIAMENTI
Per concludere, desidero ringraziare tutte le persone che mi hanno supportato tecnicamente e
psicologicamente durante la stesura della tesi e lintero periodo di lavoro, sollevandoli, ovviamente,
dalla responsabilit di eventuali errori presenti in questo documento.
A partire dal mio relatore, prof. Nunzio Alberto Borghese, che mi ha accolto allinterno del suo team
con entusiasmo, trattandomi alla pari dei veterani del laboratorio, prendendo in ampia
considerazione i miei suggerimenti, e fornendomene a sua volta di preziosi per riuscire a lavorare al
meglio.
Il mio correlatore, il dott. Renato Mainetti, che ha fornito un supporto tecnico straordinario,
aiutandomi a sbloccarmi nei momenti di difficolt, ragionando con me su algoritmi, tecniche e
approcci migliori da seguire nello sviluppo del progetto e nella scrittura di questa tesi.
Tutti i ragazzi, colleghi tesisti e ormai amici dellAISLab, con una menzione particolare per il dott.
Michele Pirovano, per i numerosi consigli e aiuti fornitomi.
Un ringraziamento particolare va anche al team del reparto di chirurgia e riabilitazione della mano
dellOspedale San Giuseppe di Milano, in particolare il Prof. Giorgio Pajardi e le dott.sse Erica Cavalli
ed Elena Mancon, grazie a cui stato possibile realizzare il concept del gioco e dellintera
piattaforma; e ai giovani pazienti e alle loro famiglie che hanno testato, e stanno ancora testando la
piattaforma per aiutarci a evolverla e migliorarla.
Infine, ultimi, ma non per importanza, vorrei ringraziare i miei amici e la mia famiglia, in particolare
mia madre, mio padre e mia sorella per gli stimoli, i consigli e il prezioso sostegno morale (e non
solo), fornitomi in questi anni di carriera universitaria.
103