Architettura del Sistema
L'applicazione prevede la presenza di
tre componenti interagenti.
Il Server ha il compito di fare da intermediario nella
fase iniziale di instaurazione della connessione diretta tra
i due client e di fare da ponte per permettere al gioco di
essere inserito come modulo all'interno di NetGame.
I Client contengono il grosso della logica del gioco
e si distinguono, nella fase iniziale, in master e
slave. Nel seguito del capitolo si chiarirà
il significato di tale distinzione.
La figura seguente mostra graficamente quanto detto.
Come si può vedere, e come si
chiarirà più approfonditamente in seguito, la
comunicazione tra i partecipanti al gioco (i clients) avviene
con modalità peer-to-peer. Si è scelta questa
soluzione perché è quella che permette un minore
tempo di latenza nel tragitto che devono compiere i dati rispetto
alle alternative (client-server e ibrida) e già si
è detto dell'importanza, in una applicazione real-time,
di limitare tale fattore.
Inoltre essendo i partecipanti solo due, non ci sono problemi
di distribuzione di messaggi e lo stato della partita sarà
mantenuto concordemente dai giocatori.
Descrizione dell'Interazione
Durante un match l'interazione
tra clienti attraversa diverse fasi.
Fase iniziale : instaurazione della
comunicazione tra i clients
Evidentemente la prima cosa da fare
è stabilire un canale di comunicazione tra i due gicatori.
Concretamente si tratterà di creare due socket in entrambi
i contesti e di collegarle agli estremi di uno stesso canale.
In questa fase inizialmente solo il server, come dettato dalle
specifiche dei server di giochi ospitabili in NetGame, ha
conoscenza di tutti i partecipanti. Proprio esso ha il compito
di instaurare tale connessione e lo fa come mostrato in figura.
In dettaglio le operazioni sono le seguenti :
- Il server invoca il metodo
remoto
prepare() sul master
- Il master crea una
ServerSocket
su una porta libera ed un InfoKey per l'accesso
- Il master comunica (in realtà
valore di ritorno della
prepare() ) al server l'oggetto
InfoKey
- Il server invoca sullo slave
il metodo remoto
start()
- Lo slave crea una socket e...
- ... la connette a quella del
master
L'oggetto InfoKey contiene tutte
le informazioni necessarie allo slave per individuare il master;
in particolare :
- Indirizzo
del nodo su cui si trova il master
- Porta della socket su cui
il master attende connessioni
- Password
In realtà la password non è
altro che un intero. Esso è generato in maniera random
sul master in seguito alla chiamata da parte del server della
funzione prepare() ed è utilizzato per evitare
che un altro processo (ad esempio un client di un'altra partita
di PingGame) possa intromettersi nella partita in questione.
Anche la porta viene scelta dinamicamente, facendo una serie
di tentativi finchè la creazione non ha successo.
Fase di settaggio dei parametri
In questa fase, che ha lo scopo di definire
tutti i parametri che caratterizzeranno la partita, i giocatori
si scambiano messaggi della classe SetParMessage . (In
realtà si scambieranno sottoclassi di tale classe,
specifici per il tipo di parametro che andranno a modificare).
Anche se la tassonomia di classi sarà affrontata con
maggior dettaglio nell'implementazione, qui se ne da comunque
un accenno per poter chiarire meglio il funzionamento.
Tutti i messaggi usati in PingGame sono
oggetti di sottoclassi della classe PingGameMessage .
Essa presenta una variabile intero di nome mode . Tale
variabile ha il compito di distinguere un tipo di messaggio
dall'altro. e potrà così essere utilizzata dal
ricevente per ricostruire adeguatamente l'informazione che
gli è stata trasmessa.
Quindi la forma generale di ogni messaggio è :
In questa fase i messaggi hanno la struttura :
Nel campo mode tutti i messaggi
di classe SetParMessage presentano il valore SETPARMODE
(0 in questa implementazione).
Il campo parameter serve ad identificare il tipo di parametro
a cui ci si riferisce. I parametri che possono essere modificati
sono stati presentati in fase di analisi.
Il campo value contiene il valore di tale parametro.
Può avere natura diversa a seconda del parametro; in
particolare può essere un intero, un reale o una stringa.
Come già si è detto in
questa fase vi è ancora distinzione forte tra master
e slave. In particolare il master ha il diritto di modificare
tutti i parametri tranne quelli personali dello slave, mentre
lo slave può modificare esclusivamente quelli.
La partita
A questo punto, quando il master decide
che i parametri sono appropriati, ha inizio la partita.
Possiamo schematizzare il comportamento
dei clients durante la partita con un diagramma degli stati.
Un giocatore può trovarsi in uno dei seguenti stati
:
MYCTRLSTATUS
TRASMITCTRLSTATUS
YOURCTRLSTATUS
Il giocatore ha sempre il controllo
della sua racchetta. A seconda dello stato in cui si trova
può avere anche il controllo della pallina ed il compito
di segnalare eventuali goals.
Lo stato della partita, rappresentato oltre che dallo score,
dalle posizioni e velocità delle due racchette e della
pallina, è quindi mantenuto in modo distribuito da
entrambi i giocatori.
In particolare lo stato della pallina viene alternativamente
gestito dall'uno o dall'altro. Si è fatta al riguardo
la scelta di assegnare il calcolo della sua posizione e della
sua velocità al giocatore maggiormente interessato
in quel momento, ovvero a quello che presidia la zona in cui
la pallina si trova. Questo perchè tale giocatore è
quello che presumibilmente si appresta a colpire la pallina,
e che quindi deve sapere con esattezza i suoi dati, prevenendo
i già citati problemi connessi alla latenza di comunicazione.
In particolare nel primo stato (MYCTRLSTATUS )
il giocatore ha il controllo della pallina e ne trasmette
la posizione anche all'avversario, che si troverà necessariamente
nel terzo stato (YOURCTRLSTATUS ).
Nel secondo stato (TRASMITCTRLSTATUS ) il giocatore
calcola la posizione della pallina, ma solo per uso locale,
cioè non la trasmette all'avversario.
Gli eventi che possono generare in modo
asincrono una transizione di stato sono di due tipi :
- Ricezione di un messaggio dall'avversario
- Movimento della pallina in
determinate zone del campo
La seguente figura mostra schematicamente
la transizione tra gli stati, specificando gli eventi che
li causano.
Il master comincia la partita nello
stato MYCTRLSTATUS , in quanto la pallina è posta
inizialmente ferma nella sua zona. Lo slave invece è
inizialmente in YOURCTRLSTATUS .
Il passaggio dal primo al secondo stato è effettuato
quando viene verificata localmente la seguente condizione
(PallaOver):
la pallina è uscita dall'area
del giocatore. Ovvero, per la prima volta, le coordinate
della pallina sono esterne alla sua area e la sua velocità
ha una componente orizzontale verso l'area dell'avversario.
Nel secondo stato si ha già comunicato
all'altro la necessità che egli assuma il controllo
della pallina, però non si è ancora ricevuta
la sua conferma, quindi si continua a gestire lo stato della
pallina per poterla comunque disegnare in questa fase di "transizione".
Per poter comprendere meglio il comportamento
in questa fase vengono ora elencati in dettaglio i formati
dei messaggi trasmessi durante la partita. Si tralascerà
il campo mode che, come detto, serve solo ad identificare
il tipo di messaggio, ed è comunque da intendersi come
primo campo di ogni messaggio.
MyControlInfoMessage
CURRENTFRAME
|
X
|
Y
|
BALLX
|
BALLY
|
CURRENTFRAME è un intero che
indica il numero di frame a cui si riferiscono le informazioni.
Viene usato per mantenere il più possibile sincronizzate
le visione del gioco dei due clients.
(X,Y) sono le coordinate della racchetta del mittente.
(BALLX, BALLY) sono le coordinate della pallina.
YourControlInfoMessage
CURRENTFRAME è un intero che
indica il numero di frame a cui si riferiscono le informazioni.
(X,Y) sono le coordinate della racchetta del mittente.
ChangeControlMessage
CURRENTFRAME
|
X
|
Y
|
BALLX
|
BALLY
|
BALLVX
|
BALLVY
|
CURRENTFRAME è un intero che
indica il numero di frame a cui si riferiscono le informazioni.
(X,Y) sono le coordinate della racchetta del mittente.
(BALLX, BALLY) sono le coordinate della pallina.
(BALLVX, BALLVY) sono le componenti della velocità
della pallina.
Questo messaggio viene inviato quando viene passato il controllo
della pallina da un giocatore ad un altro. Le informazioni
che vengono quindi inserite devono avere maggiore precisione
rispetto ai casi precedenti, in cui l'uso che ne faceva il
ricevente era semplicemente in veste grafica. In particolare
in questo caso sia la posizione che la velocità della
pallina vengono rappresentati con valori reali e non interi.
FinishMatchMessage, GoalScoredMessage, FinishMatchMessage
Questi messaggi non hanno bisogno di
ulteriori campi oltre al mode in quanto il loro significato
è unico ed inequivocabile.
Vediamo ora più in dettaglio
i comportamenti dei giocatori in ognuno dei tre stati.
Si è già detto come esistano due tipi di eventi
asincroni che causino l'evoluzione dello stato :
al gioco è stata imposta una determinata frequenza
di aggiornamento dell'immagine (20 Hz). Ad ogni frame facciamo
corrispondere un evento, nel senso che la disposizione della
pallina può portare ad una transizione
di stato o comunque all'invio di opportuni messaggi all'avversario
(si pensi alla realizzazione di 1 goal). Questo tipo di evento
possiamo quindi considerarlo sincrono, nel senso che si verifica
pressoché costantemente ogni 50ms (vedremo poi in seguito
come cercare di ottenere una frequenza unica anche su macchine
diverse).
Questo non si può dire per il secondo tipo di evento,
che corrisponde ad un messaggio ricevuto. Evidentemente questo
aspetto è molto connesso all'affidabilità della
rete, all'eventuale presenza di buffer, ecc. Potrebbe per
esempio succedere che la macchina degli stati riceva consecutivamente
tre eventi interni e poi tre eventi da messaggio, mentre l'ideale
sarebbe un'alternanza tra i due tipi (almeno per la fluidità
dell'immagine, anche se poi si vedrà un possibile metodo
per cautelarsi sotto questo aspetto).
Nella trattazione seguente non sono
considerati solo gli eventi significativi e che effettivamente
si possono verificare con un giocatore in un determinato stato.
(Quindi, per es, non è trattato il caso in cui un giocatore
nello stato MYCTRLSTATUS riceva una segnalazione di
goal da parte dell'avversario. Questo non è possibile,
perchè se il giocatore è in tale stato significa
che la pallina è nella sua area, e quindi se c'è
un goal evidentemente dovrà essere verificato da lui
ed ai suoi danni. Se si presentano condizioni incompatibili
di questo tipo, significa che si è verificato qualche
errore e lo stato della partita non è consistente alle
due parti. La partita viene perciò forzatamente terminata).
Nelle tabelle seguenti il primo campo individua il tipo di
evento. Per MSG si intende la ricezione di un messaggio, mentre
con AGG la verifica di un nuovo frame.
Operazioni in MYCTRLSTATUS
TIPO EVENTO
|
DESCRIZIONE EVENTO
|
NUOVO STATO
|
MSG. INVIATO
|
MSG
|
YourControlInfoMessage |
MYCTRLSTATUS |
-
|
AGG
|
Palla transita fuori dalla mia zona |
TRASMITCTRLSTATUS |
ChangeControlMessage |
AGG
|
Goal subito e partita finita |
FINE!
|
GoalScoredMessage;
FinishMatchMessage |
AGG
|
Goal subito e partita non finita |
MYCTRLSTATUS |
GoalScoredMessage |
AGG
|
Nessuno dei precedenti |
MYCTRLSTATUS |
MyControlInfoMessage |
Operazioni in TRASMITCTRLSTATUS
TIPO EVENTO
|
DESCRIZIONE EVENTO
|
NUOVO STATO
|
MSG. INVIATO
|
MSG
|
YourControlInfoMessage |
TRASMITCTRLSTATUS |
-
|
MSG
|
MyControlInfoMessage |
YOURCTRLSTATUS |
-
|
AGG
|
-
|
TRASMITCTRLSTATUS |
YourControlInfoMessage |
Operazioni in YOURCTRLSTATUS
TIPO EVENTO
|
DESCRIZIONE EVENTO
|
NUOVO STATO
|
MSG. INVIATO
|
MSG
|
MyControlInfoMessage |
YOURCTRLSTATUS |
-
|
MSG
|
ChangeControlMessage |
MYCTRLSTATUS |
-
|
MSG
|
GoalScoredMessage |
YOURCTRLSTATUS |
-
|
MSG
|
FinishMatchMessage |
FINE!
|
-
|
AGG
|
-
|
YOURCTRLSTATUS
|
YourControlInfoMessage |
Essendovi una zona neutra al centro
del campo, in cui nessuno dei due giocatori può agire,
si è introdotta la possibilità di recuperare
la pallina da quella zona (mediante pressione del tasto Esc),
sempre che la sua velocità sia inferiore ad un determinato
valore (valBMin ). In quel caso la pallina viene posta
nel zona del giocatore nel primo stato in quel momento e la
partita procede regolarmente.
Terminazione della partita
La partita può terminare per
diverse ragioni. Il modo più naturale è il raggiungimento
da parte di uno dei due giocatori della quantità di
goals prestabilita per vincere. Essendo però un gioco
di rete si possono verificare anche problemi di comunicazione,
o la caduta di uno dei nodi che ospitano i clients. In questi
casi la partita viene forzatamente terminata.
In ogni caso la routine di chiusura si dovrà preoccupare
di fare pulizia riguardo alle risorse allocate (socket, thread,...)
e si dovrà provvedere ad avvertire il server del gioco
(e di conseguenza anche tutto l'environment NetGame) della
terminazione.
Miglioramenti possibili
Già a livello di progetto un
miglioramento possibile dell'applicazione può essere
ottenuto disegnando, in corrispondenza del frame locale n,
non l'ultimo frame remoto ricevuto, ma una sua proiezione
all'istante n. Perchè questo sia possibile occorrerebbe
però, anche nel primo e nel terzo stato, comunicare
all'avversario non solo la posizione della mia racchetta ma
anche la sua velocità ed eventualmente la sua accelerazione.
Solo così infatti si può predire con una certa
accuratezza la posizione in un istante successivo.
Di conseguenza la comunicazione si appesantirebbe.
(E' stata comunque implementata nella fase iniziale anche
questa soluzione, non ottenendo però buoni riscontri
dal punto di vista della fluidità dei movimenti).
Struttura interna del client
I client durante una partita si trovano
a dover compiere più azioni contemporaneamente :
- Acquisire l'input dell'utente
da tastiera
- Ricevere i messaggi dall'avversario
- Aggiornare lo schermo ad una
refresh-rate costante.
Queste operazioni possono essere
agevolmente assegnate a tre Thread separati :
InputThread .
Questo thread controlla ad intervalli regolari quali tasti
della tastiera (tra quelli significativi per l'applicazione,
ovvero i 4 tasti direzionali e l'ESC) sono premuti. Aggiorna
di conseguenza degli opportuni contatori che saranno poi
utilizzati da UpdateThread per valutare la forza
da applicare alla racchetta in quell'intervallo di tempo.
SocketThread .
Questo thread è continuamente in attesa sulla socket
di comunicazione con l'avversario per ricevere i messaggi
da esso inviati. Si dovrà occupare (eventualmente
affidandosi ad altre classi) della decodifica dei messaggi
e delle contromosse essendo, come detto, la ricezione di
un messaggio un evento di transizione.
UpdateThread .
Questo thread ad intervalli regolari aggiorna la finestra
della partita. Affidandosi alle formule già presentate
nell'analisi (o meglio ad una discretizzazione di queste)
calcolerà la posizione della sua racchetta ed eventualmente,
a seconda dello stato in cui si trova, anche quello della
pallina. Controllerà se si sono verificati eventi
significativi e nel caso farà le opportune transizioni
di stato oltre a comunicare all'avversario la sua nuova
visione della partita.
Questi thread dovranno operare su aree
di memoria comuni per scambiarsi i risultati delle loro elaborazioni.
Saranno quindi necessarie delle tecniche di sincronizzazione.
Java ci viene in aiuto anche in questo caso mettendo a disposizione,
già nella classe Object, metodi di sincronizzazione
(richiesta di lock, attesa (wait) e notificazione (signal))
su oggetti.
In particolare, InputThread e UpdateThread dovranno
acquisire il lock dell'oggetto semaphore prima di accedere,
in scrittura (InputThread ) o in lettura (UpdateThread ),
alle variabili che rappresentano l'input dell'utente.
Inoltre SocketThread e UpdateThread
dovranno acquisire il lock dell'oggetto statusSem prima
di effettuare le operazioni (transizioni di stato, modifica
dello stato inteso come coordinate e velocità, invio
di messaggi) conseguenti alla valutazione di un evento.
Discretizzazione dei calcoli
Le formule differenziali presentate
nell'analisi per calcolare traiettorie ed urti di pallina
e racchette vengono in realtà discretizzate considerando
solo alcuni istanti di tempo, in particolare quelli in cui
viene aggiornata la finestra.
Se due istanti di tempo successivi : t-1 e t, posizione, velocità
ed accelerazione all'istante t sono calcolate in funzione
di quelle all'istante t-1, come espresso dalle seguenti equazioni
:
Esse assumono quindi la seguente forma :
dove in spinta si tiene conto dei comandi
immessi dall'utente nell'intervallo di tempo tra i due istanti
suddetti.
Integrazione in NetGame e modalità Stand-Alone
PingGame può essere inserito
sia come modulo all'interno di NetGame che giocato in modalità
stand-alone.
Per essere riconosciuto da NetGame, le classi
che lo implementano dovranno strutturarsi tassonomicamente secondo le
specifiche proprie dell'ambiente NetGame.
Questo è realizzato, come si può vedere nell'implementazione.
Si è visto
come avviene la connessione iniziale tra i giocatori nel caso
che il gioco sia integrato in NetGame.
Nel caso che vi si voglia giocare anche
come applicazione a se stante è possibile farlo. In
questo caso, però, mancando l'interazione da parte
del server (almeno in questa implementazione) lo slave dovrà
saper in altro modo il nodo, la porta e la password per connettersi
al master. I valori di default per porta e password sono rispettivamente
5000 e 0.
|