Lo
strumento principale per la realizzazione di un videogame
contenuto nelle librerie MIDP 1.0 è l'oggetto
javax.microedition.lcdui.Canvas (in seguito Canvas).
Un Canvas incorpora l'intercettazione degli eventi da
tastiera attraverso i metodi keyPressed(int) e keyReleased(int)
ed un rendering passivo nel metodo paint(javax.microedition.lcdui.Graphics).
Intercettazione
degli eventi da tastiera
Il
parametro dei metodi keyPressed e keyReleased corrisponde
al codice del tasto premuto e la ricostruzione dell'evento
avviene con il "classico" confronto tra codice
e set di costanti statiche presenti nella stessa classe
Canvas.
Ai
fini delle programmazione di videogame esiste un interessante
"astrazione" rappresentata dalle costanti
DOWN, UP, LEFT, RIGHT, FIRE, GAME_A, GAME_B, GAME_C,
GAME_D (in seguito game-code) e dai metodi getGameAction(int
keycode) e getKeyCode(int gameaction). Per sommi capi,
il funzionamento è il seguente: la piattaforma
di esecuzione è libera di definire un'associazione
tra tasti e funzioni "in game" in modo tale
che ad un valore game-code corrispondano uno o più
pulsanti del dispositivo. Le specifiche dell'implementazione
del metodo getGameAction(int) garantiscono che il tasto
associato non muti durante l'esecuzione del gioco-applicazione.
L'assenza di un tasto associato ad un codice game-action
è verificabile attraverso il metodo getKeyCode(int).
boolean
game_left, game_right, game_up, game_down, game_fire;
public
void keyPressed(int keyCode) {
int gameAction = getGameAction(keyCode);
if(gameAction == Canvas.UP) {
game_up = true;
}
else if(gameAction == Canvas.DOWN) {
game_down = true;
}
else if(gameAction == Canvas.LEFT) { }
else if(gameAction == Canvas.RIGHT) {
game_right = true;
}
else if(gameAction == Canvas.FIRE) {
game_fire = true;
}
}
Si
noti che il codice precedente si affida all'esistenza
di almeno un codice-tasto per ogni azione di gioco.
La
fase di rendering del motore di gioco è realizzata
attraverso il metodo paint(Graphics). Come è
tipico di Java, le librerie agiscono indipendentemente
dall'hardware ma permettono di recuperare alcune informazioni
in base a cui scegliere che tipo di funzioni usare nell'applicazione.
In particolare, attraverso il metodo Canvas.isDoubleBuffered()
è possibile stabilire se l'ambiente di esecuzione
disponga o meno di un buffer multiplo per effettuare
le operazioni di visualizzazione. Com'è noto,
il double-buffering consiste nella creazione di un buffer
in memoria sul quale vengono effettuate le operazioni
di disegno terminate le quali l'intero buffer viene
copiato sul contesto grafico corrente.
public
class GameCore extends Canvas {
private boolean doubleBuffered;
private Image backBuffer;
public GameCore() {
doubleBuffered = isDoubleBuffered();
if(doubleBuffered == false)
{
backBuffer = Image.createImage(getWidth(),
getHeight());
}
}
public void paint(Graphics screen) {
Graphics g = doubleBuffered
? Screen : backBuffer.getGraphics();
//operazioni di disegno
if(doubleBuffered == false)
screen.drawImage(backBuffer, 0, 0, Graphics.TOP | Graphics.LEFT);
}
}
Nel
caso in cui sia disponibile la "doppia bufferizzazione"
l'oggetto Graphics in argomento è per definizione
il back-buffer il che rende per lo più superfluo
l'uso di un terzo buffer come intermediario del rendering.
Sincronizzazione
motore-rendering
Il
motore di gioco di un videogame è costituito
da un ciclo all'interno del quale vengono eseguiti una
serie di aggiornamenti dello stato del sistema. L'ultima
fase del ciclo è costituita dalla presentazione
all'utente dello stato del sistema attraverso l'invio
all'hardware di una serie di istruzioni per il rendering.
Abbiamo visto come il rendering venga effettuato nel
metodo paint(Graphics) dell'oggetto Canvas, ciò
che manca è il collegamento tra il ciclo di gioco
e le operazioni di disegno. La questione principale
è relativa alla sincronizzazione tra i cicli
del motore di gioco e l'aggiornamento grafico: in generale
la vostra applicazione potrebbe non essere l'unica ad
effettuare una richiesta di aggiornamento del contesto
grafico, e anche se lo fosse dovrebbe tener conto della
possibilità che due chiamate successive per l'aggiornamento
si sovrappongano.
La
classe javax.microedition.lcdui.Display viene incontro
alle esigenze di sincronia attraverso il metodo callSerially(Runnable)
il cui effetto è quello di accodare all'ordine
di esecuzione degli eventi AWT le istruzioni contenute
nel metodo run() dell'oggetto che implementi l'interfaccia
java.lang.Runnable passato in argomento. E' facile intuire
come il comportamento del metodo callSerially(Runnable)
possa essere usato per generare un ciclo di istruzioni
sincronizzate con l'aggiornamento grafico:
import
javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public
class GameCore2 extends Canvas implements Runnable {
private Display display;
private Image backBuffer;
private boolean doubleBuffered;
private int screenWidth;
private int screenHeight;
public static final int ANCHOR = Graphics.TOP
| Graphics.LEFT;
public boolean gameLoop = false;
/**
* Il costruttore richiede come parametro
l'oggetto Display
* associato alla MIDlet
*/
public GameCore2(Display display) {
this.display = display;
screenWidth = getWidth();
screenHeight = getHeight();
doubleBuffered = isDoubleBuffered();
if(doubleBuffered
== false) backBuffer = Image.createImage(screenWidth,
screenHeight);
}
/**
* Attiva il campo di controllo
per il ciclo principale ed
* effettua la prima chiamata
al metodo run()
*/
public void start() {
gameLoop = true;
run();
}
/**
* Termina le chiamate successive
nel ciclo del motore di gioco
*/
public void stop() {
gameLoop = false;
}
/**
* Ciclo del motore di gioco
*/
public void run() {
//operazioni del
motore di gioco
//aggiornamento
grafico
repaint();
if(gameLoop) display.callSerially(this);
}
/**
* Rendering
*/
public void paint(Graphics screen)
{
Graphics g = doubleBuffered
? screen : backBuffer.getGraphics();
//pulizia del buffer
g.setColor(0x000000);
g.fillRect(0, 0,
screenWidth, screenHeight);
//operazioni di
rendering
//Copia del back-buffer
se necessario
if(doubleBuffered
== false) screen.drawImage(backBuffer, 0, 0, ANCHOR);
}
/**
* Termina il ciclo alla pressione
di un tasto
*/
public void keyPressed(int keyCode)
{
stop();
}
Il
metodo run è paragonabile ad un metodo ricorsivo
ma la successione delle chiamate è regolata dalla
coda AWT in modo tale da consumare l'esecuzione precedente
prima che sia creata la successiva (cosa che impedisce
di fatto problemi di sovraccaricamento dello stack).
Operazioni
periodiche
Nel
codice su riportato il motore di gioco esegue il ciclo
principale alla massima velocità consentita dalla
sincronizzazione con il rendering. Per le operazioni
periodiche occorre inserire una serie di istruzioni
per il calcolo del tempo trascorso tra diversi passaggi
nel ciclo, il che si ottiene facilmente attraverso l'uso
del metodo java.lang.System.currentTimeMillis():
private
long time0 = 0L;
private long time1 = 0L;
private long DTIME_1 = 50L; //periodo in millisecondi
/**
* Attiva il campo di controllo per il ciclo
principale ed
* effettua la prima chiamata al
metodo run()
*/
public void start() {
gameLoop = true;
time0
= System.currentTimeMillis();
run();
}
/**
* Ciclo del motore di gioco
*/
public void run() {
//tempo in ms al momento del
passaggio nel ciclo
time1 = System.currentTimeMillis();
//operazioni di aggiornamento
della logica di gioco ecc...
if(time1 - time0 >= DTIME_1)
{
//operazioni con
periodo DTIME_1;
time0 = time1;
}
//aggiornamento grafico
repaint();
if(gameLoop) display.callSerially(this);
}
Potento
contare su una granularità fine del timer di
sistema, è anche possibile usare direttamente
un differenziale tra passaggi consecutivi:
private
long time0 = 0L;
private long time1 = 0L;
private long dTime = 0L;
private
long elapsedTime = 0L;
private int DTIME_1 = 50; //periodo in millisecondi
/**
* Attiva il campo di controllo per il ciclo
principale ed
* effettua la prima chiamata al metodo run()
*/
public void start() {
gameLoop = true;
time0 = System.currentTimeMillis();
run();
}
/**
* Ciclo del motore di gioco
*/
public void run() {
//calcola il tempo trascorso
tra due passaggi successivi nel ciclo.
time1 = System.currentTimeMillis();
dTime = time1 - time0;
time0 = time1;
elapsedTime += time0;
// operazioni di aggiornamento
della logica di gioco ecc...
if(elapsedTime >= DTIME_1)
{
int maxLoops = 1
+ (int) elapsedTime / DTIME_1;
// esecuzione con recupero
for(int i = 0; i < maxLoops;
i++) {
elapsedTime -= DTIME_1;
//operazioni
con periodo DTIME_1;
}
}
//aggiornamento grafico
repaint();
if(gameLoop) display.callSerially(this);
}
Animazione
L'implementazione
di un'animazione, un'immagine che cambia contenuto al
variare del tempo, è realizzabile in molti modi.
Di seguito se ne propone uno generico dotato di una
certa flessibilità. Gli strumenti per il caricamento
e la gestione delle immagini sono contenuti nella classe
javax.microedition.lcdui.Image (in seguito Image); le
immagini possono essere "mutable o immutable"
a seconda che si possa o meno interagire con i dati
che la rappresentano. Le immagini caricate da un file
sono "immutable", per trattarne il contenuto
è necessario copiarle su un'immagine "mutable"
attraverso l'oggetto Graphics associato al buffer di
destinazione. Abbiamo già visto un esempio di
immagine "mutable" (il back-buffer usato per
la doppia bufferizzazione). Per caricare un'immagine
è sufficiente usare il metodo statico "createImage(String)"
della classe Image:
Image
immagine;
try {
immagine = Image.createImage("/frame.png");
} catch (java.io.IOException e) {
//gestione eccezione di IO
}
Nell'esempio
su riportato è creata un'immagine i cui dati
sono contenuti nel file "frame.png" all'interno
dell'archivio jar dell'applicazione. Per la nostra animazione
creremo prima di tutto un oggetto "AniFrame"
che rappresenti un'immagine dotata di un tempo di persistenza,
quindi passeremo a definire l'animazione come una sequenza
di oggetti AniFrame dotata di un metodo che consenta
di cambiare il frame visualizzato tenendo conto dell'ordine
e del periodo di persistenza di ciascun frame. Il modello
degli oggetti AniFrame è piuttosto breve.
import
javax.microedition.lcdui.Image;
/**
*Un oggetto AniFrame è un semplice contenitore
per un Image e un int.
*/
public class AniFrame {
public int tick;
public Image image;
public AniFrame(Image image, int tick) {
this.image = image;
this.tick = tick;
}
}
Ed
ecco uno dei modelli possibili per gli oggetti Animation.
import
javax.microedition.lcdui.Image;
public
class Animation {
private long animationTime = 0L;
private AniFrame[] frames;
private AniFrame currentFrame;
private int currentIndex;
public Animation(AniFrame[] frames) {
this.frames = frames;
reset();
}
/**
* E' possibile che l'applicazione che userà
gli oggetti
* Animation debba azzerare gli stati dell'oggetto.
*/
public void reset() {
currentIndex = 0;
currentFrame = frames[0];
animationTime = 0L;
}
/**
* Il parametro del metodo rappresenta un
intervallo di tempo.
*/
public void animate(long dTime) {
// aggiorniamo il tempo trascorso
dall'ultima animazione.
animationTime += dTime;
// se il tempo trascorso dall'ultimo
aggiornamento dell'immagine
// è maggiore del periodo
di persistenza
//del frame corrente...
if(animationTime >= currentFrame.tick)
{
animationTime -=
currentFrame.tick;
currentIndex++;
// controlliamo se il frame
precedente fosse l'ultimo della lista:
// in questo caso il frame corrente
// torna ad essere il primo
della lista (in breve, l'animazione è ciclica).
if(currentIndex >= frames.length)
currentIndex = 0;
//re-indirizziamo
il riferimento "currentFrame"
currentFrame = frames[currentIndex];
}
}
/**
* Il metodo consente l'accesso "diretto"
all'immagine del frame corrente.
*/
public Image getImage() {
return currentFrame.image;
}
}
La
classe Animation ha tre metodi pubblici, reset(), animate(long)
e getImage(). Il suo uso ruota per lo più attorno
agli ultimi due: nel ciclo del motore di gioco una fase
potrebbe essere dedicata ad eseguire l'aggiornamento
delle animazioni invocando per ciascuna di esse il metodo
animate(long) passando come parametro il valore della
differenza di tempo tra due passaggi successivi nel
ciclo. Nella fase di rendering si copierà poi
l'immagine ottenuta con getImage() sul buffer.
Riferimenti
Daniel
Sánchez - Crespo Dalmau, "Core Techniques
and Algorithms in Game Programming", New Riders
Publishing, Indianapolis, 2004.
David
Brackeen, "Developing Games in Java", New
Riders Publishing, Indianapolis, 2004
Michael
Kroll - Stefan Haustein, "Java 2 Micro Edition
Application Developement", Sams, Indianapolis,
2002.
|