GameCanvas
Un oggetto GameCanvas dispone di un back-buffer
dedicato. Per ogni GameCanvas esiste uno di questi
buffer, a cui si accede attraverso il metodo:
Graphics
getGraphics();
Trattandosi
del contesto di un back-buffer, le operazioni
condotte su questo oggetto Graphics non sono riprodotte
sullo schermo del dispositivo finché non
sia richiesto il rendering del buffer fuori schermo.
L'operazione è realizzata attraverso il
metodo:
flushGraphics();
Accanto
al back-buffer, un GameCanvas dispone di un sistema
particolare per l'intercettazione dell'input utente.
Attraverso il metodo:
int
getKeyStates();
è
possibile catturare in una sola istruzione lo
stato complessivo dell'input (tipicamente quali
tasti fossero premuti in quell'istante). È
una funzione classica del gaming, che piacerebbe
vedere implementata anche nel J2SE. L'intero ottenuto
dall'invocazione del metodo conserva le informazioni
sullo stato dei soli tasti di gioco. Per controllare
quali tasti fossero premuti al momento dell'invocazione
del metodo, si usa il meccanismo delle bandiere:
int
keys = getKeyStates();
if((keys
& COSTANTE_TASTO) != 0) {
TASTO era premuto
}
Qui
COSTANTE_TASTO può assumere uno dei cinque
valori necessariamente supportati:
DOWN_PRESSED
FIRE_PRESSED
LEFT_PRESSED
RIGHT_PRESSED
UP_PRESSED
e
altri quattro valori eventualmente supportati:
GAME_A_PRESSED
GAME_B_PRESSED
GAME_C_PRESSED
GAME_D_PRESSED
La
possibilità di accedere atomicamente allo
stato dei tasti è una caratteristica che
garantisce un'elevata consistenza al motore di
gioco, essendo assai improbabile che lo stato
di un tasto muti durante la lettura dello stato
complessivo dell'input.
Per
migliorare l'efficienza, un GameCanvas può
invitare la piattaforma a sopprimere la notifica
degli eventi di input relativi allo stato dei
tasti di gioco. La soppressione è realizzata
attraverso un parametro del costruttore:
public
class GamePit extends GameCanvas {
public GamePit() {
super(true); //soppressione della notifica
L'effetto
è valido solo per i tasti di gioco per
i quali, abbiamo visto, un GameCanvas fornisce
una strada alternativa di intercettazione, attraverso
getKeyStates. L'intercettazione dello stato dei
tasti che non corrispondano ad un controllo di
gioco, è sempre realizzabile attraverso
i metodi
keyPressed(int)
keyReleased(int)
keyRepeated(int)
del
super-tipo Canvas. Possiamo definire gli estremi
di un GameCanvas, predisposto per eseguire il
ciclo di un motore di gioco, nel codice che segue:
import
javax.microedition.lcdui.game.*;
import javax.microedition.lcdui.*;
public
class GamePit extends GameCanvas implements Runnable
{
private Thread runner;
private boolean engineLoop;
private long time0, time1;
int dTime;
public GamePit() {
super(true);
}
public void start() {
if(runner == null) {
engineLoop = true;
time0 = System.currentTimeMillis();
runner = new Thread(this);
runner.start();
}
}
public void stop() {
engineLoop = false;
}
public void run() {
Graphics g = getGraphics();
while(engineLoop) {
time1 = System.currentTimeMillis();
dTime = (int)(time1 - time0);
time0 = time1;
int keys = getKeyStates();
//gestione input, player, collisioni, entità
eccetera
//rendering su "g"
flushGraphics(); //flip buffer
}
runner = null;
}
}
Layer
Il
tipo Layer è la rappresentazione di un
elemento riproducibile su un oggetto Graphics,
dotato di una posizione e di forma rettangolare.
In particolare, Layer definisce i metodi necessari
ad impostare ed ottenere la posizione di un elemento
e a produrre uno spostamento. La concreta definizione
dell'aspetto di un Layer è lasciata ai
suoi sotto-tipi, che devono implementare il metodo
paint(Graphics);
I
Layer possiedono uno stato di visibilità
accessibile attraverso i metodi:
setVisible(boolean);
boolean isVisible();
La
reazione del rendering allo stato di visibilità
è gestita automaticamente nel caso in cui
ci si avvalga della classe LayerManager, altrimenti
deve (può) essere controllata dal programmatore
affinché ne deduca le operazioni opportune
(tipicamente un Layer il cui stato sia "invisibile"
non sarà riprodotto sull'oggetto Graphics).
TiledLayer
TiledLayer
è una sottoclasse di Layer che concretizza
la versione MIDP del concetto di tile-map. Una
Tile-Map è uno strumento usato per definire
immagini molto estese, come la composizione di
immagini più piccole (i Tile). Il sistema
di basa sull'esistenza di un elenco indicizzato
di immagini e sull'uso di una griglia (matrice)
di indici. Nella griglia, ogni cella contiene
un intero e questo intero stabilisce quale immagine
dell'elenco debba essere usata per disegnare quella
particolare cella. Il vantaggio in termini di
uso delle risorse di sistema può essere
molto elevato: una sola immagine di 24x24 pixel
potrebbe essere usata, ad esempio, per disegnare
una superficie virtuale di migliaia di pixel quadrati.
All'interno di una Tile-Map esiste un valore neutro,
generalmente 0 (zero), di fronte al quale la cella
è considerata vuota. In altri termini,
nessuna immagine è disegnata nella posizione
corrispondente a quella cella. La costruzione
di un TiledLayer richiede che sia predefinita
un'immagine, comunemente in formato png, che contenga
le singole immagini per le celle (tile). Le immagini
delle celle sono disposte consecutivamente, o
in una griglia. La forma prescelta per l'immagine
"globale" non ha importanza. Rileva,
invece, la posizione dei singoli Tile al suo interno.
Durante la costruzione infatti, il TiledLayer
assegna ad ogni Tile un indice. L'ordine di assegnazione
è da sinistra verso destra, dall'alto verso
il basso. Nel caso di una "striscia"
orizzontale, il primo tile, individuato nella
regione (0, 0, tileWidth, tileHeight) avrà
indice 1 (uno), il secondo, (tileWidth, tileHeight,
tileWidth, tileHeight) avrà indice 2, il
terzo (2 * tileWidth, 2 * tileHeight, tileWidth,
tileHeight) e così via, fino ad esaurimento
del contenuto.
La
classe TiledLayer mette a disposizione alcuni
metodi per interagire con il contenuto. Inizialmente,
la mappa rappresentata nell'oggetto è vuota:
tutte le celle hanno un valore 0 (zero). I metodi:
setCell(int
col, int row, int tileIndex);
filleCells(int col, int row, int numCols, int
numRows, int tileIndex);
consentono
di modificare il contenuto della griglia. Dei
due è il primo ad avere maggiore importanza.
Il secondo permette di modificare una regione
rettangolare della matrice di indici. Si tratta
di un sistema probabilmente utile per la costruzione
di mappe definite direttamente nel codice. Realisticamente
è improponibile la definizione manuale,
in codice, di una Tile-Map, a meno che non sia
di una semplicità estrema. La matrice di
indici è invece definita usando uno strumento
esterno al J2ME, che si occupi di creare la mappa,
possibilmente in modo visuale, e la "strip"
delle immagini per le celle. Una volta salvata
la matrice, in un formato che sia efficiente per
un'applicazione J2ME (vale a dire, che tenda ad
occupare il minore spazio possibile, in termini
di byte), essa sarò inclusa come risorsa
nel pacchetto dell'applicazione, e caricata dinamicamente
in memoria. A questo punto sarà il metodo
setCell(int
col, int row);
ad
occuparsi di costruire, in un ciclo, la matrice
del TiledLayer, basandosi sulle informazioni lette
dalla risorsa.
LayerManager
LayerManager
è probabilmente la classe più interessante
di tutto il pacchetto. In essa troviamo una realizzazione
intuitiva e di grande efficacia di un meccanismo
di rendering per motori basati su Tile-Map. In
superficie, un LayerManager è un contenitore
di oggetti Layer. Un Layer può essere inserito
usando i metodi:
append(Layer
l);
insert(Layer l, int index);
Il
primo aggiunge il Layer in coda alla lista dei
layer, il secondo lo inserisce in una posizione
specifica. Un LayerManager possiede un metodo
"paint" che riproduce su un contesto
grafico il suo contenuto. L'ordine di riproduzione
è conforme all'algoritmo del pittore, cioè
dall'ultimo inserito al primo. Il LayerManager
si occupa di scegliere, in base a posizione, dimensioni
ed occultamento, quali Layer siano dei candidati
validi alla riproduzione e quali possano essere
scartati, con beneficio delle prestazioni. Oltre
alla gestione di una lista di Layer, un LayerManager
possiede uno schermo visivo "virtuale",
in grado di muoversi all'interno di un sotto-sistema
di coordinate. Lo scorrimento è realizzato
attraverso delle traslazioni del contesto grafico:
graphics.translate(int,
int);
Dal
punto di vista dell'utente della libreria, il
sistema è trasparente e di notevole semplicità
d'uso. Un LayerManager definisce una piattaforma
visiva "virtuale", controllabile attraverso
il metodo:
setViewWindow(int
x, int y, int width, int height);
L'effetto
che si realizza manipolando questa "view-window"
è quello di uno schermo che scorre all'interno
di un'ambiente virtuale. Generalemnte width e
height sono valori costanti, pari alle dimensioni
dello schermo di gioco. I valori x e y rappresentano
invece la posizione di scorrimento all'interno
della mappa. Attraverso la piattaforma visiva,
e con l'uso di un TiledLayer, un LayerManager
è in grado di muovere l'ambiente di gioco
entro un mondo considerevolmente più vasto
delle dimensioni dello schermo. La tecnica è
quella degli "scroller", termine con
cui di definiscono motori di rendering in grado
di scorrere, appunto, per una Tile-Map, in due
o quattro direzioni. A questo proposito di parla
di scroller a due o quattro vie. Per esemplificare,
il noto gioco "Zelda" è realizzato
con uno scroller a quattro vie, mentre "Super
Mario" era (inizialmente) uno scroller a
due vie.
Per
riprodurre il contenuto di un LayerManager usiamo
il metodo
paint(Graphics,
int x, int y);
In
questo metodo, i due valori interi rappresentano
le coordinate, relative al sistema di coordinate
del dispositivo, a partire dalle quali sarà
riprodotto il contenuto attualmente proiettato
sulla piattaforma visiva. La possibilità
di definire l'origine della riproduzione è
di particolare utilità per la produzione
di giochi tendenzialmente multi-piattaforma. Nel
caso in cui lo schermo del dispositivo abbia dimensioni
maggiori della piattaforma visiva, prescelta dal
programmatore per il gioco, è sufficiente
definire come origine del rendering un punto di
coordinate:
x
= canvasWidth / 2 - viewWindowWidth / 2;
y = canvasHeight / 2 - viewWindowHeight / 2;
per
ottenere lo schermo di gioco centrato nello schermo
fisico del dispositivo.
Conclusioni
Abbiamo fornito una rapida panoramica di alcune
delle nuove caratteristiche della piattaforma
J2ME, dal punto di vista della realizzazione di
giochi. Nulla può sostituire l'esperienza
diretta e, pertanto, l'invito conclusivo non può
che essere quello di sperimentare con mano le
classi presentate. In ogni caso, si tratta solo
di strumenti, senz'altro utili, ma non definitivi.
Occorre anche ricordare che l'insieme di questi
strumenti non costituisce un framework per lo
sviluppo di giochi: in altre parole, esistono
i singoli elementi dell'animazione e dell'interazione
ma non è presente un modello complessivo
per l'accoppiamento delle molte parti in un ambiente
unico. E questo, fortunatamente, significa che
c'è ancora la necessità di un programmatore.
Bibliografia
[1] JSR-000118 Mobile Information Device Profile
Specification 2.0 Final Release, http://jcp.org/aboutJava/communityprocess/final/jsr118/index.html
|