Sprite
Gli
sprite sono immagini in movimento, di solito animate.
Il movimento richiede, in termini essenziali, una posizione
ed un vettore che indichi velocità e direzione
dell'oggetto. La posizione dello sprite sullo schermo
non richiede particolari introduzioni, si tratta di una
comune coppia di valori interi che rappresentano la posizione
sull'asse x e y. Qualche esercizio di fantasia potrebbe
essere richiesto, al contrario, per la rappresentazione
della velocità. Al di là della questione
relativa alla scomposizione del vettore nei componenti
x e y, troviamo la particolarità data dal mancato
supporto dell'attuale piattaforma J2ME ai valori in virgola
mobile. Normalmente la velocità di un oggetto nel
codice di un videogame bidimensionale è rappresentata
da un valore float che indica i pixel percorsi in un millisecondo,
il che comporta la possibilità di determinare lo
spazio percorso dall'oggetto tra un ciclo ed il successivo
del motore di gioco moltiplicando direttamente la velocità
per il tempo trascorso tra i due passaggi. E' ovvio che
anche senza float lo spazio percorso resta comunque il
prodotto del tempo per la velocità, ma occorre
aggirare la mancanza dei decimali. Senza tanti giri di
parole, possiamo controllare con un certo grado di precisione
la velocità di un oggetto sullo schermo definendo
il tempo che deve trascorrere affinchè esso si
muova di un pixel.
Per l'animazione è sufficiente dotare lo sprite
di uno o più oggetti Animation, attribuendo ad
un metodo ad hoc la possibilità di cambiare l'animazione
corrente ed immettendo il controllo sull'aggiornamento
delle immagini all'interno di un singolo metodo update
che si occupi anche di ridefinire la posizione dello sprite.
Vediamo una possibile implementazione di quanto detto
costruendo una classe Sprite predefinita per il movimento
lineare nelle quattro direzioni.
//La
classe Sprite
import javax.microedition.lcdui.Image;
public
class Sprite {
Animation[] animations;
Animation currentAnimation;
int posX, posY;
int dx, dy;
long dTimeX, dTimeY;
long timeLineX, timeLineY;
public Sprite(Animation[] animations) {
this.animations = animations;
reset();
}
//azzera velocità ed indice dell'animazione
corrente
public void reset() {
setSpeedX(0, 0L);
setSpeedY(0, 0L);
currentAnimation = animations[0];
}
//imposta direzione e variazione di tempo
per la velocità lungo l'asse X
public void setSpeedX(int dx, long dTimeX)
{
this.dx = dx;
this.dTimeX = dTimeX;
timeLineX = 0L;
}
//imposta direzione e variazione di tempo
per la velocità lungo l'asse X
public void setSpeedY(int dy, long dTimeY)
{
this.dy = dy;
this.dTimeY = dTimeY;
timeLineY = 0L;
}
//imposta l'animazione corrente tra quelle
fornite all'interno dell'array.
public void setAnimation(int index) {
currentAnimation = animations[index];
}
//aggiorna lo Sprite (posizione e frame
dell'animazione)
public void update(long dTime) {
currentAnimation.animate(dTime);
timeLineX += dTime;
timeLineY += dTime;
//aggiorna la posizione lungo
l'asse X, tenendo conto
//della possibilità che
la variazione di tempo dTime sia tale
//da richiedere una serie di
spostamenti successivi
if(dx != 0 && dTimeX
> 0L) {
while(timeLineX
>= dTimeX) {
timeLineX
-= dTimeX;
posX
+= dx;
}
}
if(dy != 0 && dTimeY
> 0L) {
while(timeLineY
>= dTimeY) {
timeLineY
-= dTimeY;
posY
+= dy;
}
}
}
//restituisce la posizione dell'oggetto
lungo l'asse X
public int getX() {
return posX;
}
//restituisce la posizione dell'oggetto
lungo l'asse Y
public int getY() {
return posY;
}
//restituisce l'immagine ottenuta dall'animazione
corrente
public Image getImage() {
return currentAnimation.getImage();
}
}
Controllo sul movimento di uno sprite,
il giocatore
Da
uno Sprite all'alter ego del giocatore il passo è
breve; una delle differenze risiede nel controllo, affidato
all'interazione con l'utente anziché a routine
più o meno "intelligenti". Abbiamo
visto precedentemente come gestire la risposta alla
pressione di tasti sul dispositivo. A questo punto occorre
precisare che non tutti i dispositivi supportano la
pressione simultanea di più tasti e la ripetizione
dell'evento pressione. Per quanto riguarda il primo
caso si tratta di definire una serie di altri 4 tasti
oltre ai 4 che definiscono il controllo sul movimento,
affinchè il giocatore possa spostarsi in 8 direzioni:
dato il numero limitato di tasti a disposizione, in
questo caso occorre passare dall'intercettazione degli
eventi tradotta dal metodo getGameAction(int) alla gestione
diretta del tastierino numerico. La seconda evenienza
deve essere affrontata a contrario: se la funzione di
ripetizione è presente, occorre prestare attenzione
a come l'applicazione ragisce alla pressione ripetuta
del tasto. Il problema si aggira facilmente con la supposizione
che tutti i dispositivi siano dotati di ripetizione
inserendo un boolean per ciascun tasto, il cui valore
cambierà a seconda che il tasto sia stato premuto
o rilasciato. Controllando il valore del boolean insieme
al codice dell'evento otterremo una reazione limitata
al caso in cui il tasto non nell'ipotetico stato "premuto".
//Controllo
del giocatore
boolean up = false;
boolean down = false;
boolean left = false;
boolean right = false;
Sprite
sprite = ...
...
public
void keyPressed(int keyCode) {
//ricaviamo il codice azione corrispondente
al tasto premuto
int gameAction = getGameAction(keyCode);
if(gameAction == Canvas.RIGHT &&
!right) {
System.out.println("RIGHT_PRESSED");
sprite.setSpeedX(1, 10L);
right = true;
}
else if(gameAction == Canvas.LEFT &&
!left) {
sprite.setSpeedX(-1, 10L);
left = true;
}
else if(gameAction == Canvas.UP &&
!up) {
sprite.setSpeedY(-1, 10L);
up = true;
}
else if(gameAction == Canvas.DOWN &&
!down) {
sprite.setSpeedY(1, 10L);
down = true;
}
}
public
void keyReleased(int keyCode) {
//ricaviamo il codice azione corrispondente
al tasto premuto
int gameAction = getGameAction(keyCode);
if(gameAction == Canvas.RIGHT && right) {
System.out.println("RIGHT_RELEASED");
sprite.setSpeedX(0, 10L);
right = false;
}
else if(gameAction == Canvas.LEFT && left) {
sprite.setSpeedX(0, 10L);
left = false;
}
else if(gameAction == Canvas.UP && up) {
sprite.setSpeedY(0, 10L);
up = false;
}
else if(gameAction == Canvas.DOWN && down) {
sprite.setSpeedY(0, 10L);
down = false;
}
}
Un
framework minimo
Riprendiamo
quanto detto finora e cerchiamo di predisporre l'insieme
minimo di strumenti che possano servire per cominciare
a fare qualche esperimento in proprio. Quello che manca
è un collegamento tra il motore di gioco ed un
oggetto MIDlet, e soprattutto una generalizzazione del
motore di gioco. A tale ultimo scopo, è sufficiente
una semplice manipolazione del sorgente di GameCore
che renda più facile l'uso di sue sottoclassi.
import
javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
public
abstract class GameCore extends Canvas implements Runnable
{
private Display display;
private Image backBuffer;
private boolean doubleBuffered;
int screenWidth;
int screenHeight;
public
static final int ANCHOR = Graphics.TOP | Graphics.LEFT;
boolean gameLoop = false;
private long time0, time1, dTime;
/**
* Il costruttore richiede come parametro
l'oggetto Display
* associato alla MIDlet
*/
public GameCore(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;
time0 = System.currentTimeMillis();
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
time1 = System.currentTimeMillis();
dTime = time1 - time0;
time0 = time1;
dTimeEngineOperations(dTime);
//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);
renderingOperations(g);
//operazioni di rendering
//Copia del back-buffer se necessario
if(doubleBuffered == false)
screen.drawImage(backBuffer,
0, 0, ANCHOR);
}
//Le sottoclassi di GameCore definiranno
semplicemente
//i due metodi che seguono, il primo per
le operazioni da svolgersi
//all'interno del ciclo prima della fase
di rendering
public abstract void dTimeEngineOperations(long
dTime);
//...il secondo per le operazioni di disegno.
public abstract void renderingOperations(Graphics
g);
}
Dal
punto di vista del codice i cambiamenti sono minimi,
ma è notevole il vantaggio nella semplicità
d'uso, Essendo infatti sufficiente "riempire"
i metodi astratti in un'estensione della classe GameCore
per ottenere un ciclo di gioco che effettui operazioni
"personalizzate". E' possibile averne dimostrazione
usando le classi definite sinora in un'applicazione
grafica il cui scopo sia quello di muovere uno sprite
sullo schermo del cellulare. L'applicazione fa uso di
due immagini che definiscono l'animazione dello Sprite.
Figura 1
Figura 2
Le
immagini sono in formato png indicizzato. La maggioranza
dei dipositivi a colori supporta un bit di trasparenza
(da cui la necessità dell'indicizzazione dell'immagine
da esportare in formato png), alcuni arrivano ad 8 bit.
Le immagini sono state realizzate con l'ottimo "The
Gimp", v. 1.3. L'indicizzazione si ottiene attraverso
il menu "immagine" > "modalità"
> "indicizzata". Il cuore dell'applicazione
è rappresentato da un'estensione della classe
GameCore, chiamata GameCoreImpl. Il costruttore della
classe si preoccupa di creare un oggetto Sprite, controllato
dal gestore di eventi "incorporato" in ciascun
Canvas, aggiornato e disegnato sullo schermo secondo
lo schema definito dalla superclasse GameCore.
import
javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
public
class GameCoreImpl extends GameCore {
private Sprite sprite;
private boolean right = false;
private boolean left = false;
private boolean up = false;
private boolean down = false;
public GameCoreImpl(Display display) {
//richiamo all'inizializzazione
della superclasse.
super(display);
//Inizializzazione dell'oggetto
Sprite. Si suppongno irrealizzabili condizioni
//di errore dovute alla lettura
dei file immagine.
try {
AniFrame[] razzoFrames
= {new AniFrame(Image.createImage("/flare0.png"),
250),
new
AniFrame(Image.createImage("/flare1.png"),
250)};
Animation razzo
= new Animation(razzoFrames);
//sfruttiamo una
parte delle capacità dell'oggetto Sprite
//dotandolo di un'unica
animazione. Tipicamente uno sprite
//possiede più
di un'animazione.
sprite = new Sprite(new
Animation[] {razzo});
sprite.setSpeedX(0,
10L);
}
catch(java.io.IOException e)
{
System.out.println(e);
}
}
// Definizione delle operazioni da compiersi
all'interno del motore
// di
gioco con precedenza rispetto al rendering, eventualmente
// basate sul differenziale di tempo
// tra passaggi successivi nel ciclo.
public void dTimeEngineOperations(long dTime)
{
sprite.update(dTime);
}
//Definizione delle operazioni di rendering.
public void renderingOperations(Graphics
g) {
g.drawImage(sprite.getImage(),
sprite.getX(), sprite.getY(), ANCHOR);
}
//Gestione degli eventi tasto premuto
public void keyPressed(int keyCode) {
int gameAction = getGameAction(keyCode);
if(gameAction == Canvas.RIGHT
&& !right) {
sprite.setSpeedX(1,
50L);
right = true;
}
else if(gameAction == Canvas.LEFT
&& !left) {
sprite.setSpeedX(-1,
50L);
left = true;
}
else if(gameAction == Canvas.UP
&& !up) {
sprite.setSpeedY(-1,
50L);
up = true;
}
else if(gameAction == Canvas.DOWN
&& !down) {
sprite.setSpeedY(1,
50L);
down = true;
}
}
//Gestione degli eventi tasto rilasciato.
public void keyReleased(int keyCode) {
int gameAction = getGameAction(keyCode);
if(gameAction == Canvas.RIGHT
&& right) {
sprite.setSpeedX(0,
50L);
right = false;
}
else if(gameAction == Canvas.LEFT
&& left) {
sprite.setSpeedX(0,
50L);
left = false;
}
else if(gameAction == Canvas.UP
&& up) {
sprite.setSpeedY(0,
50L);
up = false;
}
else if(gameAction == Canvas.DOWN
&& down) {
sprite.setSpeedY(0,
50L);
down = false;
}
}
}
La
realizzazione dell'oggetto MIDlet il cui scopo sia visualizzare
le operazioni del motore di gioco può anche essere
minimale:
import
javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public
class GameApp extends MIDlet {
private Display display;
private GameCore gameCore;
public GameApp() {
display = Display.getDisplay(this);
gameCore = new GameCoreImpl(display);
}
public void startApp() {
display.setCurrent(gameCore);
gameCore.start();
}
public void pauseApp() {
gameCore.stop();
}
public void destroyApp(boolean unc) {
gameCore.stop();
notifyDestroyed();
}
}
Il
risulato, parzialmente riscontrabile in un'immagine
statica, è indicato nell'immagine.
|