MokaByte Numero 33  -  Settembre  99 
Virtual Pub
di 
Giovanni
Lagorio
Virtual Pub è un gioco di simulazione basato su architettura Client Server.  Analiziamo la sua struttura e la sua evoluzione per capire qualcosa di più su Java



“Ti trovi all'interno di un dirigibile ipertecnologico del 25° secolo in orbita attorno alla luna. Stai partendo per una vacanza verso il posto più esotico del sistema solare `DreamLand'…” 
Inizia così la storia di Virtual Pub (http://www.virtualpub.net), un’avventura multiutente completamente in italiano nata nel 1996 e, da allora, in continua crescita. Non è semplice definire Virtual Pub; sicuramente è un videogioco ma, a differenza delle avventure tradizionali, è anche un ambiente virtuale che rende possibile conoscere altre persone ed interagire con loro. Infine, ma certamente non meno importante per noi sviluppatori, è un programma Java. Quest’articolo si concentrerà proprio su quest’ultimo punto di vista discutendo le problematiche che si sono dovute affrontare nello sviluppo del gioco e analizzando le soluzioni adottate. L’articolo non si prefigge di esporre lo stato dell’arte dello sviluppo di videogame, ma piuttosto di inquadrare in una prospettiva realistica vantaggi e svantaggi della scelta di utilizzare Java per lo sviluppo di questo genere di applicazioni.

Virtual Pub, d’ora in poi semplicemente “Pub”, è un sistema client/server. Il server si occupa della gestione degli utenti e di tutto il mondo virtuale nel quale si svolge il gioco. Il programma lanciato dall’utente per “entrare” nel pub è invece il client, scritto in Java, che si collega al server sia per ottenere informazioni (descrizione degli ambienti, lista degli utenti collegati in un dato momento, etc) sia per interagire con gli altri utenti (parlare, rubare oggetti e così via). Ad esempio, quando due persone dialogano tra loro all’interno del gioco i rispettivi client non si scambiano direttamente i messaggi, ma lo fanno attraverso il server. Quello che succede, dietro le quinte, è che il client del primo utente manda un messaggio al server che, a sua volta, lo rispedisce al client del secondo. 
Il server, sviluppato in linguaggio C, gira su una macchina Linux (Intel). La scelta del linguaggio C è stata conservativa: l’altro candidato naturale era il C++ ma tre anni fa i compilatori free erano decisamente immaturi (ricordiamo che, in fondo, il C++ è stato standardizzato solo da pochi mesi). In retrospettiva credo sia stata fatta la scelta giusta, anche se oggi io sceglierei il C++ senza riserve . Java, ai tempi, non è stato neppure preso in considerazione perché era ancora un perfetto sconosciuto per tutti i membri del team di sviluppo. Questa è stata una grossa fortuna perché la stabilità del server è una caratteristica irrinunciabile in questo tipo di applicazioni e le JVM (1.1.x) hanno iniziato ad essere usabili solo da pochi mesi e sono tuttora piene di problemi.  Forse Java 1.2 potrebbe essere preso in seria considerazione nello sviluppo di un server, ma non ho ancora acquisito sufficiente familiarità con questo strumento per esprimere un giudizio. 
Inizialmente anche il client era scritto in C e la sua interfaccia era limitata alla modalità solo testo. La versione Java, nata agli inizi del 1998, è passata attraverso varie operazioni di “lifting” fino ad arrivare alla versione attuale che potete vedere negli screen-shots delle figure [virtual1.jpg e virtual2.jpg].
 
 
 
 

Comunicazioni

Far comunicare due applicazioni Java è piuttosto semplice: è sufficiente aprire un socket e, grazie alla serializzazione (introdotta con la versione 1.1 del JDK), passare da una parte all’altra strutture arbitrariamente complesse. Ho deliberatamente omesso i dettagli; nel mondo reale le cose non sono così facili… ma quasi.
I socket (classe Socket) forniscono un canale bidirezionale di comunicazione fra un client e un server. In Java questo si traduce nell’avere a disposizione un OutputStream e un InputStream nei quali è possibile, rispettivamente, spedire e ricevere bytes in modo affidabile (è compito del protocollo TCP garantire che i dati arrivino integri e nella corretta sequenza). Il passo successivo è costruire un OutputObjectStream e un InputObjectStream sui precedenti stream ed il gioco è fatto: salvo esigenze particolari i metodi readObject e writeObject penseranno a tutti i dettagli di basso livello.
Far comunicare un’applicazione Java e un’applicazione non-Java, in questo caso un programma C, è un pochino più macchinoso, anche se concettualmente non molto diverso.
Stabilire una connessione è facile: l’idea di socket è indipendente dal linguaggio di programmazione; i problemi nascono quando si devono passare i dati da una parte all’altra. Un modo è quello di serializzare “a mano” gli oggetti che si intendono trasmettere. Serializzare un oggetto significa ottenere una sequenza di bytes che permetta, successivamente, di costruire un oggetto equivalente all’originale. Una sequenza di bytes, ad esempio sotto forma di array, può essere scritta senza problemi su un qualsiasi stream e, in particolare, può essere trasmessa attraverso un socket. 
Creare un array di bytes da trasmettere è un’operazione semplice ma scomoda in quanto gli array hanno una dimensione fissa decisa al momento della loro creazione mentre la dimensione in bytes di un oggetto serializzato dipende quasi sempre dal suo stato e, di conseguenza, cambia continuamente al runtime. Per questo facilitare la creazione degli array sono state scritte due classi: ByteArray e ByteArrayFIterator. La prima, come suggerisce il nome, rappresenta un array che cresce dinamicamente man mano che i bytes vengono aggiunti; la seconda è un iteratore (la “F” sta per forward) che permette di leggere o scrivere il contenuto dell’array (incrementandone le dimensioni automaticamente, non si “esce” mai dall’array scrivendoci dentro).
Consideriamo il seguente frammento di codice:
 
ByteArray ba = new ByteArray() ;
ba.add("Mokabyte") ;
ba.add("!") ;
byte [] bytes = ba.toBArray() ;
System.out.print("bytes = {") ;
for(int a=0; a<bytes.length; ++a)
 System.out.print(bytes[a]+" ") ;
System.out.println("}") ;
ByteArrayFIterator it = new ByteArrayFIterator(bytes) ;
System.out.println("Stringa1 = "+it.popString()) ;
System.out.println("Stringa2 = "+it.popString()) ;


L’esempio usa un oggetto ByteArray per serializzare due stringhe: “Mokabyte” e “!”; dopo di che mostra in output il contenuto dell’array di bytes e “deserializza” le stringhe mediante un iteratore ByteArrayFIterator.
Il suo output è:
 

bytes = {8 0 0 0 77 111 107 97 98 121 116 101 1 0 0 0 33 }
Stringa1 = Mokabyte
Stringa2 = !


Nella figura sottostante è mostrato, commentato, il contenuto dell’array di bytes.

I primi 4 bytes (cioè un intero serializzato) contengono la lunghezza della stringa “Mokabyte”, i successivi 8 bytes contengono invece i caratteri veri e propri. I bytes rimanenti contengono la forma serializzata della stringa “!”.
Nella  figura seguente sono rappresentate le classi principali che riguardano la comunicazione con il server: tutti gli oggetti che devono essere scambiati con il server appartengono a classi che discendono dalla classe astratta Packet, mentre la classe Server incapsula i dettagli della connessione e si occupa della spedizione/ricezione dei pacchetti.
 
 
 










La classe Packet contiene un ByteArray che rappresenta la forma serializzata dell’oggetto e un campo intero che ne identifica il tipo. Il tipo codifica la classe dalla quale istanziare l’oggetto che deve essere deserializzato. La classe Packet ha due discendenti diretti: IPacket e la simmetrica OPacket. La prima è classe base di tutti i pacchetti di input (che viaggiano dal server verso il client) e la seconda di quelli di output (trasmessi in direzione opposta). 
La classe IPacket implementa inoltre l’interfaccia Runnable, in modo tale che, non appena viene ricevuto un pacchetto esso possa venir eseguito invocando il metodo run (eventualmente in un altro thread, senza dover ricorrere all’uso di un adapter).
La creazione di oggetti, a partire dal tipo intero codificato nel pacchetto, è delegata ad una classe che implementi l’interfaccia PacketsFactory in modo tale da rendere riutilizzabile la classe Server in diversi contesti.
 
 
 
 

Interfaccia grafica

Quando il client del Pub è stato portato in Java si voleva non solo ottenere un programma in grado di girare su sistemi diversi, ma un prodotto la cui interfaccia fosse consistente su tutte piattaforme.
L’AWT, l’unica scelta al momento del porting, fornisce le funzionalità interfaccia utente indipendenti dal sistema ospitante attraverso l'uso di classi peers native. Il motivo di tale scelta è permettere il porting di AWT non solo su diversi sistemi operativi a finestre, ma anche su dispositivi hardware diversi dal “tradizionale” PC; ad esempio si può pensare di "mappare" un pulsante fisico di un telefono su un pulsante (Button) di AWT. Il prezzo di tutta questa indipendenza è che un pulsante sotto Windows, pur funzionando come uno sotto Linux, ha delle dimensioni ed un aspetto diversi. Non solo: i widget che AWT mette a disposizione sono molto limitati: è, ad esempio, impossibile cambiare il colore di un singolo item di una listbox. Questi motivi hanno portato allo sviluppo di una gerarchia di componenti che offrissero caratteristiche più avanzate (ad esempio menù popup, tooltip) e avessero un aspetto identico su ogni sistema. Tutto questo è stato ottenuto usando come unico componente AWT un Canvas per ogni finestra o dialogo (questi ultimi sono normali Frame e Dialog AWT). Tutti i componenti usati nel Pub (pulsanti, textbox, listbox e così via) vengono disegnati “a mano”. La libreria di componenti permette inoltre l’uso di pulsanti di forma qualsiasi grazie all’uso di maschere di trasparenza, si veda ad esempio la rosa dei venti della figura:


 
 
 
 
 
 
 
 
 

essa mostra, per il pulsante “nord”, la maschera di trasparenza, il pulsante premuto ed, infine, il pulsante disabilitato. 
L’unica limitazione della libreria, rispetto ad AWT, è che la gerarchia a video dei componenti è piatta, ovvero non esiste nessuna relazione contenitore/contenuto che permetta di “inscatolare” i componenti uno dentro l’altro. Arrivati a questo punto sembra d’obbligo un confronto con Swing; dal punto di vista di architettura delle classi e di flessibilità ritengo che Swing non abbia rivali, ma dal punto di vista dell’efficienza è veramente penosa: per essere usabile richiede macchine veramente potenti e molta, troppa, RAM. Per questo motivo, almeno attualmente, Swing risulta inusabile per lo sviluppo di un videogioco.
 
 
 
 

Suoni

Ogni videogioco che si rispetti non può fare a meno di qualche effetto sonoro, il Pub non è un’eccezione. Purtroppo il supporto sonoro offerto da Java 1.1.x è veramente scadente. La cosa più scandalosa è che non esiste un metodo standard, documentato, per riprodurre un suono da un’applicazione! Inoltre agli applet, gli unici fortunati fruitori di AudioClip, sono permesse solamente tre operazioni: play, stop e loop. Non è nemmeno possibile sapere se e quando la riproduzione di un suono è terminata. Ciliegina sulla torta: l’unico formato supportato è un orrendo AU mono a 8 kHz.
Per il primo problema, quello che solo gli applet possono caricare degli audioclip, esiste almeno una soluzione che consiste nell’usare una classe non documentata (sun.applet.AppletAudioClip), per tutti gli altri problemi l’unica soluzione nota è quella di rassegnarsi. In realtà esistono due altre potenziali soluzioni, entrambe impraticabili, almeno per ora. La prima è quella di passare all’uso di Java 1.2, dove finalmente anche le applicazioni possono utilizzare i suoni e sono supportati molti più formati audio (purtroppo l’interfaccia è rimasta AudioClip e quindi piuttosto povera di funzionalità). Questa strada non risulta percorribile, per il Pub, poiché il runtime di Java 1.2 richiede una configurazione minima (reale) di almeno 64 mega per ottenere prestazioni accettabili che riteniamo sia davvero eccessiva. Una seconda possibilità sarebbe quella di utilizzare il Java Media Framework, un framework aggiuntivo che permette la riproduzione di molti formati audio/video. Purtroppo la versione 1.x nativa per Windows era molto instabile, quella puro-Java troppo lenta. È in beta la nuova versione… speriamo che sia meglio della precedente (non ci vorrebbe molto!).
Nel frattempo, il Pub (dalla prossima versione per Windows) utilizzerà i suoni in formato MP3 grazie ad una libreria nativa invocata attraverso l’uso di JNI.
 
 
 
 

Conclusioni

Java è uno strumento estremamente potente e flessibile che permetterebbe di realizzare applicazioni piuttosto complesse con relativa facilità. Purtroppo la situazione attuale è piuttosto critica: le JVM 1.1.x hanno gravi problemi (si veda la bug parade sul sito Sun) e la versione 1.2.x richiede una quantità di risorse tali da esser proponibile solo su macchine molto potenti, tipicamente adibite al ruolo di server. Il futuro di Java sul lato client sarà tutt’altro che roseo se Sun non la smetterà di ignorare le richieste degli sviluppatori che non chiedono altro che una piattaforma stabile.
Si veda, ad esempio, http://developer.java.sun.com/developer/bugParade/bugs/4014323.html
un bug segnalato il 12 novembre 1996 e non ancora “fixato”!

Purtroppo non si tratta di una mosca bianca; si veda, in generale, http://developer.java.sun.com/developer/bugParade/index.html
 
 
 

 

Giovanni Lagorio, laureando in Scienze dell’Informazione presso l’Università di Genova, è uno specialista in progettazione e sviluppo con tecnologie object oriented. 
Ha sviluppato progetti in diversi linguaggi, principalmente C/C++ e Java, fra cui Virtual Pub.
È raggiungibile tramite posta elettronica all’indirizzo gio@libertyline.com

  
 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it