You are on page 1of 106

Corso di Laurea in Informatica

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

Dott. Renato MAINETTI

Anno Accademico 2013/2014

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 DELL’ARCHITETTURA 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 ALL’INTERNO 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 L’INPUT DELL’UTENTE
i. RILEVARE LE GESTURES PINCH – IN e PINCH – OUT
ii. MISURARE LA DISTANZA DELLE DITA SULLO SCHERMO
iii. VALIDARE L’INPUT DELL’UTENTE
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 ALL’UTENTE
l. HOT AIR BALLOON – SISTEMA DI AUTO-CALIBRAZIONE
8. REALIZZAZIONE DELLA PIATTAFORMA PER I TERAPISTI
i

a. REQUISITI E NECESSITA’ DEI TERAPISTI
b. IL PANNELLO DI CONFIGURAZIONE DEL GIOCO
i. INVIO DELLA CONFIGURAZIONE DAL SERVER AL GIOCO
c. IL PANNELLO DELLE STATISTICHE DEI PAZIENTI
i. INVIO DEI DATI DI SESSIONE DAL GIOCO AL SERVER
d. GESTIONE UTENTI E SICUREZZA
9. CONCLUSIONI
a. RISULTATI OTTENUTI CON HOT AIR BALLOON
b. POSSIBILI SVILUPPI FUTURI
10. BIBLIOGRAFIA

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 l’esecuzione dell’esercizio
riabilitativo è fondamentale. E’ particolarmente importante che il paziente esegua gli esercizi in
modo corretto per evitare l’insorgere 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 all’interno 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 quest’ultimo di svolgere gran parte degli esercizi riabilitativi a casa, in modo
autonomo, ma sempre sotto la supervisione dei terapisti.
Questo approccio è reso oggi possibile, dall’ampia 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 dall’industria videoludica, è possibile mappare
all’interno 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 all’interno dell’esercizio, rendendo il lavoro di
riabilitazione quotidiano più stimolante.
Questo è ciò che viene svolto oggi nell’ambito 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.

1

2 - RIABILITAZIONE
È giusto, prima di procedere oltre, definire cosa si intende per riabilitazione e medicina fisica o
riabilitativa, poiché sarà l’argomento 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 l’esecuzione 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 nell’esercizio 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 nell’esecuzione 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.

2.a - RIABILITAZIONE A CASA: PROGETTO REWIRE
REWIRE è un progetto sostenuto dall’Unione Europea che mira a generare una piattaforma di
riabilitazione multi-livello basata su exer-games (http://www.rewire-project.eu).
Scopo del progetto è quello di creare un sistema personalizzato che possa essere distribuito su
grande scala nelle case di tutti i pazienti che abbiano bisogno di affrontare un processo di
riabilitazione per recuperare funzionalità compromesse.
È un progetto indirizzato a pazienti, dimessi dalle strutture ospedaliere, che hanno bisogno di
continuare a eseguire esercizi di riabilitazione, sotto la sorveglianza remota dei loro terapisti.
La piattaforma REWIRE è costituita principalmente da tre macro-componenti:
-

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 l’ambiente virtuale
senza dover agire su nessun dispositivo fisico quale ad esempio mouse, tastiera, joypads,
joysticks ecc.

2

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 dell’Unviersità 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 l’elaborazione dei dati ricevuti dalle singole Patient
Stations.

-

NETWORKING STATION (NS): Installata a livello regionale, la Networking Station, fornisce
funzionalità per l’analisi avanzata dei dati collezionati dalle esperienze di riabilitazione dei
singoli pazienti su tutte le strutture che utilizzano Rewire, questo permette di individuare e
collezionare percorsi riabilitativi comuni tra diverse strutture, creando una consistente
knowledge base, e permettendo così di migliorare la qualità globale del servizio.

Attualmente REWIRE fornisce principalmente strumenti e minigiochi studiati per la riabilitazione
cognitiva in pazienti affetti da NSU (Negligenza Spaziale Unilaterale), o con disabilità fisiche postictus.
Lo scopo ultimo è quello di supportare e fornire strumenti riabilitativi adatti al maggior numero di
patologie e disabilità possibili.

2.b - IGER: Un game engine per la riabilitazione
È bene, ai fini di comprendere al meglio il progetto descritto in questo documento, spendere due
parole su IGER, il game engine utilizzato all’interno della Patient Station di REWIRE.
Un game engine è un sistema (software) disegnato per la creazione e lo sviluppo di videogiochi. Si
tratta, in sostanza, di una collezione di librerie che forniscono funzionalità atte a risolvere una serie
di problematiche comuni allo sviluppo di quasi tutti i videogiochi.
Tradizionalmente esso si compone di:
-

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 all’interno del gioco.
- Engine Fisico: componente che si occupa di simulare la fisica all’interno del mondo virtuale
ricreato all’interno 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 all’interno del gioco.
- Tools opzionali: quali ad esempio, tool per la gestione dei suoni, le animazioni, l’intelligenza
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 l’esperienza di gioco da un paziente all’altro.
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 l’evoluzione delle abilità psicomotorie del paziente.
- Strumenti per il monitoraggio in tempo reale dei movimenti dei pazienti.
- Strumenti per l’acquisizione 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. D’altro 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.

4

3 - RIABILITAZIONE DELLA MANO
Ispirandosi al modello fino a qui descritto, quello che si è voluto fare in questo progetto è stato
ricreare, in piccolo, questi sistemi applicandoli al caso specifico della riabilitazione della mano.
Questo progetto è stato svolto in collaborazione con l’unità di chirurgia della mano dell’Ospedale S.
Giuseppe di Milano, diretta dal Prof. Giorgio Pajardi. A questo progetto hanno partecipato anche la
Dott.ssa Erica Cavalli e la Dott.ssa Elena Mancon, le quali ci hanno invitato ad alcune sessioni di
riabilitazione con una paziente e aiutato a inquadrare il tipo di lavoro che avremmo dovuto svolgere
per realizzare il progetto. Per quanto riguarda il laboratorio AISLab, hanno collaborato al progetto il
Dott. Renato Mainetti e il Dott. Michele Pirovano.
Innanzi tutto è bene inquadrare il target di pazienti considerato.
Abbiamo avuto la possibilità di sviluppare un mini-gioco riabilitativo per pazienti molto giovani (dai
quattro anni in su) affetti principalmente da agenesia delle dita della mano, o che hanno subito
traumi che hanno portato all’amputazione parziale o totale delle dita.
Tali pazienti hanno subito un intervento di “transfer” delle falangi di alcune dita dei piedi al posto di
quelle amputate o mancanti: scopo di tale operazione è quella di restituire al paziente, almeno
parzialmente, la funzionalità della mano, dando la possibilità di “toccare” e “prendere” oggetti.
Si tratta di un intervento particolarmente consigliato, specialmente a pazienti affetti da agenesia
delle dita, in quanto, essendo nati senza le dita della mano, e trovandosi nella fase iniziale di
apprendimento, che specialmente a quell’età, è particolarmente legata all’interazione con
l’ambiente circostante tramite il tatto e il contatto con gli oggetti, rischiano di sviluppare ritardi
nell’apprendimento.
Anche in questo caso, la fase riabilitativa post-operatoria, è particolarmente lunga, si tratta di periodi
di tempo dell’ordine di diversi anni e ha in questo caso due obbiettivi:
-

Il primo, prettamente fisico, è quello di esercitare il più possibile le dita impiantate,
effettuando dei movimenti di avvicinamento e allontanamento delle dita, rotazione e presa
di oggetti.
- Il secondo, di carattere più neurologico, consiste in una serie di esercizi atti a sviluppare la
consapevolezza nel bambino del nuovo arto e delle sue capacità.
Bisogna notare come il target di utenza sia profondamente differente da quello a cui si riferisce ad
oggi la piattaforma sviluppata all’interno del progetto REWIRE.
Mentre per REWIRE si ha a che fare con pazienti generalmente affetti da danni cerebrali dovuti a
ictus o a pazienti con problemi posturali o di movimento, in ogni caso adulti, in questo caso si ha a
che fare con pazienti molto giovani, bambini, per cui le tecniche di intrattenimento, e motivazione
del paziente vanno rivisitate in un'altra ottica.
E’ importante, soprattutto, strutturare i giochi in maniera diversa, tenendo il conto dell’età
dell’utente medio che lo andrà ad utilizzare, in particolare:
-

-

Il gameplay deve essere adattato alle capacità e all’età del bambino, non bisogna proporre
nulla di molto complesso, ed è fondamentale, per mantenere acceso l’interesse 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 dall’osservazione dei dati ottenuti dalle
misurazioni effettuate durante l’intera 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 d’artificio, colori e
animazioni.

3.a - STRUMENTI DIGITALI GIÀ UTILIZZATI NELLA RIABILITAZIONE DELLA MANO
A seguito dei primi incontri effettuati con i terapisti abbiamo potuto apprendere, che al momento,
vengono già utilizzati in maniera del tutto sperimentale, dei videogiochi durante alcune sessione di
riabilitazione con il bambino.
Si tratta di giochi per tablet, generalmente iPad, sviluppati da terzi a scopo di intrattenimento. I
terapisti, con i genitori dei giovani pazienti, hanno analizzato alcune delle applicazioni offerte nei
vari App Store, scelto quelle che più sembravano adatte al paziente in questione, e hanno provato
ad utilizzarle durante alcune sessioni riabilitative.
Questo tipo di approccio al problema porta sicuramente alcuni vantaggi:
-

Innanzi tutto la moltitudine di applicazioni e giochi presenti nei vari store multimediali, offre
sicuramente un’ampia scelta, questo permette di trovare l’applicazione che più riesce a
catturare l’interesse del bambino e sarà quindi più facile mantenerlo concentrato
sull’esercizio da svolgere.
- L’applicazione installata è disponibile per tutti, senza necessità di infrastrutture particolari,
come ad esempio un server ospedaliero per lo storage dei dati ottenuti dall’applicazione, 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 all’acquisto di un gioco sull’app store è sicuramente irrisorio rispetto a
quello di realizzazione di applicazioni ad hoc.
Vi sono però purtroppo una serie di svantaggi nell’utilizzo di queste applicazioni commerciali,
pensate per l’intrattenimento 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 l’utente in caso di movimenti sbagliati, inoltre
non si considera neanche l’aspetto 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.

6

-

Infine non viene fornita ai terapisti un interfaccia per l’analisi dei dati acquisiti durante il
gioco, riguardanti i movimenti effettuati, per cui non è possibile monitorare eventuali
progressi o peggioramenti del paziente.

3.b - OBIETTIVO DEL PROGETTO
Alla luce di quanto descritto finora, a seguito degli incontri preliminari avuti coi terapisti, è stato
stabilito l’obbiettivo del progetto da realizzare:
Un prototipo di mini-gioco per pazienti a cui sono state già impiantate due dita, l’esercizio sarà quello
di avvicinare le dita come per “pizzicare” (pinching) lo schermo di un dispositivo touch screen (nel
caso specifico dei test eseguiti, si tratta di un tablet), l’applicazione dovrà reagire sia
all’avvicinamento che all’allontanamento delle dita (pinch-in e pinch-out), dovrà essere
configurabile dal terapista tramite un opportuna interfaccia, e dovrà essere in grado di registrare i
movimenti effettuati durante l’intera sessione di gioco, i dati raccolti dovranno poi essere presentati
al terapista mediante un opportuna interfaccia in grado di evidenziare eventuali miglioramenti o
peggioramenti delle abilità motorie del paziente.
L’intero sistema di monitoraggio dovrà essere trasparente al paziente, e il sistema di feedback ingame dovrà essere adattato per essere di facile comprensione a bambini molto piccoli.
Il prototipo verrà realizzato utilizzando HTML5 e Javascript e verrà installato su un webserver in
università, l’applicazione verrà quindi resa disponibile alla struttura e testata su un campione di
pazienti scelto a discrezione dei terapisti.

7

4 - STRUMENTI SOFTWARE UTILIZZATI
Prima di addentrarsi nei dettagli dello sviluppo del progetto, è bene parlare degli studi che sono stati
effettuati su tecnologie già esistenti quali il game engine Panda3D, il software di modellazione 3D
Blender, e le API del Leap Motion controller, scopo di tali studi è stato quello di prendere confidenza
con alcuni degli strumenti utilizzati, ad oggi nello sviluppo di videogiochi sia all’interno del progetto
REWIRE che in ambito commerciale.

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 dell’audio
Un physic engine integrato (oltre a supportarne di più complessi, come ad esempio l’Nvidia
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 dell’intelligenza artificiale nel
gioco, che fornisce ad esempio strumenti per la ricerca di un oggetto nello spazio di gioco,
l’evitare gli ostacoli, il trovare e seguire percorsi per spostarsi da un punto a un altro ecc.

5.a - FUNZIONAMENTO – LOOP DI GIOCO
Il funzionamento basilare di un qualsiasi videogioco mai prodotto è sempre il medesimo, e consiste
sostanzialmente in quattro fasi:
-

Fase di inizializzazione (o init()): è la fase in cui vengono istanziati e inizializzati tutti i
componenti che verranno utilizzati all’interno del videogioco, dal timer, ai giocatori, agli
oggetti inanimati, l’ambiente, suoni, luci ecc.
- Fase di input: è la fase in cui vengono letti dall’esterno gli input del giocatore (per esempio
il movimento di un joystick, la pressione di un tasto).
- Fase di aggiornamento (o update()): è la fase in cui vengono modificati alcuni parametri di
gioco, come ad esempio la posizione dei giocatori.
- Fase di disegno (o draw()): è la fase in cui viene effettivamente disegnato il frame che verrà
mostrato al giocatore, con tutti i componenti e i modelli le cui proprietà sono state definite
nelle fasi precedenti.
In genere la fase di inizializzazione, viene eseguita una sola volta, all’avvio del gioco.
Dopo di che si entra in un ciclo (loop) “infinito” (in realtà non è proprio infinito, ma termina in
condizioni particolari, quali ad esempio la terminazione del gioco), che esegue alternativamente la
fase di aggiornamento e di disegno del frame.
8

La velocità con cui avviene questo ciclo, o meglio l’intervallo di tempo trascorso tra due fasi di
disegno indica il “frame rate” del nostro gioco.
Initialize

In Panda3D, lo schema descritto sopra è ovviamente implementato, ma trasparente al
programmatore.
L’inizializzazione di un nuovo gioco e il conseguente lancio del ciclo di Update e Draw viene fatto in
Python estendendo la classe ShowBase, che inizializza alcuni strumenti utili allo sviluppo del gioco
quali ad esempio:
-

Il loader: utilizzato per caricare qualsiasi tipo di gameObject all’interno del nostro gioco, dai
modelli 3d, alle textures, ai suoni, ecc.
- Il render: che si occupa, di tutto ciò che è grafica all’interno del gioco
- Il taskMgr: per la gestione dei vari task da eseguire all’interno del gioco
- La camera: ovvero la telecamera di default che delimita l’area 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 è l’oggetto 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 dell’oggetto render (o come figlio di uno dei suoi figli), affinché il
renderer di panda3D renderizzi l’oggetto 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 dell’origine 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, all’interno della scena di un gioco 3D, scopo di tale test è
stato quello di capire come effettuare il mapping dell’input ricevuto da un dispositivo esterno
all’interno del gioco.
Per questo tipo di test, sono stati utilizzati due approcci differenti:
-

L’utilizzo di una camera ortografica invece che proporzionale per inquadrare la scena, tale
approccio consente di mappare, senza distorsioni un’area all’interno di uno spazio
tridimensionale, su un piano.
- L’utilizzo 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

5.b - COLLISION DETECTION IN PANDA3D
Il problema del rilevare le collisioni (Collision Detection), è un problema chiave nello sviluppo di un
qualsiasi videogioco, sia esso bidimensionale che tridimensionale.
Ogni qual volta due oggetti “si toccano”, abbiamo una collisione, e se non vogliamo che, ad esempio,
una palla che cade da 10 metri, attraversi semplicemente la superficie e prosegua indisturbata la sua
caduta verso il nulla, dovremmo rilevare la collisione con il suolo e gestirla appropriatamente,
fermando ad esempio la palla all’impatto, o se stiamo realizzando un gioco fedele alle leggi della
fisica, la faccia rimbalzare più volte fino a farla fermare completamente.
Esistono varie tecniche per rilevare in maniera più o meno efficiente la collisione tra due oggetti nei
videogiochi.
In videogiochi bidimensionali molto semplici, spesso viene usato quello che in inglese viene
chiamato “Box Collider”:
Si tratta di costruire un quadrato invisibile attorno agli sprites su cui vogliamo rilevare la collisione
della stessa altezza e larghezza di entrambi gli oggetti, quando i due quadrati si intersecano, allora
si è verificata una collisione.
In Panda3D, vengono forniti sostanzialmente due metodi per gestire il problema delle collisioni:
1. La creazione di geometria dedicata alla collisione.
2. Abilitare il rilevamento delle collisioni su tutta la geometria visibile.
Il primo metodo è di più complessa implementazione, in quanto è necessario costruire una
geometria secondaria dedicata esclusivamente alla collision detection, ma più efficiente in quanto i
poligoni su cui rilevare le collisioni sono, in genere, molti meno, di quelli presenti in tutta la scena di
gioco, e sono, di solito, anche figure poco complesse come cubi, sfere, ecc.
Questo metodo è dunque più adatto ad applicazioni finite, il secondo invece è molto più grezzo, ma
molto più rapido da implementare.
Il secondo metodo, ovviamente, è di più facile gestione, ogni elemento che costituisce la geometria
visibile del gioco, può collidere con qualsiasi altro elemento nel gioco, il vantaggio è ovviamente
dovuto al fatto che non è necessario costruire geometrie dedicate alla collision detection, lo
svantaggio, è che i poligoni visibili all’interno di un gioco di media complessità può essere spesso
molto elevato, per cui il gioco costruito può risultare molto oneroso in termini di risorse di calcolo
richieste per gestire il rilevamento di collisioni su ognuno di essi.
In ogni caso in Panda3D, il sistema di collisioni ruota sostanzialmente attorno ai seguenti oggetti:
-

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.

-

CollisionTraverser, che è l’oggetto fondamentale del sistema di collisioni di Panda3D, si
tratta in sostanza di una struttura dati che colleziona al suo interno tutti i collider creati dal
programmatore nella scena: un collider è sostanzialmente, una coppia oggetto
(generalmente un collisionSolid) / handler.
Ecco un esempio del sistema di collisioni di panda3D in funzione, le collisionSphere intorno alle due
sfere sono state scalate in modo da essere un po’ più grandi dei modelli visibili, in modo da essere
facilmente visibili:

5.c - MAPPATURA DI INPUT BIDIMENSIONALE NELLO SPAZIO: CAMERA ORTOGRAFICA
Ora che abbiamo visto come funziona il sistema di collision detection in PANDA3D, possiamo tornare
al problema originario, ovvero quello di mappare il movimento del mouse, che avviene su un piano
bidimensionale, all’interno della scena di gioco in panda3D, mappato di default su un ambiente a 3
dimensioni.
La prima delle due tecniche elencate prima era quella della camera ortografica.
12

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 l’ambiente, come
farebbe una telecamera reale, o come farebbe l’occhio umano; con una lente ortografica, che
elimina di fatto gli effetti prospettici, mantenendo le linee di proiezione perfettamente parallele fra
di loro.
L’effetto ottenuto dall’utilizzo 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, all’interno della
scena di gioco.
Per testare questa tecnica è stato realizzato un mini-gioco in cui una pallina doveva seguire il
puntatore del mouse all’interno 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 all’interno della nostra scena:
Sappiamo che Panda3D mappa la posizione del mouse con dei valori decimali compresi tra -1 e 1 sia
sull’asse 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 dell’area ripresa,
è setFilmSize.
Una volta definita la dimensione dell’area ripresa si tratterà solamente di convertire le coordinate
del mouse, comprese tra -1 e 1 in coordinate spaziali, comprese tra -1 e 1 sull’asse delle ascisse e 0.75 e +0.75 sull’asse delle ordinate (questo valore per le ordinate è dipendente dall’aspect 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 l’asse Y è realizzata semplicemente sapendo che l’aspect 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

Vediamo la parte che ci interessa:
def trackMouse(self, task):
# Track the mouse only if it's in the window otherwise we get errors
if(base.mouseWatcherNode.hasMouse() == True and self.trackingEnabled == True):
mousePos = base.mouseWatcherNode.getMouse()
self.mouseVector = Vec3(mousePos.getX(), mousePos.getY(), 0)
# Mouse position in X goes from -1 to 1
tX = self.mouseVector.getX()
# Since camera aspect ratio is 4:3, mouse relative position in our world
# will be 4/3 of its absolute position
tY = (self.mouseVector.getY() / 4) * 3
newWaypoint = Point3(tX,tY,0)
# etc. etc. . . . . .

Ed ecco uno screenshot del gioco finito, da notare come la posizione della pallina corrisponda alle
attuali coordinate del puntatore del mouse:

5.d - MAPPATURA DI INPUT BIDIMENSIONALE NELLO SPAZIO: RAY CASTING
La tecnica della camera ortografica vista nel capitolo precedente, semplifica di molto il problema
della mappatura di devices bidimensionali, o che utilizzano semplicemente un sistema di coordinate
differente rispetto a quello di panda3D, all’interno della scena di gioco; ma allo stesso tempo
impedisce la realizzazione di giochi completamente tridimensionali: questo è dovuto alla natura non
14

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 all’interno 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

# This generates a collisionRay and attaches it to the active camera, then it
# returns a nodePath to that ray
def setupMouseRayCastingLogic(self):
collisionNode = CollisionNode('Mouse Ray collision node')
self.pickerRay = CollisionRay()
collisionNode.addSolid(self.pickerRay)
return base.camera.attachNewNode(collisionNode)

Da notare, l’ultima 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
l’orientamento del nostro raggio tramite un comodo metodo setFromLens():

Tale metodo setta l’origine del collisionRay su cui viene chiamato nel punto corrispondente alle
coordinate nel range [-1,1] sul piano focale minimo (ovvero sul piano più vicino all’osservatore, 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 l’evento 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 l’utente 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 all’interno del
game engine, un dispositivo di input differente, il Leap Motion Controller.

17

6 - LEAP MOTION CONTROLLER
Il Leap Motion Controller, è un dispositivo di input, pensato e sviluppato per il tracciamento
dettagliato delle mani e di tutte le dita dell’utente, senza richiedere nessuna forma di contatto fisico
con il dispositivo.
Il funzionamento è molto simile a quello del Microsoft Kinect, ma a differenza di quest’ultimo, il cui
scopo è principalmente quello di tracciare l’intero corpo degli utenti nel suo raggio d’azione, l’area
analizzata dal leap motion controller è molto ristretta, questo permette di ottenere una risoluzione
superiore a quella di kinect, che può lavorare, in near mode, a 1 metro di distanza dal soggetto,
contro il range di interazione di 7 - 25 cm del Leap, ciò rende il dispositivo potenzialmente adatto a
tracciare i movimenti della mano e la posizione delle singole dita nel dettaglio.

6.a - PROGRAMMARE IL LEAP – API
Leap Motion fornisce agli sviluppatori una serie di librerie scritte in diversi linguaggi per interfacciarsi
con il dispostivo: poiché scopo dello studio effettuato era quello far funzionare il leap con Panda3D,
sono state analizzate le librerie Python.
Il concetto che sta dietro alla comunicazione con il leap, è quello di creare una propria classe,
sottoclasse di Listener, che implementi le funzioni che vengono richiamate per gestire i diversi tipi
di eventi che possono essere sollevati dal device, vediamo i quelli fondamentali:
-

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 dall’evento sollevato quando
viene ricevuto un nuovo frame dal dispositivo, all’interno 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 dell’utente.
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:
-

La classe FRAME: è probabilmente la classe più importante, essa contiene tutte le istanze di
tutti i singoli oggetti rilevati nel frame corrente, quali ad esempio: quante mani sono state
rilevate, quante dita, quanti strumenti (come matite, penne, ecc.), quanto è grande al
momento l’area monitorata dal dispositivo, i frame al secondo generati attualmente, ecc.

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 dell’hand tracking: le
“gestures”.
Per gesture si intende un tipo di movimento molto comune, ricorrente o che ha un intenzione
particolare che può essere compiuto dall’utente.
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:

6.b - PRIMI TEST
Scopo del test è stato quello di prendere confidenza con le API fornite dal produttore del dispositivo,
si è realizzata una semplice applicazione a riga di comando che a ogni frame verificava l’inclinazione
di una mano posizionata sopra il dispositivo e stampava su standard input stringhe come “La mano
punta verso il basso di … gradi” oppure “la mano è inclinata verso sinistra di … gradi”, ecc.
Ovviamente il cuore di un applicazione così semplice sarà solamente l’implementazione del metodo
on_frame(), di cui riportiamo il codice;
Il codice del main è poco interessante, conterrà unicamente la logica di inizializzazione del nostro
listener e un loop “infinito”, che manterrà il programma attivo fino alla pressione di un tasto,
vediamo dunque la parte interessante:

19

# Questo è l'evento che ci interessa realmente, ogni volta che il listener fa
polling sul leap genera un frame che contiene tutte le info
# su ciò che il dispositivo è riuscito a trackare
def on_frame(self, controller):
# Recupero il frame corrente
frame = controller.frame()
# Controllo che ci sia almeno una mano:
if(not frame.hands.is_empty):
print("%d Mano/i Rilevata/e" % (len(frame.hands)))
# Recupero la prima mano, hands è un vettore
hand = frame.hands[0]
# la direzione della mano intesa dal palmo verso le dita
direction = hand.direction
# è la normale del palmo della mano, un vettore che
# punta verso il basso
normal = hand.palm_normal
pitch = direction.pitch * Leap.RAD_TO_DEG
roll = normal.roll * Leap.RAD_TO_DEG
yaw = direction.yaw * Leap.RAD_TO_DEG
if(pitch > 0):
print("La mano punta verso l'alto di %f gradi" % (pitch))
else:
print("La mano punta verso il basso di %f gradi" % (pitch))
if(roll < 0):
print("La mano e' inclinata verso destra di %f gradi" % (roll))
else:
print("La mano e' inclinata verso sinistra di %f gradi" %
(roll))
if(yaw > 0):
print("La mano e' ruotata verso destra di %f gradi" % (yaw))
else:
print("La mano e' ruotata verso sinistra di %f gradi" % (yaw))
else:
print("Metti la mano sopra il leap!")

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 l’istanza 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 all’attributo 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 all’interno 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
all’interno di panda3D.
Una delle difficoltà riscontrate nell’utilizzo del leap all’interno del loop di gioco è stata quella di
sincronizzare l’input 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 all’interno 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 all’area scansionata dal dispositivo lungo l’asse Z, questo ci permetterà
di settare la velocità dell’aereo in funzione della posizione della mano: più la mano è vicina al bordo
superiore dell’area 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 l’unico scopo di
semplificarne l’utilizzo all’interno di panda3D.
Degno di nota è anche il metodo normalized_point() della classe InteractionBox (classe che
rappresenta l’attuale 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 all’estremità inferiore dall’area nel range attuale del
dispositivo e 1 il punto all’estremità superiore per ognuno dei 3 punti cardinali dello spazio.

Ecco di seguito il codice:

22

def on_frame(self, controller):
# Recupero il frame corrente
frame = controller.frame()
# Controllo che ci sia almeno una mano:
if(not frame.hands.is_empty):
# Recupero la prima mano, hands e' un vettore
hand = frame.hands[0]
# la direzione della mano intesa dal palmo verso le dita
direction = hand.direction
# E' la normale del palmo della mano, un vettore che punta verso il
# basso
normal = hand.palm_normal
# Rilevo l'attuale rotazione della mano
self.handPitch = direction.pitch * Leap.RAD_TO_DEG
self.handRoll = normal.roll * Leap.RAD_TO_DEG
self.handYaw = direction.yaw * Leap.RAD_TO_DEG
# Estraggo la posizione della mano sull'asse Z
interactionBox = frame.interaction_box
# Ottengo un vettore che rappresenta la posizione del palmo
# normalizzata in relazione all'attuale
# interaction_box
normalized_palm_position =
interactionBox.normalize_point(hand.palm_position)
# Il leap usa un sistema di coordinate per cui l'avanti e' 0
# e indietro e' 1, a me serve il contrario
self.reversed_normalized_palm_position =
1 - normalized_palm_position.z
else:
print("Metti la mano sopra il leap!")
self.prevFrame = frame

Costruito il nostro listener personalizzato, vediamo ora come utilizzare i dati ottenuti dal listener per
far muovere l’aeroplano 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 l’inclinazione 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

def move(self, task):
# Get target values for pitch roll and yaw,
targetPitch = self.inputListener.handPitch;
targetRoll = self.inputListener.handRoll;
targetYaw = self.inputListener.handYaw;
# If values are set to None, simply terminate the task
if(targetPitch is None or targetRoll is None or targetYaw is None):
return task.cont
# Yaw and Roll values are inverted in leap, so
# we need to change their sign
targetRoll *=-1
targetYaw *=-1
# Get dt to move smoothly
dt = globalClock.getDt()
# We want to rotate smoothly towards the target so we don't
# simply set the player Hpr to the values
# obtained from the inputListener, but we rotate a certain
# number of degrees per second towards the target
# Smoothly change heading
if(self.playerRootNP.getH() < targetYaw):
newH = self.playerRootNP.getH() - self.yawSpd * dt
if(newH > targetYaw):
newH = targetYaw
if(self.playerRootNP.getH() > targetYaw):
newH = self.playerRootNP.getH() - self.yawSpd * dt
if(newH < targetYaw):
newH = targetYaw
else:
newH = self.playerRootNP.getH()
# Smoothly change pitch
if(self.playerRootNP.getP() < targetPitch):
newP = self.playerRootNP.getP() + self.pitchSpd * dt
if(newP > targetPitch):
newP = targetPitch
if(self.playerRootNP.getP() > targetPitch):
newP = self.playerRootNP.getP() - self.pitchSpd * dt
if(newP < targetPitch):
newP = targetPitch
# Smoothly change roll
if(self.playerRootNP.getR() < targetRoll):
newR = self.playerRootNP.getR() + self.rollSpd * dt
if(newR > targetRoll):
newR = targetRoll
if(self.playerRootNP.getR() > targetRoll):
newR = self.playerRootNP.getR() - self.rollSpd * dt
if(newR < targetRoll):
newR = targetRoll
# Rotate the plane
self.playerRootNP.setHpr(newH,newP,newR)
# Check Throttle
self.checkThrottle()
# This actually moves the plane
self.playerRootNP.setPos(self.playerRootNP, 0,
self.throttle * self.playerMaxSpd * dt,0)
self.throttleLabel.setText("Throttle: %.1f" %(self.throttle * 100))
return task.cont

Come è possibile osservare dal codice del “Move Player Task” creato nella classe Player, la gestione
dell’input è affrontata in maniera piuttosto semplice:
Per prima cosa si accede all’inputListener 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 dell’aeroplano.
24

A questo punto tramite semplici operazioni matematiche si convertono i dati ricevuti dal dispositivo
in dati utilizzabili all’interno del gioco, per fare un esempio, è possibile osservare come i gradi di
rollio e imbardata dell’aeroplano arrivino dal dispositivo con segno opposto a quello utilizzato
all’interno 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 l’esatta posizione dell’aeroplano.
Il problema di questo approccio è che i movimenti dell’aereo risulterebbero troppo repentini per
essere realistici in caso di movimenti molto rapidi della mano dell’utente; il codice scritto qua sopra
cerca di ovviare al problema in maniera molto semplice: l’attuale posizione della mano non è la
posizione attuale dell’aereo, ma la posizione in cui l’aereo deve arrivare.
Come è possibile osservare, l’aereo ha delle velocità di rotazioni impostate per ogni asse: il valore
ricevuto in input è l’angolo a cui l’aereo deve arrivare (ad esempio targetRoll), se la rotazione attuale
dell’aereo è inferiore o superiore rispetto all’angolo obiettivo allora bisogna ruotare l’aereo verso
tale angolo della sua velocità.
In questo modo si ottiene un movimento molto più fluido e naturale.

6.c - INTRODURRE NUOVE GESTURE – GRABBING
Come è stato detto nella parte di introduzione al Leap Motion Controller, al momento le API fornite
dal produttore del dispositivo non forniscono strumenti semplici per l’introduzione di gestures
personalizzate e le gestures rilevate di default dal dispositivo sono molto poche e limitate, in
25

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 dall’immagine, 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 l’ambiente 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,
l’eventualità di una possibile gesture di presa, e il conseguente rilascio.
Per prima cosa bisogna identificare un modo per poter segnalare all’utente l’effettivo 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 all’interno 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 l’utente ha
effettuato una presa e a false quando non l’ha effettuata oppure l’ha 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 all’interno 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 nell’ambiente 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 l’utente
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 l’utente 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 l’evento di grabbing, allora una pallina sulla superficie comincerà a
seguire la nostra mano:

6.d - IL LEAP MOTION CONTROLLER E LA RIABILITAZIONE
Dai test effettuati, il Leap Motion Controller si è dimostrato essere uno strumento particolarmente
valido per la realizzazione di videogiochi per la riabilitazione della mano, in quanto permette un
tracciamento preciso e molto rapido della posizione di ogni singolo dito, del palmo, della velocità di
movimento e ciò permette di sviluppare, con questi dati, moduli per il rilevamento di diverse
gestures adeguate per i singoli esercizi richiesti dai terapisti.
Nel caso specifico della riabilitazione della mano dei bambini, con cui abbiamo avuto a che fare in
questo periodo di lavoro, però, è stato deciso di non adottare il dispositivo, almeno nella fase iniziale,
per i seguenti problemi riscontrati:

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 l’utente a
operare sul leap senza guardarlo, il che porta spesso i bambini a spostarsi al di fuori del
raggio d’azione del dispositivo rendendo non fluido l’input 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 l’ambiente.
- 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 all’interno 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

7 - REALIZZAZIONE DEL GIOCO: HOT AIR BALLOON
Per quanto riguarda il progetto descritto nelle prossime pagine si è preferito utilizzare dei comuni
tablet, molto più diffusi del Leap Motion Controller, per una serie di motivazioni:
-

Abbiamo innanzi tutto potuto notare come i giovani pazienti abbiano già sviluppato un
altissima dimestichezza nell’utilizzo di questi dispositivi con applicazioni commerciali,
questo semplifica di molto la fase di apprendimento nell’utilizzo dell’applicazione 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
l’utente le sollevi realmente dal dispositivo.
Si è cercato, come vedremo nel prossimo capitolo, di sviluppare un’applicazione che fosse
compatibile con il maggior numero di dispositivi, pertanto, l’intero gioco è stato sviluppato come
applicazione web in HTML5 e Javascript.

7.a - SCELTA DELL’ARCHITETTURA WEB-BASED
Le motivazioni di questa scelta sono molteplici:
Innanzitutto, volendo realizzare un’applicazione per la riabilitazione a casa a bassissimo costo, l’idea
è stata quella di dare la possibilità ai pazienti di sfruttare qualsiasi dispositivo di cui fossero già in
possesso a casa, per utilizzare l’applicazione sviluppata, gli unici requisiti sarebbero stati una
connessione ad internet, un browser web e uno schermo touchscreen, di cui sono dotati ad oggi
tutti i più comuni smartphone e tablet.
Inoltre prototipare un videogioco utilizzando le canvas di HTML 5 e javascript, è molto più rapido
che sviluppare lo stesso nativamente per ciascun dispositivo da supportare, questo ha permesso di
presentare ai terapisti in un tempo relativamente breve, un prototipo di gioco da testare sui pazienti.
L’architettura web-based presenta inoltre una serie di vantaggi anche per quanto riguarda lo
sviluppo dell’interfaccia di configurazione e analisi dei dati da fare utilizzare ai terapisti.
Anche in questo caso non è necessario installare nulla in ospedale per permettere ai terapisti di
accedere ai dati dei singoli pazienti, ma è sufficiente sfruttare un qualsiasi terminale con accesso a
internet per accedervi. Tutti i dati dei pazienti vengono mantenuti anonimamente sul server e sono
sempre a disposizione dei terapisti
I vantaggi dell’adottare questa architettura sono quindi: completa indipendenza dal client, velocità
di sviluppo, gestione centralizzata dei dati dei pazienti, la possibilità di creare interfacce web per la
configurazione e analisi dei dati relativi ai pazienti in maniera molto rapida ed efficace.
Tale approccio presenta però alcuni svantaggi:
Innanzi tutto un gioco sviluppato per un browser web non può essere utilizzato in assenza di
connessione a internet o con connessioni molto lente, ciò può non essere adatto per pazienti che
abitano in luoghi con accesso difficoltoso alla rete.

30

Le capacità delle librerie grafiche per web per quanto molto avanzate, sono ancora lontane
dall’essere 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 l’intera 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.

7.b - IL PROGETTO DA REALIZZARE
A seguito di una serie di incontri preliminari svolti con i chirurghi che si occupano di operare i
pazienti, con i fisioterapisti che si occupano della riabilitazione, con una piccola paziente di 4 anni e
i suoi genitori, è stato deciso come procedere nella realizzazione del prototipo del primo videogioco.
La paziente, molto giovane, affetta da agenesia delle dita di una mano, aveva subito qualche mese
prima un operazione di transfer delle falangi di due delle dita dei piedi sulla mano.
Abbiamo potuto assistere a una sessione di riabilitazione:
Essa si divide sostanzialmente in due parti, una prima parte consiste in una serie di esercizi fisici in
cui al paziente, attraverso il gioco, vengono fatti maneggiare oggetti di diversa consistenza e
dimensione per realizzare costruzioni, tale processo serve a stimolare il processo di corticalizzazione
del nuovo arto.
La seconda parte, cognitiva, serve a fare prendere al paziente consapevolezza della sua nuova mano,
e del fatto che essa può essere usata in maniera funzionale per diversi scopi.
Come già accennato in precedenza, ci è stato mostrato come i terapisti abbiano già provato ad
utilizzare applicazioni commerciali per tablet all’interno della sessione di riabilitazione.
È stato interessante osservare come la piccola paziente utilizzasse queste applicazioni: si trattava in
genere di giochi senza uno scopo preciso, a cui ad una qualsiasi azione corrispondeva una reazione:
applicazioni di disegno, o semplici giochi in cui toccando elementi della scena mostrata succedeva
qualcosa.
Tali applicazioni, come spiegato in precedenza, avevano il difetto di non richiedere un movimento in
particolare, per cui non potevano essere utilizzate per svolgere e valutare un esercizio in particolare,
ma solo per far esercitare e muovere la mano in libertà alla paziente.
A seguito delle osservazioni svolte durante la sessione di riabilitazione, abbiamo avuto un colloquio
con i terapisti, con i quali abbiamo identificato un movimento da tracciare con il primo gioco da
realizzare.
Il movimento in questione è quello di avvicinamento e allontanamento delle dita della mano “a
pizzico” (in inglese “pinching”): questo è ad esempio il movimento, molto spesso richiesto, da

31

applicazioni per la gestione di fotografie, per la scalatura delle immagini: allontanando le dita si
“zoomma” ovvero si ingrandisce l’immagine, 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 l’analisi 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 dell’intera piattaforma per i terapisti.

7.c - HTML 5 – TAG CANVAS E REALIZZAZIONE DEL LOOP DI GIOCO IN JAVASCRIPT
Elemento fondamentale per la realizzazione di videogiochi basati sul web ed eseguibili su un
qualsiasi browser web, è l’elemento Canvas.
Si tratta di un estensione dell’HTML, che permette, tramite un linguaggio di scripting, nel nostro caso
Javascript, il rendering dinamico di immagini bitmap.
Il tag html canvas definisce un area di altezza e larghezza definiti su cui è possibile, tramite API
javascript dedicate disegnare dinamicamente, così come viene fatto utilizzando in altri ambiti API
per la grafica bidimensionale.
Di per sé il tag canvas delimita unicamente un contenitore di grafica bidimensionale, e le API ad esso
associate sono unicamente API grafiche, il contenitore non è predisposto di per se ad ospitare un
videogioco.
Come è possibile, allora, realizzare animazioni sfruttando questo elemento?
Il concetto è di per sé abbastanza semplice:
Si scrive all’interno di una funzione, che d’ora in poi chiameremo DRAW, che verrà richiamata
costantemente ogni tot millisecondi tramite la funzione javascript “setInterval(function, time)”
Questa funzione draw, non farà altro che cancellare come prima cosa, l’intero contenuto del canvas,
e ridisegnarlo subito dopo con opportune modifiche.
In questo modo genereremo una serie di frame, il cui framerate sarà regolato dal parametro “time”
che passeremo alla funzione setInterval.
Trattandosi i giochi sostanzialmente di animazioni interattive, il passo tra il realizzare un animazione
e un semplice videogioco è molto breve, vediamo quindi come ricreare il loop di gioco, la cui
struttura è stata descritta nel capitolo sugli studi preliminari, in un’applicazione web in javascript.
7.c i - DEFINIRE LA ZONA DI GIOCO ALL’INTERNO DELLA PAGINA WEB
Per prima cosa dobbiamo dedicare una zona della nostra pagina web a ospitare il gioco, per fare ciò
utilizziamo il tag canvas.
Bisogna ricordarsi di assegnare un id univoco all’elemento e fissare altezza e larghezza:

32

<canvas id="myCanvas" width="800", height="600" style="border 1px solid black">
OPS! Your broser doesn't seem to support canvas!
</canvas>

Come è possibile notare, è sufficiente scrivere del testo all’interno del tag canvas per gestire
l’eventuale accesso all’applicazione 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 l’elemento canvas definito all’interno della nostra pagina HTML attraverso il suo id,
recuperiamo l’oggetto “context”, che utilizzeremo all’interno 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 l’aggiornamento 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 all’interno 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 nell’esempio 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 l’oggetto 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.

7.d - HOT AIR BALLOON – DISEGNARE SUL CANVAS
Uno dei primi problemi affrontati nello sviluppo del gioco è stato quello di passare dalla struttura
descritta nel capitolo precedente, a qualcosa di visualizzabile e giocabile.
Il tag canvas, di per sé è solamente un contenitore di contenuti grafici, non possiede di suo nessuno
strumento adatto a disegnare, per fare ciò è necessario utilizzare un linguaggio di scripting, come
javascript e l’oggetto javascript che ci permette di disegnare sul nostro canvas è il “Graphic Context”:
recuperabile con il metodo canvas.getContext(‘2d’); tale oggetto permette al programmatore di
disegnare proceduralmente diverse figure all’interno del canvas, di cancellare disegni effettuati in
parte o nella totalità di esso, scrivere del testo, e disegnare immagini da file sia intere che parziali.
I metodi forniti da questo importante oggetto sono diversi e di diversi tipi:
METODI DI CREAZIONE
-

Rect(): crea un rettangolo di dimensioni date, nella posizione specificata
Arc(): crea un arco di lunghezza e ampiezza specificata: notare come un ampiezza di 2 x Pi
Greco, formi un cerchio perfetto.
METODI DI DISEGNO
Sono sostanzialmente i metodi fill e stroke applicati ai vari elementi creati, ad esempio:

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 l’intero contesto ai valori passsati come parametri
- rotate(): ruota l’intero contesto all’interno 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 sull’oggetto da disegnare,
ma sull’intero 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.

METODI PER LO SWITCH DI CONTESTO
-

save(): salva il contesto attuale
restore(): ripristina il contesto precedentemente salvato con il metodo save()

Ora che conosciamo gli strumenti basilari per disegnare all’interno del nostro canvas, è bene
spendere qualche parola sul sistema di coordinate utilizzato per posizionare i disegni all’interno di
questo contenitore:
[IMMAGINE]
Come è possibile osservare il punto di origine della nostra “tela” si trova nell’angolo superiore
sinistro, la distanza da questo punto misurata in pixel: sia sull’asse X che sull’asse Y ci fornisce un
metodo univoco per posizionare i nostri oggetti sulla scena.
Ad esempio se volessimo disegnare un pallino qualcosa esattamente nell’angolo superiore destro di
un canvas di 800x600px, allora posizioneremo il nostro pallino nel punto: (800,0).

7.e - IL PRIMO SFONDO E LA MONGOLFIERA
Ora che conosciamo meglio l’elemento canvas e il graphic context, possiamo tornare ad
HotAirBalloon, la prima cosa da fare è disegnare lo sfondo della nostra scena, e in seguito posizionare
la mongolfiera, che verrà fatta muovere al giocatore.
Partiamo dallo sfondo, si tratta di un semplice sfondo statico, una singola immagine da mostrare
all’interno dell’intero canvas: la sequenza di operazioni da svolgere è la seguente:
-

Creare un oggetto di tipo immagine, vuoto all’interno del DOM html, questo in javascript si
può fare sfruttando la classe standard “Image”
Caricare effettivamente l’immagine da file, che risiede sul server, e impostarla come
sorgente, dell’immagine appena creata
Scalare l’immagine per entrare comodamente all’interno del nostro contenitore
35

- Disegnare l’immagine a ogni frame.
Prima di fare ciò, però, è necessario fare una riflessione: il target della nostra applicazione web, sono
i dispositivi mobili, in particolare tablet, il paziente mentre gioca non dovrà avere la sensazione di
trovarsi all’interno di una pagina web, ma di una vera e propria applicazione, per tanto sarebbe
opportuno che il nostro canvas, non avesse dimensioni predefinite, ma occupasse l’intera pagina
visualizzata.
Javascript fortunatamente, ci fornisce due proprietà particolarmente utili al nostro scopo, esse sono:
- window.innerWidth
- window.innerHeight
Queste due proprietà ci forniscono la dimensione, in pixel dell’area che può ospitare del contenuto
all’interno della nostra pagina: in sostanza, ci fornisce la dimensione della zona bianca della pagina
web dove possiamo inserire contenuto, senza che l’utente debba scrollare verticalmente o
orizzontalmente la pagina.
In questo modo sarà sufficiente, impostare le proprietà:
- canvas.width = window.innerWidth
- canvas.height = window.innerHeight
Per occupare l’intera pagina visualizzata con il nostro canvas.
Ora che abbiamo ridimensionato correttamente il nostro contenitore, possiamo procedere con lo
sfondo:
Bisogna innanzi tutto fare una considerazione sulla scelta di questo elemento: ora il nostro canvas,
non ha più dimensioni note, ma esse saranno strettamente dipendenti dalla dimensione dello
schermo del dispositivo utilizzato dall’utente,
Sarà possibile adattare la dimensione dello sfondo in funzione della dimensione del canvas
accedendo alle proprietà width e height di quest’ultimo, ma dobbiamo pensare che esso potrebbe
essere, allargato o ristretto senza rispettare le sue proporzioni per poter occupare l’intera area su
cui si estende il canvas.
Per questo motivo il motivo rappresentato nello sfondo dovrà essere il più neutrale possibile, per
evitare che l’utente si accorga di eventuali deformazioni dell’immagine:
Un cielo con delle nuvole, paesaggi collinari, ecc. possono fare al caso nostro in quanto, in caso di
allargamento dell’immagine senza mantenere le proporzioni, ci ritroveremmo semplicemente con
nuvole un po’ più allungate o colline un po’ più piatte, ma nulla di estremamente innaturale.
Ecco quindi come caricare e disegnare il nostro sfondo:
-

All’interno del costruttore della nostra classe game, creiamo l’immagine e le assegniamo la
sorgente.

this.takeoffbg = new Image();
this.takeoffbg.src = "sprites/bg.jpg";

All’interno 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 nell’area definita dal rettangolo passato come argomento.
Il metodo beginPath() è un metodo di servizio che permette di comunicare al context l’inizio di un
nuovo tratto da disegnare.
Il metodo drawImage(), infine, serve per disegnare la nostra immagine, prende come parametro
l’immagine 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
nell’immagine seguente:

C’è però purtroppo un problema nascosto in quanto scritto, dovuto alla modalità in cui vengono
caricate le immagini all’interno 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 l’immagine “sprites/bg.jpg” dal server, viene
lanciato il caricamento del file, quindi il download dell’immagine dal server alla macchina client, ma
l’esecuzione del codice, non viene interrotta, questo vuol dire che spesso ci si ritroverà a eseguire
l’istruzione successiva a quella di caricamento, prima che il client sia realmente in possesso
dell’immagine richiesta.

37

ASYNC
LOAD IMAGE

ACCESS IMG

DOWNLOAD

ERROR

NEXT ISTR

DATA
READY

In che modo questo può generare problemi?
Supponiamo di aver lanciato il gioco da client, a un certo punto il nostro codice istanzierà la nostra
variabile background, e richiederà il download dell’immagine, il download però è particolarmente
lento, ciò può accadere per diversi motivi: sovraccarico del server, connessione di qualità scadente,
ecc.
Il download non è ancora completato, quando il nostro codice, che gira lato client, e che ha
proseguito la sua esecuzione, arriva a eseguire per la prima volta il metodo draw, che ha al suo
interno l’istruzione drawImage(), che richiede che l’immagine sia pronta, ma l’immagine è ancora in
download, cosa succede allora? Il risultato non è prevedibile!
Quello che è certo è che ovviamente, questa situazione, crea una serie di problemi all’interno del
gioco!
Come si può ovviare al problema?
Una delle tecniche più adatte è quella che prevede di effettuare il preloading degli sprites, ovvero
caricare le nostre sprites prima che esse vengano utilizzate.
Per fare ciò, in javascript è possibile assegnare una funzione di callback all’attributo “onload”,
presente in qualsiasi oggetto di tipo Image.
Tale callback, come suggerisce il nome, verrà chiamata non appena verrà catturato l’evento di
caricamento completato dell’immagine.
A questo punto, quindi possiamo riscirvere il nostro codice, per gestire anche questo problema:
-

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 all’attributo onload del nostro sfondo, il cui unico
scopo sarà settare a true la variabile definita prima.
38

-

Verificheremo lo stato della variabile takeOffBgReady per scegliere se disegnare o meno il
nostro sfondo.
Ecco il codice aggiornato del nostro costruttore:
var takeoffBgReady = false;
this.takeoffbg = new Image();
this.takeoffbg.src = "sprites/bg.jpg";
this.takeoffbg.onload = function(e){
takeoffBgReady = true;
}

E del metodo draw:
this.draw = function(self){
// FRAME DRAW LOGIC HERE
self.ctx.clearRect(0,0,window.innerWidth, window.innerHeight);
self.ctx.beginPath();
if(takeoffBgReady === true){
self.ctx.drawImage(self.takeoffbg ,0,0,
window.innerWidth, window.innerHeight);
}
}

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 un’ulteriore 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 all’interno 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à all’interno 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 dell’immagine, e solo a caricamento completato tale variabile viene settata a true.
La classe giocatore, così come l’abbiamo 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.
L’utilizzo di questa classe è particolarmente semplice, inseriremo nel costruttore del nostro gioco,
una variabile player che ospiterà un’istanza della nostra classe Player, e all’interno del nostro
metodo draw, chiameremo il metodo draw della classe Player, per disegnare il nostro giocatore.
this.player = new Player(0,0);

E

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 dall’angolo 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 l’istruzione di creazione del
giocatore:
this.player = new Player(0,0);

Potremmo trovarci nella situazione in cui l’immagine 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 l’immagine sia pronta, prima di settare la posizione iniziale del
giocatore, il problema che si pone a questo punto è “quanto aspettare?”
Potremmo monitorare, all’interno 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 dell’oggetto player.
È stata adottata questa seconda opzione:

41

Per prima cosa è necessario modificare come segue la funzione di callback chiamata al caricamento
completo dell’immagine:
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, all’interno del costruttore della classe listener, sarà sufficiente predisporre un
listener appropriato, che, al rilevamento dell’evento “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 all’interno del gioco è stato gestito il problema del preloading delle
sprites, e del caricamento asincrono dei contenuti, per qualsiasi altro elemento utilizzato all’interno
del gioco è stato utilizzato il medesimo approccio.
Al momento abbiamo caricato la mongolfiera del giocatore, e l’abbiamo posizionata nella posizione
iniziale, ecco il risultato:

42

Quello che verrà affrontato ora è uno dei temi centrali dell’intero progetto, la gestione dell’input
dell’utente attraverso lo schermo touch screen del dispositivo utilizzato.

7.f - HOT AIR BALLOON – RILEVARE E VALIDARE L’INPUT DELL’UTENTE
I problemi da affrontare a questo punto dello sviluppo del gioco sono sostanzialmente tre, dobbiamo
infatti essere in grado di:
-

Rilevare la posizione delle dita sullo schermo
Rilevare l’evento di pinch-in e pinch-out in utenti con mobilità ridotta, che quindi
difficilmente riusciranno a compiere movimenti ampi e facilmente distinguibili.
- Essere in grado di misurare a ogni istante la distanza: in cm, tra le dita attualmente sullo
schermo, questi dati saranno fondamentali, in seguito, per realizzare statistiche sulla qualità
del movimento effettuato.
Analizziamo queste problematiche una per volta:
RILEVARE LA POSIZIONE DELLE DITA SULLO SCHERMO
Il W3C definisce le specifiche per i touch events come un insieme di eventi a basso livello che servano
a rappresentare uno o più punti di contatto tra una superficie touch-sensitive e gli elementi di una
pagina HTML rappresentati sotto di essa.
In javascript viene fornita un’implementazione a queste specifiche tramite una serie di oggetti ed
eventi catturabili a runtime.
Questi eventi sono:
-

-

Touchstart, scatenato nel momento in cui l’utente tocca la superficie e genera almeno un
punto di contatto “touch point”, tali punti sono elencati all’interno dell’oggetto che
rappresenta l’evento generato.
Touchmove, scatenato quando un utente sposta, trascinandolo un “touch point” sulla
superficie.
43

-

Touchend, scatentato ogni qualvolta, l’utente 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 all’interno 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 dell’utente.
Cosa fondamentale, sarà prevenire all’interno 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 all’interno di questo gioco, su iPad, scatena
l’ingrandimento o l’allargamento 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 nell’area 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 l’esistenza della stringa createTouch all’interno dell’oggetto 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

var touches = [];
if(touchable){
/* Using standard events to precisely detect finger position */
this.canvas.addEventListener("touchstart", function(e){
e.preventDefault();
});
this.canvas.addEventListener("touchmove", function(e){
e.preventDefault();
touches = e.touches;
});
this.canvas.addEventListener("touchend", function(e){
e.preventDefault();
touches = [];
});
}

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 all’interno 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 l’evento:
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 dall’angolo superiore sinistro dell’intera finestra del
browser
- pageX, pageY: sono le coordinate misurate dall’angolo superiore sinistro della pagina visualizzata
- screenX, screenY: è la posizione misurate dall’angolo superiore sinistro dell’intero schermo del
client.
All’interno 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 dell’evento touchend, il comportamento dell’handler è semplicemente quello di
rimuovere l’intero 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 dell’oggetto Finger, che rappresenterà
come dicevamo prima, un dito sullo schermo:
// Fingers
this.fing1 = new Finger();
this.fing2 = new Finger();

A questo punto all’interno 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 all’utente, posizioniamo le dita nel punto di contatto
rilevato all’interno 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;
}

E le disegniamo nel metodo draw:
// Draw GUI elements
if(self.fing1.show === true){
self.fing1.draw(self.ctx);
}
if(self.fing2.show === true){
self.fing2.draw(self.ctx);
}

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

7.f i - RILEVARE LE GESTURES PINCH – IN e PINCH – OUT
Ora che siamo in grado di contare le dita sullo schermo e rilevarne la loro posizione, il prossimo
passo da seguire sarà quello di riconoscere le gestures di pinching sia in apertura che in chiusura:
Analizziamo nel dettaglio il movimento: di norma il pinching consiste nell’avvicinamento progressivo
delle dita dell’utente, fino, idealmente, alla loro unione completa, è ovviamente impossibile
richiedere a un paziente con scarsa mobilità della mano di riuscire ad unire completamente le dita:
ciò gli sarà fisicamente impossibile.
Per tanto nel gioco si è intesa questa gesture come l’avvicinamento progressivo delle dita, non fino
alla loro unione, ma fino a quando esse non abbiano raggiunto un distanza obiettivo:
“min_finger_target_distance” e le dita siano state staccate dallo schermo.
Ovviamente per il movimento opposto, il pinch-out il discorso è analogo, le dita partono da una
distanza minima e si allargano fino a raggiungere una distanza richiesta:
“max_finger_target_distance”, quando le dita abbandonano la superficie viene scatenato l’evento
di pinch-out.

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 un’opportuna 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:
-

“pinch”: scatenato quando l’utente ha effettuato un movimento di avvicinamento delle dita
per un certo numero di pixel e le ha staccate dallo schermo
- “pinching”: scatenato in continuazione durante il movimento di avvicinamento delle dita
- “pinchOut”: scatenato quando l’utente ha effettuato un movimento di allontanamento delle
dita per certo numero di pixel e le ha staccate dallo schermo.
Nostro scopo, in questa fase, sarà quindi catturare l’evento “pinching” e all’interno di esso
monitorare costantemente la distanza tra le dita del paziente a ogni aggiornamento. Rilevare gli
47

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 all’interno 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 l’ingrandimento o il rimpicciolimento della pagina, con il
metodo preventDefault(), e a impostare le variabili definite in precedenza a true:
N.B. All’interno del metodo update, una volta gestito l’evento, 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 l’utente 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 l’utente 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

Ecco il codice del metodo update aggiornato:
this.update = function(self){
// There can be a pinch only if there are two fingers on the screen
if(touches.length === 2){
[...]
// Check if user is pinching
if(pinching === true){
// User is still pinching, simply monitor the finger distance
// and play some animation like flames under the balloon
console.log("Pinching");
pinching = false;
}
}
else{
self.fing1.show = false;
self.fing2.show = false;
// Check if user pinched, we check this here, because
// when the pinched event is triggered
// fingers are not on the screen anymore!
if(pinched === true){
// Pinch is complete: record finger distance and movement data
// than make the player climb
console.log("Pinched");
pinched = false;
}
// Check if user pinched-out
if(pinchout === true){
// Pinch-out is complete: record finger distance and movement data
// than make the player descend
console.log("Pinched-Out");
pinchout = false;
}
}
[...]
}

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 l’unico 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 all’interno 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
un’applicazione 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 un’immagine: <img>, in cm tramite l’attributo width:
div{
width: 1cm;
}

Sapendo poi che la funzione jquery width(); restituisce la larghezza dell’elemento 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 l’iPad 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; l’intero
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 dell’ultimo valore registrato prima dello
scatenarsi dell’evento “pinched” (ovvero l’evento che stabilisce quando un paziente ha staccato le
dita dallo schermo), potremo validare l’input secondo i valori richiesti dal terapista.
7.f iii - VALIDARE L’INPUT DELL’UTENTE
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;
}

Il calcolo di questa distanza è ottenuto applicando banalmente il teorema di Pitagora, ottenendo
così la distanza in Pixel, che poi viene convertita in pollici dividendo per i DPI dello schermo, quindi
in cm moltiplicando per la costante 2.54.
A questo punto assumiamo di avere recuperato dal file di configurazione dell’utente, che vedremo
in seguito come viene generato, i valori di apertura e chiusura massimi delle dita impostati dal
terapista.
51

Quello che dobbiamo verificare ora è:
-

Se è stato scatenato l’evento “pinched”, ovvero il pinch corretto è terminato, e la distanza
iniziale delle dita nel pinch corrente è >= della distanza di apertura massima richiesta e la
distanza finale delle dita è <= della distanza di chiusura massima richiesta, allora il pinch è
valido altrimenti non lo è.
- In maniera analoga ma con valori opposti ci si comporta per l’evento “pinchout”: se la
distanza delle dita a inizio del movimento era <= della distanza di chiusura richiesta e la
distanza a fine movimento >= della distanza di apertura richiesta, allora il pinch-out è valido.
Ecco questo controllo espresso nel codice del metodo update:
Per prima cosa dobbiamo registrare la distanza di partenza delle dita per fare ciò abbiamo bisogno
di una variabile booleana, che chiameremo “pinchStarted” e due variabile di tipo float o double:
“currPinchDistance”, per mantenere in memoria a ogni iterazione la distanza attuale delle dita, e
“currPinchStartDistance”, per mantenere in memoria la distanza di partenza del pinch corrente:
var pinchStarted = false;
var currPinchDistance = 0;
var currPinchStartDistance = 0;

Ora all’interno 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 l’evento di pinch
concluso e la seconda serve a conservare temporaneamente la distanza di chiusura delle dita
registrata.
A questo punto sia all’interno dell’if 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 all’interno 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 dall’esercizio per stabilire se fare muovere la
mongolfiera, oppure tenerla ferma generando un opportuno feedback all’utente.

53

// Save current pinch values, we will send all
// data to the server at the end of the session
pinchStartArray.push(currPinchStartDistance);
pinchStopArray.push(currPinchEndDistance);
if(currPinchStartDistance >= currPinchEndDistance){
// Then we had a pinch-in, we should make the balloon climb
if(currPinchStartDistance >= pinchRequiredStartDistance &&
currPinchEndDistance <= pinchRequiredStopDistance){
// Pinch is valid! Move the balloon up.
player.move("up");
}
else{
// Pinch is invalid! Notify user
[...]
}
}
else{
// We had a pinch-out, we should make the balloon descend
if(currPinchStartDistance <= pinchRequiredEndDistance &&
currPinchEndDistance >= pinchRequiredStartDistance){
// Pinch-out is valid! Move the balloon up.
player.move("down");
}
else{
// Pinch is invalid! Notify user
[...]
}
}

Il codice è estremamente semplice in questo caso, si tratta semplicemente di confrontare i valori
registrati con i valori richiesti dal terapista per validare il movimento, da notare come per il pinchout il valore di partenza delle dita venga confrontato con il valore di chiusura impostato dal terapista,
questo viene fatto perché il movimento di allargamento delle dita parte, ovviamente a dita chiuse!

7.g - HOT AIR BALLOON – STRUTTURA DEL GIOCO E GAMEPLAY
Quanto descritto finora è servito a gestire correttamente l’input dell’utente, rilevando con
accuratezza la distanza tra le dita e salvando i dati ottenuti in opportune variabili.
Il gioco però così ha ancora poco significato, abbiamo una mongolfiera che può salire e scendere
sullo schermo, ma in realtà non c’è nulla da fare: è bene perciò spendere qualche parola nel
descrivere come il gioco finale è stato strutturato, sia dal punto di vista di calibrazione, registrazione
e salvataggio dei dati dell’esercizio, sia dal punto di vista del gameplay vero e proprio, ovvero di ciò
che l’utente dovrà fare per completare una partita.
Il gioco in sé è strutturato in 3 fasi.
FASE 1 – DECOLLO E CALIBRAZIONE
Questa è la fase iniziale del gioco, la mongolfiera si trova a terra nella parte centrale dello schermo,
e l’utente dovrà effettuare alcuni pinch sullo schermo per far iniziare a salire la mongolfiera, scopo
di questa fase è far salire la mongolfiera tanto da superare il bordo superiore dello schermo.
Dietro le quinte, in questa fase, viene eseguito, su richiesta del terapista il sistema di autocalibrazione (di cui discuteremo meglio in seguito), utilizzato per stabilire la distanza massima e
minima di pinch richiesta nell’esercizio nel caso in cui il terapista non l’abbia preimpostata nella
configurazione.
FASE 2 – VOLO E DATA-COLLECTION

54

Questa è la fase principale del gioco, ha durata variabile da 1 a 5 minuti, la lunghezza dell’esercizio
è impostabile dal terapista in fase di configurazione.
L’utente 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 dell’esercizio e il conseguente
inizio della fase 3.
FASE 3 – ATTERRAGGIO E INVIO DEI DATI DI SESSIONE AL SERVER
Quest’ultima 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 dell’utente 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 dell’atterraggio 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 l’invio asincrono dei dati di sessione rilevati
durante la fase 2 al server. Durante l’atterraggio il sistema infatti richiama una procedura il cui scopo
è preparare i dati per l’invio 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:
-

-

Gestione delle fasi di gioco.
Animazioni: in particolare Sprite Animation (per gli uccelli e le esplosioni da impatto) e
scrolling infinito dello sfondo. (per il cielo)
Collision Detection.
Feedback e suggerimenti all’utente. (Ovvero come suggerire all’utente dove deve arrivare
il suo pinch affinchè sia ritenuto valido o come far comparire scritte e immagini sullo
schermo per descrivere determinati eventi).
Sistema di auto-calibrazione.
Invio dei dati di gioco al server.

55

7.h - HOT AIR BALLOON – FASI DI GIOCO
Nella sezione precedente abbiamo descritto come il gioco sia stato strutturato in 3 fasi, ognuna di
esse prevede variazioni all’interno del gioco, sia grafiche, sia in termini di funzionalità e problemi da
gestire.
Ci serve per tanto un modo di distinguere le varie fasi, di rilevare il momento in cui eseguire la
transizione da una fase all’altra, gestire eventuali animazioni ecc.
Per fare ciò, in questo progetto, ci si è avvalsi di una variabile, di tipo stringa, visibile all’interno
dell’intera classe Game.
Essa assume, per ogni sessione di gioco tre valori:
- “takeoff”, la fase 1 del capitolo precedente
- “flying”, la fase 2
- “landing”, la fase 3.
La classe Game, inizializza la variabile al valore “takeoff” nel costruttore, è importante identificare i
momenti in cui effettuare la transizione da una fase all’altra.
Per quanto riguarda il passaggio da “takeoff” a “flying”, si è deciso di controllare semplicemente la
posizione della mongolfiera, se essa supera il bordo superiore dello schermo (ovvero la posizione del
giocatore sull’asse Y è minore di 0), si passa alla fase “flying”.
La fase “flying” inizia con un’animazione che posiziona la mongolfiera nel punto più comodo per
iniziare a giocare: vicino al bordo sinistro dello schermo, il più lontano possibile dal luogo da cui
cominceranno a comparire gli uccellini da evitare, una volta in posizione, viene avviato il timer di
gioco.
L’ultima fase è la fase di “landing”, è avviata allo scadere del timer di gioco, anche questo
cambiamento di fase provoca l’avvio di un’animazione che sposta la mongolfiera verso il bordo
inferiore dello schermo e cambia sfondo, ad animazione terminata il controllo viene restituito
all’utente per l’atterraggio.
Oltre alla variabile gamestatus, sarà dunque essenziale un’ulteriore variabile, questa volta booleana,
per distinguere le animazioni dalle vere e proprie fasi di gioco.

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” all’interno dei metodi
update e draw della classe di gioco, possiamo decidere come fare comportare l’applicazione.
Come è possibile osservare dal codice abbiamo inserito un macro-controllo all’interno 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
l’input dell’utent 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,
all’interno 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.

7.i - HOT AIR BALLOON – ANIMAZIONI
Altro elemento fondamentale che caratterizza qualsiasi videogioco, sia esso bidimensionale o
tridimensionale, è quello delle animazioni.
Nel nostro caso specifico, per animazione, intendiamo un qualsiasi movimento o effetto ottico su
un qualsiasi game object effettuato senza necessaria interazione dell’utente da un qualsiasi
elemento di gioco:
- Il volo degli uccellini
- La rotazione su se stesse delle monetine
- Le esplosioni dovute all’impatto degli uccellini con la mongolfiera
- Lo scorrere automatico del cielo sullo sfondo.
- Il fuoco che viene emesso dal bruciatore della nostra mongolfiera a ogni pinch dell’utente.
Per realizzare queste animazioni sono state utilizzate diverse tecniche a seconda del tipo di effetto
desiderato, analizziamole una per volta.

UCCELLI, MONETINE ED ESPLOSIONI – SPRITESHEET ANIMATION
Per il movimento degli uccellini da evitare e delle monetine da prendere è stata utilizzata una tecnica
di animazione denominata “spritesheet animation”.
Essa consiste nel generare, utilizzando un programma di photo-editing quali ad esempio Adobe
Photoshop o GIMP, delle spritesheets: ovvero delle immagini allungate divise in blocchi di larghezza
e altezza fissate, in cui viene disegnato il protagonista della nostra animazione nei vari frame che le
compongono, ecco un esempio:

58

L’animazione, 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 all’ultimo, per poi ricominciare dall’inizio, con una frequenza di aggiornamento
sufficientemente rapida, si darà l’illusione all’utente 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 l’animazione degli
uccellini all’interno 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
l’intera immagine in una posizione stabilita all’interno del canvas:
ctx.drawImage(img, x, y, width, height);

Prende come parametri l’immagine da disegnare, e il rettangolo entro cui l’immagine dovrà
essere disegnata, se l’immagine 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 all’interno di un
canvas html:
ctx.drawImage(img, sx, sy, swidth, sheight, x, y, width, height);

In questo caso il metodo prende come primo parametro l’immagine da disegnare, nel nostro
caso l’intera spritesheet, sx e sy sono le coordinate da cui iniziare il disegno nell’immagine
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 dell’intero spritesheet
- Le dimensioni di un singolo riquardo all’interno 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 all’interno 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:
-

Disegneremo innanzi tutto la porzione del nostro spritesheet in posizione:
(𝑙𝑎𝑟𝑔𝑒𝑧𝑧𝑎 𝑏𝑜𝑥 ∗ 𝑠𝑙𝑖𝑑𝑒 𝑐𝑜𝑟𝑟𝑒𝑛𝑡𝑒, 𝑎𝑙𝑡𝑒𝑧𝑧𝑎 𝑏𝑜𝑥 ∗ 𝑐𝑜𝑙𝑜𝑛𝑛𝑎 𝑐𝑜𝑟𝑟𝑒𝑛𝑡𝑒)
La porzione della spritesheet da disegnare saraà data dalle variabili sprite_width e
sprite_height.
60

-

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 dall’angolo 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 l’animazione delle monetine e delle esplosioni sullo schermo.
Ecco una sequenza di screenshot tratta da una sessione di gioco per mostrare l’effetto ottenuto.

61

ANIMAZIONI – SFONDO SCORREVOLE
Ora che abbiamo visto come è stata implementata la tecnica delle spritesheets animations in
HotAirBalloon, passiamo ad analizzare un altro tipo di animazione utilizzato: si tratta dell’animazione
dello sfondo.
Il concetto che sta dietro a questo tipo di animazione è quello di dare all’utente la sensazione di
movimento orizzontale, quando in realtà la mongolfiera rimane ferma in un punto preciso dello
schermo, come si fa ad ottenere questo effetto?
Semplicemente utilizzando uno sfondo di dimensioni superiori a quelle del canvas su cui viene
utilizzato, e disegnando, a ogni frame, una porzione differente dell’immagine.
Ecco uno schema di ciò che si vuole cercare di fare:

Come è possibile osservare dall’immagine, i riquadri colorati rappresentano il nostro canvas, quello
che si è fatto è stato disegnare una porzione dell’immagine grande quanto il canvas attuale, a partire
da una posizione (lungo l’asse X) diversa e incrementale a ogni frame, dando così l’illusione di
movimento.
Questo obbiettivo è facilmente raggiungibile utilizzando una variabile di posizione sull’asse 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;

Esiste però un problema: cosa succede quando incrementando di 10 px in 10 px il valore di
“frame_pos_x” ci ritroveremo a disegnare un box che parte da un punto tale per cui avremo: 𝑓𝑟𝑎𝑚𝑒
𝑝𝑜𝑠 𝑥 + 𝑐𝑎𝑛𝑣𝑎𝑠 𝑤𝑖𝑑𝑡ℎ > 𝑠𝑝𝑟𝑖𝑡𝑒 𝑤𝑖𝑑𝑡ℎ
o, in termini, più semplici, quando il nostro riquadro supererà la dimensione dell’immagine
sorgente?

62

In questo caso, la soluzione consiste, nello spezzare il disegno in due parti:
-

La prima parte sfrutterà la porzione in fondo all’immagine sorgente, che ci è rimasta da
disegnare, e verrà disegnata dal punto (0,Y) al punto (0 + parte rimanente, Y) del canvas.
- La seconda parte, verrà disegnata dal punto (0 + parte rimanente + 1, Y) alla fine del box, e
prenderà la porzione di immagine da disegnare, dal punto iniziale dell’immagine sorgente.
Di seguito ecco una rappresentazione grafica dell’effetto che si è cercato di ottenere in questo modo.

Ed il codice completo del metodo draw per la classe AnimatedBackground:

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 dell’immagine sorgente, effettuiamo
due chiamate al metodo drawImage, consecutive, la prima per disegnare la prima parte
dell’immagine, 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, l’angolo superiore sinistro del disegno) avrà raggiunto o superato la
larghezza dell’immagine sorgente, potremo ricominciare ad eseguire una sola draw, ripartendo dal
punto con ascisse 0. Questo è ciò che viene controllato dall’ultimo 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 l’avvio o la fermata dell’animazione al semplice assegnamento di un valore diverso a
questa proprietà.

64

7.j - HOT AIR BALLOON – GAMEPLAY: MOVIMENTO DEI NEMICI; MONETE E
COLLISION DETECTION
Ora che abbiamo visto come sono state realizzate le animazioni principali all’interno del gioco e
come sia stata gestita l’introduzione di nuovi attori all’interno della scena, è il momento di pensare
a rendere il gioco divertente ed analizzare il gameplay.
Vediamo quindi come vengono gestiti i nemici, le monete bonus, le collisioni tra il giocatore e questi
elementi, e come ciò infici sul punteggio del giocatore.
Come abbiamo detto in precedenza, la fase centrale di gioco è caratterizzata dalla mongolfiera del
giocatore posizionata vicino al bordo sinistro dello schermo, e i vari pinch le permettono di prendere
o perdere quota a seconda delle necessità.
Scopo del gioco è evitare gli uccellini e prendere le monetine.
Iniziamo con i nemici, bisognerà eseguire i seguenti passi:
-

Istanziare almeno un nemico all’inizio della fase di volo.
Posizionarlo oltre il bordo destro dello schermo sull’asse X, ma ad un’altezza casuale (asse
Y) in modo che non sia prevedibile il punto da cui esso comparirà.
- Per differenziare il gioco potremo regolare in maniera casuale anche dimensione, colore e
velocità dell’uccellino.
- Rilevare il momento in cui l’uccellino sparisce dallo schermo, ci sono due possibili casi:
o L’uccellino supera la mongolfiera del giocatore e vola dietro alla parte sinistra dello
schermo
o L’uccellino impatta sulla mongolfiera facendo perdere punti al giocatore
In entrambi i casi dovremo gestire il respawn del game object.
- Per rendere la difficoltà di gioco incrementale, bisogna escogitare un sistema che, in
funzione del gametime impostato dal terapista per il singolo esercizio, faccia incrementare
il numero di uccellini sullo schermo in maniera lineare.
Iniziamo da tutta la logica di inizializzazione:
var
var
var
var

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 all’interno 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 dell’uccellino a runtime, passando da una spritesheet ad un’altra.
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 nell’array, 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 l’uccellino 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 all’uccellino si sono intersecati, false altrimenti:
In caso di esito positivo, ci limitiamo a settare a false, la proprietà alive del “bird”
corrente, a lanciare un’animazione 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 all’utente.
Anche questa volta settiamo a false la proprietà alive, ma non eseguiamo alcuna
animazione ne azione particolare, quest’eventualità significa semplicemente che il
giocatore è riuscito ad evitare l’uccellino.
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 L’utente ha colpito l’uccellino all’update precedente
o L’utente ha superato l’uccellino all’update precedente
o È trascorso un tempo superiore a “bird_increase_frequency” ed è stato abilitato un
altro uccellino all’interno del vettore birds.
Qualunque sia la causa per la quale ci troviamo in questa condizione, l’unica cosa da
effettuare è il respawn dell’uccellino, 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 all’interno 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, nell’ultima 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 l’utente 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.
Nell’esempio mostrato abbiamo un frame rate di 25 FPS (Frame al Secondo) per tanto in un minuto,
il metodo update verrà chiamato:
25𝑥60 = 1500 𝑣𝑜𝑙𝑡𝑒
Generiamo un numero casuale nell’intervallo [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
dall’inizio 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].
L’effetto sarà quello di avere la garanzia (100% di probabilità) che almeno una moneta compaia sullo
schermo in un minuto, senza che però sia prevedibile l’istante esatto in cui accadrà.

7.k - HOT AIR BALLOON – FEEDBACK E SUGGERIMENTI ALL’UTENTE
Parliamo ora di messaggi all’utente e feedback corretto, abbiamo visto, nella parte introduttiva,
come sia importante, specialmente per un gioco creato per la riabilitazione che ogni tipo di
movimento sia monitorato costantemente, e un eventuale movimento sbagliato venga corretto
immediatamente.
In un gioco così semplice come Hot Air Balloon, verrebbe semplicemente in mente di far comparire
sullo schermo messaggi quali “allarga di più le dita”, “stringile di più” oppure semplicemente
“bravo!”
In questo caso, però, il gioco è stato studiato per pazienti in una fascia di età molto bassa (dai 3 anni
in su), bisogna perciò riflettere sul fatto, che molti pazienti, potrebbero non saper leggere!
Bisogna pertanto pensare a un sistema di feedback alternativo, almeno per ciò che riguarda il
sistema di input, bisognerà essere in grado di comunicare in maniera universalmente comprensibile
almeno le seguenti informazioni:
-

Se le dita sono state rilevate dal gioco.
Pinch eseguito correttamente
Pinch sbagliato
Distanza dove devono arrivare le dita affinchè un pinch (sia in chiusura che in apertura) sia
ritenuto valido.
Per quanto riguarda il rilevamento delle dita del gioco, la soluzione trovata è stata quella descritta
nel paragrafo sulla gestione dell’input, di mostrare dei pallini colorati in corrispondenza delle dita
per tutto il periodo in cui viene catturato l’evento “pinching”, ovvero per tutto il periodo in cui
abbiamo almeno due dita sullo schermo.
Parlando invece di pinch corretto o scorretto, si è semplicemente pensato di sostituire scritte quali:
“bravo!” o “sbagliato!” con degli smiley colorati comunemente usati nelle applicazioni per bambini
molto piccoli:

L’informazione che invece ha richiesto un po’ più di lavoro, è stata quella relativa al mostrare la
distanza da raggiungere con le dita per eseguire il tipo di pinch desiderato.
I problemi da affrontare sono stati fondamentalmente due:
69

-

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 dell’inizio del pinch si trovano a una distanza superiore o uguale alla
distanza di apertura massima delle dita richiesta dal terapista, allora assumiamo che l’utente
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 l’utente 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
l’errore 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 l’utente, 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 all’inizio del movimento.
Vediamo il codice utilizzato per risolvere questo problema nel caso del pinch-in, per il pinch-out è
esattamente speculare:

70

if(currPinchStartDistance >= fingerRequiredStartDistance){
/* Calculate the difference between the current finger distance *
* and the required one */
var diff = currPinchStartDistance - fingerRequiredTargetDistance;
diff = diff / 2.54 * deviceDPI; // pixel to cm
// Calculate the angle between the two fingers on screen
// (angular coeff. of the segment between the fingers)
var deltaX = currPinchStartPos.p1.x - currPinchStartPos.p2.x;
var deltaY = currPinchStartPos.p1.y - currPinchStartPos.p2.y;
var angleInDegrees = Math.atan(deltaY / deltaX) * 180 / Math.PI;
// Setting markers position
if(currPinchStartPos.p1.x < currPinchStartPos.p2.x){
targetOpen1.pos_x = currPinchStartPos.p1.x + (diff / 2) *
Math.cos(angleInDegrees * Math.PI / 180);
targetOpen1.pos_y = currPinchStartPos.p1.y + (diff / 2) *
Math.cos(angleInDegrees * Math.PI / 180);
targetOpen2.pos_x = currPinchStartPos.p2.x - (diff / 2) *
Math.cos(angleInDegrees * Math.PI / 180);
targetOpen2.pos_y = currPinchStartPos.p2.y - (diff / 2) *
Math.cos(angleInDegrees * Math.PI / 180);
}
else{
// Same as before but with opposite signs
[...]
}
// Now rotate the markers by 90 degrees so that they are
// perpendicular to the segment between fingers
targetOpen1.angle = angleInDegrees + 90;
targetOpen2.angle = angleInDegrees + 90;
[...]
}

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 d’ora 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:
tan−1 ( 𝑑𝑒𝑙𝑡𝑎𝑋

180
)× 𝑑𝑒𝑙𝑡𝑎𝑌 𝜋

Salviamo questo valore nella variabile “angleInDegrees”.
A questo punto la posizione dei marker ci sarà data dalle formule dentro l’if, per esempio la posizione
del marker di sinistra sarà il punto di coordinate: 𝑑𝑖𝑓𝑓 𝑥

= 𝑝𝑜𝑠𝑖𝑧𝑖𝑜𝑛𝑒 𝑑𝑒𝑙 𝑑𝑖𝑡𝑜 𝑑𝑖 𝑠𝑖𝑛𝑖𝑠𝑡𝑟𝑎 (𝑠𝑢 𝑥) + (
) ∗ cos(𝑎𝑛𝑔𝑙𝑒𝐼𝑛𝐷𝑒𝑔𝑟𝑒𝑒𝑠 ∗ 𝜋/180)
2 𝑑𝑖𝑓𝑓 𝑦

= 𝑝𝑜𝑠𝑖𝑧𝑖𝑜𝑛𝑒 𝑑𝑒𝑙 𝑑𝑖𝑡𝑜 𝑑𝑖 𝑠𝑖𝑛𝑖𝑠𝑡𝑟𝑎 (𝑠𝑢 𝑦) + (
) ∗ sin(𝑎𝑛𝑔𝑙𝑒𝐼𝑛𝐷𝑒𝑔𝑟𝑒𝑒𝑠 ∗ 𝜋/180)
2
71

Ragionamento analogo viene fatto per il marker di destra.
Come ultima cosa, il marker viene ruotato su se stesso di 90 gradi, in quanto non lo vogliamo
parallelo al segmento che unisce le due dita, ma perpendicolare!
Di seguito un semplice schema per comprendere il lavoro svolto:
x
f1
angle_in_degrees
diff/2

diff/2

f2
y

Ed ecco il risultato finale ottenuto per il pinch-in (sopra) e il pinch-out (sotto):

72

7.l - HOT AIR BALLOON – SISTEMA DI AUTO-CALIBRAZIONE
Ora che abbiamo visto come suggerire all’utente quanto ampio debba essere il movimento da
effettuare per ottenere un qualche tipo di risposta dal gioco, prima di passare alla comunicazione
dei dati di sessione al server che si preoccuperà di generare tutte le informazioni da mostrare al
terapista, è bene riprendere un discorso legato all’input dell’utente che non è stato affrontato finora.
Abbiamo visto come, per ottenere informazioni quali la distanza minima da percorrere con le dita
sullo schermo, oppure il punto da cui un pinch corretto debba partire, ci si sia affidati
completamente ai parametri impostati dal terapista in fase di configurazione tramite una schermata
che vedremo nel capitolo successivo.
Si è però voluto dare all’utente, con lo scopo di testare comodamente l’effettiva capacità del
paziente ad utilizzare il gioco, la possibilità di giocare senza che il terapista fosse obbligato a
effettuare misurazioni particolari e a calibrare precisamente l’esercizio sul paziente.
Per fare ciò è stato ideato un sistema di auto-calibrazione, attivabile sempre dal pannello di
configurazione del gioco, il quale cerca, durante la FASE 1 della sessione, ovvero quella di decollo, di
impostare i parametri di gioco automaticamente in funzione di alcuni pinch eseguiti liberamente
(senza vincoli particolari di distanza).
Attenzione, è bene ribadire che questo sistema non è stato pensato per sostituire la più precisa
configurazione ad hoc dell’esercizio effettuabile dal terapista, ma per fornire uno strumento rapido
per testare velocemente la possibilità fisica, di un determinato paziente, ad utilizzare HotAirBalloon.
Di seguito verrà descritto il funzionamento del sistema di auto-calibrazione.
Il concetto alla base di questo sistema è molto semplice:
-

Si fanno effettuare alcuni pinch al paziente liberamente, senza verificare cioè che raggiunga
determinate misure preimpostate.
- Si registra la distanza delle dita all’inizio 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, all’interno del metodo update verifichiamo di essere nella fase 1 di gioco e che sia
stato effettivamente richiesto l’utilizzo 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 l’effettivo 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

8 - REALIZZAZIONE DELLA PIATTAFORMA PER I TERAPISTI
Ora che abbiamo visto come è stato realizzato il gioco da presentare ai pazienti, e abbiamo analizzato
nel dettaglio tutti gli aspetti che ne hanno caratterizzato lo sviluppo, quali implementazione del loop
di gioco in un canvas HTML, la realizzazione di animazioni, la gestione dell’input, l’autocalibrazione
dei dispositivi ecc.
Possiamo passare ad analizzare l’ultimo aspetto importante del lavoro svolto: la realizzazione della
piattaforma di configurazione e analisi dei dati per i terapisti.
Nel capitolo introduttivo di questa tesi, è stata presentata in breve la piattaforma REWIRE a cui
l’intero progetto fin qui descritto si è, in piccolo, ispirato: ecco, se il gioco vero e proprio descritto
finora insieme al dispositivo utilizzato dal paziente, ovvero il suo tablet personale, corrispondono
alla PATIENT STATION, quella che verrà descritta nei paragrafi successivi sarà l’equivalente della
HOSPITAL STATION.
Partiamo con la nostra analisi dagli aspetti relativi ai requisiti della piattaforma.

8.a - REQUISITI E NECESSITA’ DEI TERAPISTI
Abbiamo visto come il gioco utilizzi una serie di parametri impostati dai terapisti per validare l’input
e personalizzare l’esercizio in funzione delle abilità fisiche del bambino.
È stato dunque necessario pensare a una comoda interfaccia che permettesse al terapista di gestire
i pazienti e generare, per ognuno di essi una configurazione ad hoc.
Le necessità emerse, riguardo la configurazione dell’esercizio da svolgere, a seguito degli incontri
effettuati sono le seguenti:
-

Possibilità di gestire più pazienti contemporaneamente.
Possibilità di impostare l’ampiezza del movimento desiderato.
Possibilità di decidere quanto dovesse durare una singola sessione di gioco.
Possibilità di abilitare o disabilitare la funzione di auto-calibrazione del gioco
Possibilità di scegliere il dispositivo in possesso dal paziente da una lista quanto più ampia
possibile. (Maggiore è il supporto di dispositivi, maggiore è la base di utenza soddisfatta a
basso costo: è più probabile, cioè, che il paziente possieda il dispositivo).
Oltre a configurare il gioco, è fondamentale, per il terapista, poter analizzare i dati rilevati durante
le singole sessioni di gioco, in modo da poter apprezzare gli eventuali miglioramenti effettuati dal
paziente e adeguare la difficoltà dell’esercizio per adattarsi alle sue condizioni.
Durante gli incontri, è quindi emersa la necessità di predisporre, sulla Hospital Station, una pagina
di analisi delle sessioni in cui emergessero, per ogni singolo paziente:
-

-

-

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 all’inizio e fine di ogni movimento di pinching,
effettuato all’interno della singola sessione selezionata.
Essendo stato l’intero gioco sviluppato con tecnologie web-based, si è deciso di implementare
l’intera 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 l’ospedale
per il quale è stato creato l’intero progetto, era già provvisto). Vedremo poi nel paragrafo finale di
questo capitolo come sia stato ristretto l’accesso 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 dell’esercizio.

8.b - IL PANNELLO DI CONFIGURAZIONE DEL GIOCO
Iniziamo la nostra analisi dando uno sguardo a uno screenshot dell’intera pagina di configurazione
finita, commenteremo poi le varie parti e vedremo come i dati, rilevati ottenuti dai form presenti in
questa pagina, vengano preparati per essere inviati al gioco.

Come è possibile osservare, il pannello di configurazione si presenta come una normalissima pagina
web, essa è divisa sostanzialmente in 3 parti:
-

-

-

Selezione e configurazione del paziente in alto: in questa sezione è possibile selezionare
pazienti già precedentemente inseriti nel sistema, aggiungerne di nuovi, o resettare la
password relativa al paziente correntemente selezionato.
Configurazione della piattaforma al centro: in questa sezione è possibile selezionare il
dispositivo che il paziente utilizzerà per svolgere il proprio esercizio e se utilizzare o meno il
sistema di auto-calibrazione del gioco.
Configurazione dell’esercizio in basso: in questa sezione è possibile personalizzare la
configurazione dell’esercizio impostando manualmente la “distanza obbiettivo delle dita”
(apertura e chiusura delle dita), il margine di tolleranza e il tempo di gioco.
77

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 l’invio al gioco.
Per quanto riguarda il paziente corrente, ci basta sapere in questa fase, che il suo nominativo
univoco è mantenuto nella sessione corrente, all’interno della variabile globale:
window.patient = "PXXX";

Per quanto riguarda, invece l’aspetto grafico dell’intera pagina è stato utilizzato il framework
bootstrap sviluppato da Twitter Inc. l’azienda che sviluppa e mantiene l’omonimo 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 un’aspetto grafico piacevole e coerente anche in assenza di web-designer
esperti che si occupino dell’aspetto 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 l’impostazione 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 l’implementazione è molto semplice, al rilevamento dell’evento “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 dell’utente.
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
l’invio vero e proprio.
È bene spendere qualche parola nel descrivere lo standard utilizzato per la gestione dei file di
configurazione e delle sessioni di gioco all’interno 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

La stessa collezione di informazioni mantenuti in un file XML apparirebbe come segue:
<!DOCTYPE glossary PUBLIC "-//OASIS//DTD DocBook V3.1//EN">
<glossary><title>example glossary</title>
<GlossDiv><title>S</title>
<GlossList>
<GlossEntry ID="SGML" SortAs="SGML">
<GlossTerm>Standard Generalized Markup Language</GlossTerm>
<Acronym>SGML</Acronym>
<Abbrev>ISO 8879:1986</Abbrev>
<GlossDef>
<para>A meta-markup language, used to create markup
languages such as DocBook.</para>
<GlossSeeAlso OtherTerm="GML">
<GlossSeeAlso OtherTerm="XML">
</GlossDef>
<GlossSee OtherTerm="markup">
</GlossEntry>
</GlossList>
</GlossDiv>
</glossary>

Il grosso vantaggio derivato dall’utilizzo di questa sintassi all’interno di un’applicazione 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 l’intero 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 dell’esercizio 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 l’interfaccia
all’utente.
Per fare ciò effettuiamo in fondo alla funzione saveConfig() una POST request sul server richiedendo
l’esecuzione di un semplice script PHP il cui unico scopo sarà creare il file “config.json” in una
directory dedicata all’utente:

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 l’istanziazione
dell’oggetto XMLHttpRequest, la preparazione dei dati e l’esecuzione 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 dell’array 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
dell’utente, che analizzeremo nella parte finale del capitolo, avremo accesso anche nella classe game
alla variabile globale window.patient, contenente l’id relativo al paziente corrente.
Una volta in possesso di questo dato, per recuperare i dati relativi all’ultima 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 un’HTTP 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 all’interno di un
oggetto javascript valido, che possiamo passare tranquillamente al launcher del gioco, il quale
istanzierà l’oggetto della classe game.js che al suo interno avrà istruzioni di questo tipo:
[...]
this.devicePPI = therapistConfig.device_dpi;

/* IPAD 1st Gen 132 PPI */

/* Parameters used by the auto-calibration system */
if(therapistConfig.override_autocalibration === "1"){
this.autocalibration = false;
this.avgStart =
parseFloat(this.therapistConfig.fingerRequiredStartDistance);
this.avgEnd =
parseFloat(this.therapistConfig.fingerRequiredTargetDistance);
}
/* Parameters that can be set by the therapist, NOTE: "need a setting
interface for this..." */
this.pinchRequiredFreq =
parseFloat(this.therapistConfig.pinchRequiredFreq) * 1000;
this.fingerRequiredStartDistance =
parseFloat(this.therapistConfig.fingerRequiredStartDistance);
this.fingerRequiredTargetDistance =
parseFloat(this.therapistConfig.fingerRequiredTargetDistance);
this.allowedGameTime = parseInt(this.therapistConfig.gameTime);
this.pinchErrorTollerance = parseFloat(this.therapistConfig.tollerance) / 100;
[...]

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 all’interno del pannello di
configurazione stesso quando, tramite l’opportuno menu a tendina, il terapista seleziona un
paziente dalla lista, viene recuperato, se esiste, l’ultimo 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.

8.c - IL PANNELLO DELLE STATISTICHE DEI PAZIENTI
Descriviamo ora l’ultimo elemento chiave dell’intera piattaforma: abbiamo sottolineato più volte la
necessità, da parte del terapista, di accedere, per quanto riguarda applicazioni di questo tipo, ai dati
relativi alle prestazioni dei pazienti durante le loro sessioni di gioco.
L’analisi di tali dati è fondamentale per poter valutare la qualità dell’esercizio assegnato al paziente,
valutarne i progressi o gli eventuali peggioramenti e capire se, per un determinato individuo,
l’esercizio proposto dal gioco è valido oppure no.
Per questi motivi si è deciso di generare una schermata, nella quale fosse possibile analizzare i dati
di sessione di ogni singolo paziente:

Come è possibile osservare dall’immagine, anche questa schermata è divisa in 3 macroaree:

84

-

Selezione del paziente in alto.
Storico sessioni, con media delle distanze delle dita sia in apertura che in chiusura per ogni
sessione di gioco registrata.
- Dettagli ultima sessione con:
o Dati di calibrazione.
o Numero di pinch validi/invalidi registrati.
o Distanza di apertura e chiusura delle dita per ogni pinch rilevato nella sessione
(compreso quelli invalidi).
o Media delle distanze tra le dita in apertura, chiusura e distanza percorsa, dove la
distanza percorsa è semplicemente data da: 𝑑𝑖𝑠𝑡𝑎𝑛𝑧𝑎𝑃𝑒𝑟𝑐𝑜𝑟𝑠𝑎
= 𝑑𝑖𝑠𝑡𝑎𝑛𝑧𝑎𝐴𝑝𝑒𝑟𝑡𝑢𝑟𝑎 − 𝑑𝑖𝑠𝑡𝑎𝑛𝑧𝑎𝐶ℎ𝑖𝑢𝑠𝑢𝑟𝑎
Per la realizzazione dei grafici è stata utilizzata la libreria “google charts” che fornisce API javascript
per la gestione e realizzazione di grafici dinamici all’interno delle pagine web.
Analizziamo il tutto partendo dal recupero dei dati di sessione dal gioco:

8.c i - INVIO DEI DATI DI SESSIONE DAL GIOCO AL SERVER
Abbiamo visto, nel capitolo 7, come la fase centrale di gioco in HotAirBalloon sia la fase in cui
effettuiamo le rilevazioni e validazioni su tutti i pinch effettuati dall’utente:
Per la costruzione dei nostri grafici nella schermata di statistiche mostrata sopra abbiamo bisogno
di trasferire alcune informazioni dal gioco al server, dobbiamo però rispondere ai seguenti quesiti:
- Di quali informazioni abbiamo bisogno?
- Quando trasferiamo queste informazioni?
- Come le trasferiamo?
Per rispondere alla prima domanda diamo un’ulteriore sguardo alla schermata di statistiche,
osservandola bene ci rendiamo conto che per generarla avremo bisogno di:
-

Dati di calibrazione: siano essi ottenuti dal sistema di auto-calibrazione o impostati
manualmente dal terapista, è utile comunicare quali erano gli obbiettivi da raggiungere nella
sessione che si sta visualizzando, per cui dovremo trasferire:
o Cardinalità del campione analizzato dal sistema di auto-calibrazione (se è stato
usato)
o Distanza minima delle dita che il paziente doveva raggiungere idealmente
o Distanza massima in apertura che il paziente doveva raggiungere
o Il valore di tolleranza impostato dal terapista
- Per lo storico delle sessioni distanza media (minima e massima) delle dita raggiunta in ogni
sessione analizzata
- Per la sessione visualizzata:
o Numero di pinch totale (numero di pinch validi + numero di pinch invalidi)
o Per ogni pinch: distanza delle dita all’inizio del movimento e distanza delle dita alla
fine del movimento
o Ancora una volta media delle distanze raggiunte in tutti i pinch di sessione.
Tutti questi dati sono già disponibili come variabili pubbliche all’interno della nostra classe di gioco,
lo vedremo fra qualche riga.

Quando trasferiamo le informazioni?
85

Abbiamo due possibili opzioni:
-

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 un’ulteriore 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, all’interno 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 l’istanza corrente dell’oggetto
“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,

// Pinch sample cardinality
// Sample Average Finger Start
// Distance
// Sample Average Finger Stop
//Distance

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 dell’opportuno 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 l’istante di tempo corrente ritornato dalla funzione time() di PHP.
Il primo file è singolo e viene aggiornato a ogni sessione: contiene i dati relativi all’ultima 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

<!-- Load google javascript API -->
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
// Load the Visualization API and the piechart package.
google.load('visualization', '1.0', {'packages':['corechart', 'controls']});
// Set a callback to run when the Google Visualization API is loaded.
google.setOnLoadCallback(drawChart);
[...]
</script>

Come è possibile osservare dall’ultima istruzione, viene richiesta l’esecuzione 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 dell’utente corrente:

// Callback that creates and populates a data table,
// instantiates the charts, passes in the data and
// draws them.
function drawChart() {
[...]
// Retrieve data from json file
/* Get average pinch data, from every recorded session */
$.get("../HotAirBalloon/callbacks/getPatientHistory.php?patient="+window.patient
,function(data){
// Handle response
data = JSON.parse(data);
[...]
});
[...]
}

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:
-

Lo script dovrà scansionare tutti i file di sessione del paziente corrente.
Per ognuno di questi file estrapolare le informazioni di sessione e calcolare la media di tutte
le distanze delle dita misurate.
Salvare il tutto in una opportuna struttura dati
Convertire la struttura dati in formato json, in modo che sia facilmente utilizzabile dal nostro
script lato client.
Restituire il risultato

Ecco il codice dello script:

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);
?>

Ora vediamo come la funzione drawChart disegnerà il grafico sullo schermo:
Innanzi tutto il nostro grafico sarà dotato di un sistema controllo del range da visualizzare, in questo
modo sarà possibile filtrare i dati mostrati dal grafico in funzione della data:
sarà cioè possibile scegliere se mostrare lo storico delle sessioni di gioco da un giorno X a un giorno
Y oppure da un’ora X a un’ora Y…
Predisponiamo innanzi tutto un div adatto ad ospitare il nostro grafico nel codice html della pagina:
<div id="history_chart" class="span12">
<div id="chart0"></div>
<div id="control0"></div>
</div>

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

var dashboard = new
google.visualization.Dashboard(document.getElementById('history_chart'));

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:

Ecco il codice per generarla
var control = new google.visualization.ControlWrapper({
'controlType' : 'ChartRangeFilter',
'containerId' : 'control0',
'options' : {
// Filter by the number of pinches axes
'filterColumnIndex' : 0,
'ui' : {
'chartType' : 'LineChart',
'chartOptions' : {
'chartArea' : {'width' : '90%', 'height': '20%'},
'hAxis' : {'baselineColor' : 'none'},
},
'chartView':{
'columns' : [0,1]
},
//'minRangeSize' : 5
},
},
'state' : {
'range' : {
'start' : range_start_date,
'end' : new Date(parseInt(data[data.length - 1].timestamp) * 1000)
}
}
});

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

var chart = new google.visualization.ChartWrapper({
'chartType' : 'ColumnChart',
'containerId' : 'chart0',
'options' : {
'title' : "Multiple session patient results",
'chartArea' : {'height' : '80%', 'width' : '90%'},
'legend': {'position': 'none'},
'colors': ['#3366CC', '#79CDCD']
}
});

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

Ecco il grafico generato

È 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

8.d - GESTIONE UTENTI E SICUREZZA
Avendo la necessità di provare la piattaforma su un diverso numero di pazienti, e avendo ogni
paziente necessità di configurazione diversa, si è pensato di rendere l’intero sistema multi-utente.
Questo aspetto è stato fino ad ora soprasseduto dichiarando semplicemente l’esistenza di una
variabile window.patient, la quale conteneva il nome del paziente correntemente autenticato.
Vediamo però come è stato affrontato questo problema:
Innanzi tutto abbiamo due categorie di utenza a cui pensare:
-

I pazienti: ogni paziente ha due cartelle dedicate sul server, una per il mantenimento dei file
di configurazione e un’altra 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 all’elenco dei pazienti,
alle loro configurazioni e alle statistiche di ogni paziente.
Iniziamo con l’autenticazione dei pazienti.
Trattandosi l’intero 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
dell’ospedale 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 l’id univoco del paziente e mostrato il seguente form da compilare con la password
scelta dall’utente e la conferma di quest’ultima.

92

Alla pressione del tasto di conferma verrà validato il form controllando l’effettiva compilazione e
uguaglianza dei campi password e confirm password dopo di che, verrà richiamata uno script PHP
lato server che creerà l’utente 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 l’effettiva compilazione dei campi obbligatori del form, una volta
superati i controlli viene eseguita una richiesta HTTP di tipo POST al server richiedendo l’esecuzione
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 l’utente 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 dell’utente 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 l’elenco 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 dell’utente secondo la convenzione adottata dall’intero 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 dell’avvio della sessione di gioco è stato aggiunto un form da compilare per autenticare
l’utente.

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.

Analizziamo il processo di login più nel dettaglio:
$('#login-form').submit(function(e){
e.preventDefault();
/* First check if input is valid */
var username = $('#inputUsername').val().toUpperCase();
var passwd = $('#inputPassword').val();
if(username.length === 0){
/* Username is empty */
$('#inputUsername').parent().parent().addClass('error');
$('#empty_fields_error').fadeIn(500);
setTimeout(function(){$('#empty_fields_error').fadeOut(500)}, 5000);
return false;
}
var regex = /P\d\d\d/i;
if(regex.test(username) === false){
/* Username is not valid */
$('#inputUsername').parent().parent().addClass('error');
$('#patient_id_error').fadeIn(500);
setTimeout(function(){$('#empty_fields_error').fadeOut(500)}, 5000);
return false;
}
if(passwd.length === 0){
/* Password is empty */
$('#inputPassword').parent().parent().addClass('error');
$('#empty_fields_error').fadeIn(500);
setTimeout(function(){$('#empty_fields_error').fadeOut(500)}, 5000);
return false;
}
/* Attempt login */
$.post("callbacks/userLogin.php", {user:username, pass:passwd})
.done(function(result){
if(result === "LOGGED"){
/* Start the game */
startGame(username);
/* Dispose Modal */
$('#login-modal').modal("hide");
}
else{
/* Invalid login attempt */
$('#inputUsername').parent().parent().addClass('error');
$('#inputPassword').parent().parent().addClass('error');
$('#login_error').fadeIn(500);
setTimeout(function(){$('#login_error').fadeOut(500)}, 5000);
return false;
}
});
});

Alla pressione del tasto “Sign In”, viene innanzi tutto verificata l’effettiva immissione dei dati in tutti
i campi del form e l’effettiva validità del nome utente inserito (tramite l’espressione 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 dell’utente che sta tentando di effettuare il login.

Osserviamo la callback nel dettaglio:
<?php
$result = "INVALID";
$user = $_POST['user'];
$pass = sha1($_POST['pass']);
/* Try to match user & pass in the users file */
$fp = fopen('../users','r');
while(!feof($fp)){
$parts = fscanf($fp,"%s %s\n");
/* Match POST data with current string */
if($parts[0] == $user && $parts[1] == $pass){
$result = "LOGGED";
break;
}
}
fclose($fp);
echo $result;
?>

È da notare, innanzi tutto, come la password venga immediatamente passata alla funzione sha1, se
essa sarà identica alla password impostata in fase di creazione dell’utente, 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 all’interno 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:
-

In caso affermativo, richiama la procedura di lancio del gioco, attraverso la funzione
startGame passando come parametro il nome del paziente corrente.
- In caso negativo, comunica l’errore all’utente e ripropone il form di login.
Questo è il modo in cui viene gestita l’autenticazione degli utenti, passiamo a quella dei terapisti.
96

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 l’autenticazione dei terapisti.
Stiamo parlando del modulo auth_basic e dell’applicazione htpasswd.

Il concetto che sta alla base dell’utilizzo di questo modulo è quello di:
-

Spostare tutti i file che costituiscono l’area ad accesso riservato in una cartella dedicata
all’interno del webserver.
- Definire le credenziali degli utenti che potranno accedere a quest’area
- Comunicare al webserver di restringere l’accesso 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 l’elenco 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 l’utilizzo:

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 l’aggiunta del primo terapista alla piattaforma:
sudo htpasswd -c users nomeutente

Con l’opzione –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 all’interno 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à all’utente 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 l’intero pacchetto è stato installato su un WebServer virtuale predisposto su una
macchina dedicata presso il laboratorio AISLab del dipartimento di Informatica dell’Università degli
Studi di Milano e reso disponibile al reparto di chirurgia e riabilitazione della mano dell’ospedale 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 all’utilizzo della piattaforma e ai possibili miglioramenti.
Tuttora si stanno effettuando modifiche alla piattaforma per renderla il più utilizzabile possibile.

9.a - RISULTATI OTTENUTI CON HOT AIR BALLOON
Per quanto riguarda il gioco HotAirBalloon, abbiamo avuto modo di testare una versione preliminare
del gioco sulla piccola paziente della quale avevamo assistito alla sessione di riabilitazione durante
fase di progettazione della piattaforma.
La bambina ha avuto qualche difficoltà nel comprendere la meccanica di gioco, probabilmente per
la giovane età, per tanto la sessione giocata di fronte a noi è stata molto breve, nonostante ciò
abbiamo potuto osservare la capacità del gioco di rilevare le dita sullo schermo e il conseguente
movimento.
Abbiamo però osservato l’effettiva necessità di effettuare qualche sessione di training sui pazienti
con i terapisti per fare comprendere, specie a bambini così piccoli, come è strutturato il gioco.
Il personale clinico si è preso carico di effettuare test su una base di pazienti più ampia, di età
differente e a diversi stadi del processo di riabilitazione.
Ad oggi sulla piattaforma sono registrati 6 pazienti attivi sulla piattaforma, dai dati ottenuti tramite
i pannelli di statistiche delle varie sessioni è stato possibile osservare come, per il paziente medio,
sia ovviamente difficoltoso, ma non impossibile, effettuare movimenti di pinching validi (ovvero
all’interno dei parametri impostati dal terapista o dal sistema di configurazione), d’altra parte il
sistema si è, finora, dimostrato in grado di ottenere dati veritieri sulle distanze percorse dalle dita
dei pazienti sia per pinch validi e invalidi.
Per migliorare la giocabilità del gioco, è stato introdotto, in una fase successiva di sviluppo, il
parametro di tolleranza, descritto nel capitolo 7 e 8, che permette un margine di errore più ampio
nella fase di calibrazione (sia da parte del terapista che da parte del sistema di auto-calibrazione), è
stato scritto un manuale per la configurazione dei parametri di gioco ed è stato consegnato ai
terapisti per garantire una configurazione dell’applicazione adatta ai singoli pazienti, siamo
attualmente in attesa di conoscere ulteriori sviluppi.

99

9.b - POSSIBILI SVILUPPI FUTURI
Nonostante i vari problemi di immaturità della piattaforma, il personale clinico dell’ospedale San
Giuseppe si è dimostrato entusiasta delle possibilità offerte dalla piattaforma. Si sta, pertanto,
pensando a eventuali sviluppi della piattaforma e progetti futuri.

NUOVI GIOCHI WEB-BASED
Innanzi tutto si sta pensando alla capacità di dispositivi come tablet e smartphone di tracciare altre
tipologie di gestures delle dita: come ad esempio il “tap”, lo “swipe” ecc. Tramite le librerie javascript
utilizzate in HotAirBalloon e i touch event previsti dallo standard W3C sarà, in futuro, possibile
generare altri videogiochi web-based, quindi costruiti utilizzando la stessa architettura usata nel
progetto qui descritto, che possano sfruttare lo stesso backend per i terapisti e al contempo fornire
un set di esercizi diversi e che coprano uno spettro più ampio della routine riabilitativa assegnata
normalmente ai pazienti.
NUOVI STRUMENTI
Inoltre, ora il laboratorio AISLab sta lavorando alla produzione di semplici giochi sensorizzati, in
grado di misurare la forza impressa sugli stessi e inviare segnali a dispositivi quali smartphone o
tablet in grado di elaborarli e utilizzarli di conseguenza: lo scopo è quello di realizzare giocattoli fisici
con cui fare giocare i bambini ottenendo dei dati che, oltre ad essere inviati al terapista per
opportune analisi sullo stato del paziente, possono essere utilizzati per interagire con giochi virtuali,
l’idea è quella che un azione sul gioco reale provochi una reazione anche sul gioco virtuale,
trasformando così il gioco fisico in una sorta di controller per il gioco virtuale.

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 l’uscita 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

-

Leap Motion Controller, ne abbiamo già parlato in questo documento, ma dalla
realizzazione del progetto ad oggi, la casa produttrice del dispositivo ha rilasciato in
versione BETA la nuova SDK che introduce un sistema innovativo di tracciamento della
mano, basato sulla rilevazione dello scheletro: questo ha permesso di scrivere API in grado
di risolvere alcuni problemi che ne rendevano difficoltosa la programmazione nella prima
versione:
o Si possono ora rilevare nativamente gestures quali il Pinching o il Grabbing
o Può essere misurata la qualità del rilevamento della mano: se ad esempio le mani si
sovrappongono, il valore di “confidence” sui dati rilevati scende da 1 a 0
o Ora è possibile distinguere le mani rilevate, destra o sinistra
o Identificare un dito precisio nell’aria scansionata (ad esempio: indice della mano
sinistra)
o Identificare la posizione delle ossa

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 97–111 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 Wüest
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.

SITI WEB E MANUALI ONLINE
9. Panda3D official manual: https://www.panda3d.org/manual/
10. Leap Motion Controller SDK documentation for the Python Programming Language:
https://developer.leapmotion.com/documentation/python/
11. W3School Javascript tutorial: http://www.w3schools.com/js/
12. W3C touch events document page: http://www.w3.org/TR/touch-events/

102

RINGRAZIAMENTI
Per concludere, desidero ringraziare tutte le persone che mi hanno supportato tecnicamente e
psicologicamente durante la stesura della tesi e l’intero 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 all’interno 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 dell’AISLab, 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
dell’Ospedale 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 dell’intera
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