Realizzare videogiochi in J2ME

La realizzazione di un videogioco oggi è molto più semplice rispetto al passato non tanto perchè l’hardware permette
effetti impensabili sino a qualche anno fa ma perchè il supporto software offerto dalle API e dai framework sgrava il programmatore di
compiti tediosi quali la gestione delle collisioni o l’animazione degli oggetti.

Tutte queste problematiche sembravano scomparse ma sono riapparse quando si è cercato di scrivere videogiochi
per la piattaforma Java per il mobile, J2ME.
La piattaforma J2ME non è certo nata per fare videogiochi ma le sue risorse così limitate hanno
rappresentato una sfida tecnologica per moltissimi geek che hanno voluto vedere cosa si poteva creare.

Questo articolo non vuole essere un corso di programmazione di videogiochi per J2ME e neppure un corso sulle
tecniche più avanzate ma solo un racconto delle difficoltà tecniche incontrate durante la realizzazione
di un gioco commerciale su piattaforma J2ME e delle soluzioni adottate.

Cosa è J2ME

Java 2 Mobile Edition è la piattaforma di Sun per le applicazioni su dispositivi mobili; cellulari, palmari.
E’ una Java Virtual Machine (JVM) molto simile alla versione 1.1 e
si divide in due componenti fondamentali il “Connected Limited Device Configuration” (CLDC) ed il
“Mobile Information Device Profile (MIDP)”.
CLDC è la vera virtual machine e contiene le classi di sistema dei package java.io, java.lang e java.util.
MIDP contiene le classi di interfaccia utente, di networking e gestisce il ciclo di vita di un’applicazione.

Midlet, file .jad e .jar

Quando si vuole installare sul proprio telefonino un’applicazione J2ME, che si chiama midlet, serve il file jar (formato ben noto nel modo Java) ma anche
un file con estensione jad che contiene informazioni come la dimensione ed il nome dell’applicazione.
Il file jad dovrebbe essere sempre presente però molti produttori non lo richiedono.

Architettura MIDP – Classe Canvas

La scrittura di una interfaccia grafica J2ME richiede la conoscenza di alcune classi di MIDP, che qui non verranno
approfondite. Ci concentremo, invece, sull’unica classe indispensabile per scrivere videogiochi, la classe Canvas.

La classe Canvas
permette un accesso a “basso livello” all’interfaccia grafica e alle notifiche di eventi di tasti premuti.

Il ciclo di vita dell’applicazione è gestito da un thread che dialoga con il canvas, questo
è talmente frequente che spesso l’oggetto canvas stesso è Runnable.

class GameCanvas extends Canvas implements Runnable {
    public GameCanvas(MIDlet game) {
        new Thread(this).start();
    }

    public void run() {

        while (true) {
        	// wait exit event
        	// ...
        }
    }
    ...
}

I formati grafici supportati da J2ME sono JPEG e PNG.
Per la scrittura di giochi è consigliato PNG perché gestisce la trasparenza.

J2ME e portatilità del codice

Sappiamo che se esiste una Java Virtual Machine per una piattaforma allora su di essa girerà tutto il codice Java,
previa compatibilità di versione (1.4, 1.5 e così via).
la stessa cosa vale per J2ME previa compatibilità di versione di MIDP e CLDC.

La portatilità di applicazioni Java è in effetti, eccetto casi rarissimi, del 100%.
Ma se questo 100% è corretto nel mondo desktop (J2SE e J2EE) e nel mondo J2ME di applicazioni “normali”
non lo è per videogiochi J2ME.

Per evitare malintesi diciamo che il codice è portabile al 100% ma
l’applicazione si comporta in modo diverso da piattaforma a piattaforma e spesso si deve ricorrere
ad API proprietarie o bisogna fare i salti mortali.

Il progetto

Tutto nasce dalla richiesta di realizzare uno sparatutto “portabile” e giocabile sul telefonino.
Dato che Symbian è supportato da un sottoinsieme dei
modelli in commercio e soprattutto perché presente esclusivamente nei
cellulari di fascia medio/alta, è stato scelto J2ME.

Alla luce di questa scelta si definiscono le specifiche progettuali, alcune delle quali verrano cambiate o eliminate in corso d’opera:

  • Il gioco è il più classico degli sparatutto, l’astronave amica deve respingere varie ondate
    di nemici che cambiamo forma, diventano più resistenti e più veloci al passare dei livelli
  • Le marche su cui deve funzionare devono essere almeno Nokia, Siemens e Motorola
  • Deve funzionare anche sui vecchi cellulari con MIDP 1.0 (scelta assai discutibile)
  • Il suono deve sfruttare la polifonia
  • Il gioco deve essere pronto per ieri altrimenti la concorrenza ci frega
  • Il codice deve essere unico per tutti i dispositivi, non sono previste versioni per modello e/o marca

L’ultima decisione sarà quella con maggior impatto negativo nel ciclo di sviluppo.

Si scoprirà poco dopo che MIDP 1.0 non supporta il suono quindi il gioco sarà muto.

Pronti? Si parte!

Il risultato del prototipo

Figura 1. Il risultato del prototipo

La tastiera

Viene realizzato un prototipo dove l’astronave si muove e (contemporanemente) spara ma nascono i primi problemi.

Sugli emulatori Nokia tutto funziona, sui cellulari Nokia non su tutti i modelli provati, su Motorola
e Siemens si ha una casistica variegata (un modello sì, uno no).

I problemi sono tutti legati alla tastiera:

  • su alcuni modelli tenendo premuto un tasto non arriva l’evento di un secondo (o un terzo) tasto premuto.
    Implica che se l’astronave si muove non può sparare o viceversa
  • su alcuni modelli funziona correttamente la pressione di due tasti
    simultaneamente ma l’azione di rilascio viene notificata solo quando
    entrambi sono stati rilasciati. Ciò implica che quando l’astronave
    si muove sparando il rilascio di un tasto di movimento non le
    impedisce di continuare a muoversi
  • mantenendo premuto un tasto arriva solo il primo evento onkeypress anziché ripetuti eventi di pressione dello stesso
    (anche se MIDP prevede un esplicito evento keyRepeated).
    Implica che alla prima pressione l’astronave fa un movimento ma il tasto va rilasciato
    e premuto ripetutamente per vederla spostarsi

Da questa esperienza arriva la prima (dura) lezione: gli emulatori non sono affidabili e l’hardware fa la differenza.

Soluzione ai problemi con la tastiera

Visto che keyRepeated non funziona su tutti i modelli allo stesso modo si decide di memorizzare lo stato corrente dei tasti
premuti utilizzando gli eventi keyPressed e keyReleased e, nel loop principale dell’applicazione, di verificare lo stato di questa variabile.
La variabile può contenere lo stato di più tasti perché ogni bit contiene l’informazione premuto/rilasciato di un tasto.

Il gioco usa 5 bit della nostra variabile pressedKeys

    private byte pressedKeys;

    public final static byte KEY_NONE    = 0;
    public final static byte KEY_LEFT    = 1 << 0;
    public final static byte KEY_RIGHT   = 1 << 1;
    public final static byte KEY_UP      = 1 << 2;
    public final static byte KEY_DOWN    = 1 << 3;
    public final static byte KEY_FIRE    = 1 << 4;

    /**
     * keyCode contiene i codici J2ME dei tasti
     * keyActions contiene le azioni associate ai tasti
     * es. tasto 1 sposta in alto a sinistra
     *     tasto 9 sposta in basso a destra
     */
        keyCodes[0]     = getKeyCode(LEFT);
        keyActions[0]   = Shape.KEY_LEFT;
        keyCodes[4]     = getKeyCode(FIRE);
        keyActions[4]   = Shape.KEY_FIRE;


        keyCodes[5]     = KEY_NUM1;
        keyActions[5]   = Shape.KEY_LEFT | Shape.KEY_UP;
        keyCodes[13]     = KEY_NUM9;
        keyActions[13]   = Shape.KEY_RIGHT | Shape.KEY_DOWN;

    private void flushKeys() {
        pressedKeys = KEY_NONE;
    }

    protected void keyPressed(int keyCode) {
        for (int i = 0, s = keyCodes.length; i < s; i++) {
            if (keyCode == keyCodes[i]) {
                pressedKeys |= keyActions[i];
                break;
            }
        }
    }
    
    protected void keyReleased(int keyCode) {
        for (int i = 0, s = keyCodes.length; i < s; i++) {
            if (keyCode == keyCodes[i]) {
                pressedKeys &= ~keyActions[i];
                break;
            }
        }
    }

    public void mainLoop() {
        while (1) {
            ...
            ...
            cannon.move(pressedKeys);
            if ((pressedKeys & Shape.KEY_FIRE) == Shape.KEY_FIRE) {
                cannon.fire();
            }
        }
    }

Collisioni

Una collisione avviene quando due oggetti occupano lo stesso spazio sullo schermo ad esempio quando un missile colpisce un’astronave.
E’ una delle operazioni più delicate perché deve essere assai efficiente dato che un buon 60%-70% di uno sparatutto
non fa altro che collision detection.

MIDP 2.0 semplifica enormemente la gestione delle collisioni disponendo di un oggetto Sprite
che si occupa di visualizzare una particolare immagine presa da un insieme e di verificare se una di queste immagine collide con un altra.
Il metodo collidesWith
è in grado di verificare se avviene una collisione pixel per pixel (ignorando eventuali aree di trasparenza) oppure una semplice
verifica di intersezione tra due aree geometriche.
Lo svantaggio principale del confronto tra aree è che può essere rilevata una collisione quando ancora gli oggetti non si sono “realmente” toccati come mostrato in Figura 2.
Il grande vantaggio è dato dalla velocità di esecuzione.

Collisione tra due aree geometriche

Figura 2. Gli oggetti non si “toccano” ma viene rilevata una collisione

Uno dei vincoli progettuali però vuole la compatibilità con MIDP 1.0 che non ha un oggetto Sprite e tanto meno un modo standard per rilevare collisioni.
Il problema non è grave, in fondo si tratta di verificare delle coordinate.

    public boolean collidesWith(Shape shape) {
        final int j = (x + width) - 1;
        final int l = (y + height) - 1;
        final int j1 = (shape.x + shape.width) - 1;
        final int l1 = (shape.y + shape.height) - 1;
        if (shape.x > j || j1 < x || shape.y > l || l1 < y) {
            return false;
        }
        return true;
    }

Si noti che Shape è un oggetto scritto per il gioco, che contiene le coordinate dell’immagine e le sue dimensioni.

Un comportamento strano si è verificato con alcuni Siemens sui quali il metodo Sprite#collidesWith di MIDP 2.0
al crescere del numero di oggetti sullo schermo rallentava il gioco, sostituendo la chiamata con il nostro metodo il problema è scomparso, meglio così.

Effetti di animazione

Ogni buon gioco per risultare accattivante deve avere qualche animazione ad esempio un’eplosione oppure un alieno che mentre si muove ruota.
Un’animazione deve rimanere fluida indipendentemente dal numero di oggetti sullo schermo, ovviamente con dei limiti.

L’oggetto Sprite di MIDP 2.0 offre la possibilità di visualizzare il frame precedente o successivo tramite
prevFrame
e
nextFrame.
Questi metodi però non sono sufficienti a realizzare un’animazione realistica, uno dei problemi principali è quello
di avere oggetti diversi che cambiano aspetto in tempi diversi.
Un’esplosione può cambiare frame in N millisecondi, un alieno invece può richiede un numero di millisecondi diverso.
Per risolvere questo problema ci sono (almeno) due tecniche:

  • ogni oggetto gestisce un proprio timer per cambiare forma
  • tutti gli oggetti presenti sullo schermo sono sincronizzati con un unico timer

La prima soluzione è molto object oriented dato che ogni oggetto è auto consistente.
Purtroppo all’aumentare anche di qualche decina di oggetti sullo schermo il gioco diventa lentissimo, si pensi a cento (o mille) GIF animate per capire cosa succede.
La seconda soluzione è più complicata da gestire (neanche tanto) ma ha il vantaggio
di sincronizzare tutti gli oggetti visibili in un solo colpo.

Il loop principale chiama per ogni oggetto visibile il metodo nextFrame mostrato di seguito

    public void nextFrame() {
        long d = frameDelay - GameCanvas.millisec;
        frame = 0;

        for (int i = frameDuration.length; —i >= 0; ) {
            if (d <= frameDuration[i]) {
                ++frame;
            }
        }
    }

L’array frameDuration contiene la durata in millisecondi di visibilità dell’iesimo frame che rimane visibile fino a quando non “scade”.
Ad esempio l’esplosione è composta da tre frame (vedi Figura 3.) il primo deve essere visibile per 120 ms, il secondo per 400
ed il terzo per 200 quindi frameDuration sarà

Frame dell'esplosione

Figura 3. Frame dell’esplosione

frameDuration[] = {120, 400, 200};

Nella realtà le cose sono un pò più complicate per via di varie ottimizzazioni ma quanto detto da l’idea del lavoro che c’è dietro le quinte.

Da questa esperienza arriva la seconda lezione: object oriented sì ma con parsimonia, i videogiochi in J2ME possono risentire pesantemente
di pattern male applicati.

Contenere la dimensione del jar

Il jar installabile su un dispositivo J2ME ha una dimensione massima di qualche centinaio di Kbyte ma varia da marca a marca.
Per ridurre le dimensioni in genere si agisce su due tipi di file: le classi compilate e le risorse grafiche.
Le classi compilate vengono sottoposte ad un’operazione di liposuzione tramite offuscatore che provvede a ridurre al minimo
la lunghezza dei nomi dei metodi e delle variabili più altre operazioni sui byte code.
La tecnica di offuscamento nelle applicazioni J2ME, non solo giochi, è prassi comune e serve non
solo a proteggere il codice quanto ad avere file più piccoli.

Le risorse grafiche vengono invece accorpate in pochi file, ad esempio l’intera sequenza di frame dell’esplosione di Figura 3.
è contenuta in un unico file .PNG, poi a livello applicativo si accede ai singoli frame in base alla dimensione.
Questa tecnica è standard in MIDP 2.0 tanto che la classe Sprite agevola enormemente la gestione di immagini packed facendosi
carico di operazioni tediose quali l’accesso all’iesimo frame.
Si può ottenere ancora di più ottimizzando le immagini png con programmi tipo optipng
che possono dare risultati entusiasmanti.

Da questa esperienza arriva la terza lezione: un obfuscator può fare la differenza quando si hanno decine di classi.

Contenere la memoria occupata

Mai come con J2ME è possibile imbattersi frequentemente in messaggi di “Out of Memory Exception”, spesso dovuti ad immagini bitmap troppo grandi.
Qui entra in gioco il buon senso più che un limite di piattaforma, se infatti è vero che i dispositivi J2ME hanno poca RAM è pure
vero che la si può utilizzare con intelligenza.
Il motivo principale di “Out of Memory” è causato da sfondi di gioco troppi grandi, ad esempio il terreno di un pianeta con fiumi, rocce e quant’altro.
MIDP 2.0 offre una classe per gestire i Tiles che altro non sono che immagini di dimensioni ridotte che combinate insieme formano un sfondo.
Non è stato difficile gestire una oggetto simile con MIDP 1.0.

Per non incorrere in problemi di RAM è necessario fare un uso parsimonioso di Vector,
meglio creare matrici piccole e di dimensione finita.
Può sembrare una limitazione forte quella di lavorare con matrici di dimensione finita ma stiamo creando un gioco e sapere
in fase di progettazione quanti oggetti sono presenti in un determinato livello non è una cosa
strana anzi, al contrario, è quasi un prerequisito.

Per quando possibile gli oggetti non più utilizzati vanno deferenziati impostandoli a null.

        Graphics g = fuelImage.getGraphics();
        ...
        ...
        // g non serve piu'
        g = null;

Non mi è mai capitato di fare o vedere una cosa simile in una applicazione Java SE/EE ma su J2ME sì.
Anche questa è una operazione necessaria solo su alcune implementazioni J2ME (Siemens in primis) ma
probabilmente legata ad un garbage collector troppo pigro.

Da questa esperienza arriva la quarta lezione: dimenticare che ci pensa Java a gestire la memoria e pianificare attentamente
il contenuto dei livelli di gioco.

Nei tre mesi di realizzazione sono sorti altri problemi legati all’hardware più vecchio ma alla fine si è deciso
che non valeva la pena incaponirsi.

Tools

Tralasciando i tool realizzati all’interno della software house tra cui un eccellente generatore di tile che andrebbe venduto come prodotto separato,
sono stati usati software open ad eccezione di PhotoShop e di DashO poi abbandonato.

Come IDE l’immancabile Eclipse affiancato da una decina di script ANT.
Il build di jad/jar è stato affidato all’ottimo e semplice Antenna.
Come obfuscator il buon DashO fu sostituito da Proguard che si è rivelato
comunque un prodotto validissimo.
Il già citato optipng è stato affincato da
ImageMagick quando si doveva impacchettare qualche tile velocemente.
Ovviamente non potevano mancare i tool MIDP come Il WTK di SUN,
l’SDK di Nokia e Motorola.
A vario titolo sono stati usati altri tool elencati anche su j2me.javastaff.

Conclusioni

Scrivere giochi è un’attività davvero bella, la componente algoritmica non è mai fine a se stessa e le ottimizzazioni
possono fare la differenza.
Quando poi ci si trova a dover lavorare con pochi kbyte e poca ram il tutto diversa ancora più divertente.
J2ME ha fatto passi da gigante e la versione MIDP 2.0 permette di creare facilmente programmi complessi pur restando
dei problemi tra i vari brand.
Certo non è flessibile come Symbian ma scrivere codice Java è molto più confortevole
che scrivere codice C++.

Chi volesse provare una demo del gioco puo’ scaricare i jad
ed il jar.

Comments

  1. E’ possibile avere il sorgente completo del gioco?

  2. davidef says:

    @Andrea Grandi: Mi spiace ma i sorgenti sono coperti da copyright. Quello che invece sto provando a fare consiste nel riadattare il codice della demo in modo da rendere pubblicabili i sorgenti.

  3. Sinceramente non pensavo che dietro un gioco per cellulari ci fossero tutte queste problematiche: è un lavoro davvero affascinante, occorre saper ponderare fino al minimo dettaglio ogni scelta implementativa…

  4. Mi pare un tuffo nel passato… 1985… giochini per Spectrum Sinclair…

  5. Padrino says:

    Che figata st’articolo.
    La fa molto semplice, però è ben dettagliato.

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.