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
|