MokaByte 94 - Marzo 2005
MIDP2.0, Evoluzione in-game
di
Pierluigi
Grassi
Il passaggio da MIDP1.0 a MIDP2.0 ha apportato notevoli mutamenti all'armamentario disponibile per la creazione di giochi. In questo articolo di occupiamo di una panoramica sintetica di alcune nuove caratteristiche introdotte. In particolare, osserviamo quattro Tipi di notevole interesse: GameCanvas, Layer, TiledLayer, LayerManager

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

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