MokaByte 95 - Aprle 2005 
Layer MIDP2.0
LayerManager, TiledLayer e Sprite
di
Pierluigi
Grassi
Proseguiamo la panoramica sugli strumenti dedicati ai giochi del profilo MIDP2.0. Dopo aver esaminato rapidamente la classe GameCanvas, ci occupiamo degli oggetti destinati a riempirne le istanze. Gli elementi in questione sono tutti sotto tipi di javax.microedition.lcdui.game.Layer.

Layer e LayerManager
Il profilo MIDP2.0 introduce nel mondo J2ME due tipi dedicati alla gestione di immagini, per un uso tipicamente orientato al gioco. Il primo è rappresentato nella classe javax.microediton.lcdui.game.Layer. Un Layer è definibile come un oggetto in grado di riprodursi in un contesto grafico, in una posizione definita da una coppia ordinata di interi (x,y) eventualmente manipolabili. La parte più interessante di Layer è la sua relazione con un altro tipo, LayerManager. Un LayerManager, letteralmente "gestore di livelli", è un tipo complesso, il cui obiettivo è la rappresentazione a video di una collezione di oggetti Layer. Come Layer, LayerManager possiede un metodo per riprodurre il suo contenuto in un contesto grafico. I Layer sono inseriti nel gestore di livelli attraverso i metodi:

append(Layer l);
insert(Layer l, int index);

La riproduzione del contenuto avviene attraverso il metodo:

paint(Graphics g, int x, int y);

L'ordine di riproduzione rispetta il cosiddetto "algoritmo del pittore". Ciò che è inserito per primo è riprodotto per primo. I livelli che seguono sono visualizzati a coprire quelli che precedono. I due valori interi richiesti dal metodo paint rappresentano le coordinate, relative all'origine del sistema di coordinate di Graphics (in alto a sinistra, relativamente allo schermo, in assenza di trasformazioni), a partire dalle quali questo LayerManager genererà il suo contenuto visivo. Ogni Layer contenuto nel gestore di livelli ha per origine quel punto di coordinate (x,y). Oltre a contenere e visualizzare, un LayerManager offre la possibilità di controllare quale regione di spazio sarà presentata a video in durante la riproduzione. Il controllo è disponibile attraverso il metodo:

setViewWindow(int x, int y, int width, int height);

I quattro valori interi sono interpretati come posizione e dimensione di un rettangolo. Questo rettangolo è uno schermo virtuale che scorre sul contenuto del LayerManager. L'invocazione del metodo "paint" riproduce a video quelle parti degli oggetti Layer contenute all'interno di questo rettangolo. Il meccanismo assume un senso pieno se immaginiamo che l'insieme di Layer contenuti nel LayerManager, o al limite un solo Layer, rappresentino una superficie maggiore di quella visualizzabile a video. La finestra di proiezione di un LayerManager, controllata attraverso il succitato metodo setViewWindow, permette di riprodurre di volta in volta una parte di ciò che astrattamente rappresentabile, in quanto contenuto nel gestore di livelli. Il sistema è particolarmente utile per rappresentare le mappe a quadranti (Tile Map) ma niente vieta la sua applicazione ad ambiti ulteriori al gioco (ad esempio un desktop virtuale di grandezza maggiore del display fisico).

 

Sprite
Il tipo javax.microediton.lcdui.game.Sprite è la rappresentazione MIDP2.0 del concetto classico di Sprite. Un elemento visibile e animabile, tanto attraverso uno spostamento nello spazio di coordinate a cui appartiene, quanto attraverso una sostituzione dell'immagine visualizzata. La riproduzione del contenuto avviene attraverso il metodo "paint" che Sprite eredita da Layer. In quanto elemento "vivo", uno Sprite rappresenta in sé anche il concetto di collisione con un altro Sprite. Sotto il profilo dell'animazione per sostituzione, uno Sprite accetta, anche in costruzione, una sequenza di "frame" (così è detta un'immagine che appartenga ad una sequenza animabile). La documentazione del profilo MIDP2.0 è piuttosto esplicita riguardo al meccanismo di definizione dei frame in uno Sprite. La riassumiamo per maggiore chiarezza del discorso. Uno Sprite può avere un'immagine statica. Così costruito, lo Sprite è animabile per spostamento, e non per sostituzione dell'immagine. Può essere il caso di uno Sprite che rappresenti un proiettile: la sua immagine non cambia, muta solo la posizione. Oppure, uno Sprite può avere un'immagine "dinamica". In questo caso, l'immagine che lo Sprite riproduce attraverso il suo metodo "paint" può essere cambiata. Il mutamento avviene scorrendo una lista di frame. La lista di frame è caricata in costruzione e può essere cambiata in esecuzione, attraverso il metodo:

setImage(Image img, int frameWidth, int frameHeight);

In questo oggetto "Image img", ogni frame è adiacente all'altro. Il costruttore ed il metodo su riferito identificano ogni frame in base alla dimensione (frameWidth, frameHeight). Idealmente, l'immagine unica è suddivisa in una griglia, in cui ogni cella ha larghezza frameWidth ed altezza frameHeight. Ad ogni cella è associato un indice progressivo, da 0 a N, a partire dalla prima cella in alto a sinistra (dal punto di vista di chi osservi l'immagine), scorrendo la griglia da sinistra verso destra, dall'alto verso il basso. La ragione di questa apparente complicazione si coglie in termini di prestazioni. Per immagini piccole, immagazzinate in formato compresso, le dimensioni in byte dell'intestazione del file compresso possono essere uguali o superiori a quelle dei dati dell'immagine. In un ambiente in cui i byte contano ancora, la presenza di questo "overhead" è giustamente considerata inaccettabile. La soluzione è generare un unico file immagine, per tutti i frame. In questo modo si ottiene una sola "intestazione", valida per tutte le immagini che compongono l'animazione.
Una volta caricati i singoli frame, uno Sprite consente di definire la sequenza, che costituirà l'animazione, attraverso il metodo:

setFrameSequence(int[] sequence);

L'array "sequence" contiene la successione di indici, assegnati ai singoli frame durante la parzializzazione dell'immagine unica. Il frame attualmente assegnato allo Sprite è controllabile attraverso i metodi:

nextFrame();
prevFrame();
setFrame(int index);

I metodi di scorrimento, nextFrame e prevFrame, consentono di spostarsi lungo l'array sequence. Lo spostamento è ciclico: giunti al termine della sequenza, la successiva invocazione di nextFrame imposterà come attuale il frame contenuto nella posizione 0 della sequenza. A parti invertite, lo stesso vale per prevFrame. Vale la pena notare che l'animazione di uno Sprite MIDP2.0 non prevede il concetto di temporizzazione. In altri termini, il tipo Sprite presuppone che ogni frame dell'animazione perduri per un tempo definito altrove. Accanto al concetto di animazione "per frame", uno Sprite incapsula anche dei metodi utili a determinare quando uno Sprite sia in collisione con un altro Sprite o con un TiledLayer (un tipo di Layer che esamineremo a breve). Il metodo per verificare se uno Sprite collida con un altro è:

public boolean collidesWith(Sprite s, boolean pixelLevel);

Qui di interessante c'è il secondo valore. Una premessa. Il profilo MIDP2.0 introduce la gestione delle immagini a livello pixel. Le API del profilo MIDP1.0 trattavano le immagini come monoliti, al più controllabili attraverso l'oggetto Graphics associato. I nuovi attrezzi consentono, tra l'altro, di attraversare un'immagine passando per i singoli pixel che la compongono. Le API dedicate ai giochi sfruttano questa possibilità anche nella gestione delle collisioni e quel booleano "pixelLevel" ne è testimone. La versione minima della collisione si ottiene passando "false" come secondo argomento. Qui lo sprite controlla semplicemente se la sua area intersechi quella dello Sprite, primo argomento del metodo. Con "true" il controllo scende ai singoli pixel che compongono i due sprite. In questa seconda modalità, si ha collisione quando un pixel opaco dello Sprite invocante si sovrapponga ad un pixel opaco dello Sprite in argomento. L'evoluzione è rilevante, perché l'analisi per pixel consente di gestire collisioni tra sprite evoluti, come quelli usati nei vecchissimi motori pseudo 3D (vecchi in termini di piattaforma Desktop PC), senza dover caricare, oltre alle immagini, delle mappe di collisione.

 

TiledLayer
La classe TiledLayer permette di creare con semplicità delle mappe a quadranti (altrimenti dette "a celle" o, secondo il termine originario, "Tile Map"). Una tile map è un'immagine costituita dalla ripetizione di più immagini aventi le stesse dimensioni. L'immagine della mappa è costruita usando una griglia di celle. Ad ogni cella è associato un indice e questo indice identifica il componente di un array che contiene l'immagine che sarà usata per riempire, a video, quella cella. Anche qui, la documentazione delle API MIDP2.0 è piuttosto esplicita nel descrivere il fenomeno, per cui ci limiteremo a riassumere i punti interessanti.
Il costruttore richiede come argomenti il numero di colonne e righe che costituiranno la griglia della mappa. Segue un oggetto Image che contiene tutte le immagini usate per le celle della mappa. Le immagini sono immagazzinate (e recuperate) esattamente come avviene per i frame di uno Sprite. Gli ultimi due argomenti sono la larghezza e l'altezza dei quadranti della mappa, che devono corrispondere all'altezza ed alla larghezza delle immagini usate per le celle. Una volta creata, la mappa è riempita con il metodo:

public void setCell(int col, int row, int tileIndex);

Qui "tileIndex" rappresenta l'indice associato al set di immagini da usare per le celle. La prima immagine del set ha indice 1 (il valore 0 è usato per segnalare che la cella corrispondente non possiede un'immagine di riferimento). A parte leciti esperimenti, il riempimento di una mappa è fatto prelevando i dati da un file, immagazzinato nell'applicazione midlet come risorsa e il file è prodotto usando un'applicazione desktop appositamente creata. Alcune celle della mappa possono contenere un tipo predefinito di animazione. L'animazione è ottenuta per sostituzione dell'immagine di riferimento. Le celle animabile sono contrassegnate da un valore intero negativo. In pratica l'animazione consente nel sostituire, attraverso il metodo:

public void setAnimatedTile(int animatedTileIndex, int staticTileIndex);

tutte le immagini associate ad una cella che abbia il valore, negativo, "animatedTileIndex", con l'immagine di riferimento, di indice "staticTileIndex".
Con un TiledLayer è possibile costruire immagini considerevolmente più ampie del display di un cellulare. Associando un TiledLayer ad un LayerManager, è possibile navigare all'interno di questa mappa. L'associazione è realizzata con il metodo:

public void addLayer(Layer l);

di un LayerManager (un TiledLayer è un tipo di Layer). La navigazione avviene controllando lo schermo di proiezione del LayerManager:

public void setViewWindow(int x, int y, int width, int height);

Per affidare il controllo nelle mani dell'utente, è sufficiente associare alla pressione dei tasti di movimento l'incremento o decremento dei valori che saranno passati, come primi argomenti, al metodo su citato.

 

Bibliografia
[1] Sun Microsystem - "JSR118 Specification", disponibile in allegato a al J2ME Wireless Toolkit 2.2 di Sun e alla pagina web http://jcp.org/aboutJava/communityprocess/final/jsr118/index.html
[2] Sanchez Crespo-Dalmau - "Core techniques and algorithms in game programming", New Riders, 2002

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it