Tile
Map Rendering
Il
vantaggio consiste nella possibilità di definire
una certa varietà nell'ambientazione a partire
da un numero relativamente limitato di settori, caricati
in memoria durante una fase di inizializzazione e "manipolati"
dal motore grafico attraverso l'uso di puntatori. Ogni
ambientazione costruita usando le tile-map può
essere facilmente rappresentata usando una matrice bidimensionale,
al cui interno siano inseriti degli indici di riferimento
ad un array contenente le immagini "statiche"
caricate in memoria. Esistono numerosi tipi di motori
grafici bidimensionali, basati su tile-map, ciascun
dei quali riflette le diverse forme delle mappe su cui
si basano. La forma più semplice è rappresentata
dai motori a schermata singola, in cui la tile-map definisce
le informazioni per un livello che non si estende oltre
le dimensioni dello schermo. Supponendo di disporre
di un'area di visualizzazione di 120x120 pixel e di
usare celle da 24 pixel, la tile-map conterrà
25 elementi in una matrice di 5 righe per 5 colonne.
...
Image[] tileStore = new Image[3];
int[][]
tileMap = {
{0, 0, 0, 0, 0},
{1, 0, 0, 0, 0},
{1, 0, 0, 0, 0},
{1, 0, 0, 0, 0},
{2, 2, 2, 2, 2}
};
...
Per
poter usare una tile-map, il ciclo di rendering deve
essere istruito circa le dimensioni delle singole celle
e dello schermo. Di norma il numero di celle visualizzate
sullo schermo dovrebbe essere arrotondato per eccesso,
in modo da garantire una corretta visualizzazione su
schermi che non dispongano di un'area trattabile che
sia un multiplo della dimensione delle sezioni. Usando
un'implementazione del framework definito nell'articolo
precedente è possibile ottenere rapidamente un
esempio di motore basato su mappe a schermo singolo.
Le classi necessarie all'esecuzione del codice seguente
sono GameApp.java e GameCore.java, entrambe presentate
negli articoli precedenti.
Figura 1
Figura 2
Figura 3
import
javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import java.io.*;
public
class GameCoreImpl_2 extends GameCore {
//punto di ancoraggio per le immagini
final int ANCHOR = Graphics.TOP | Graphics.LEFT;
final int TILE_WIDTH = 24; //larghezza in
pixel delle celle
final int TILE_HEIGHT = 24; //altezza in
pixel delle celle
int screenWidth; //larghezza dello schermo
int screenHeight; //altezza dello schermo
int xTiles; //numero di celle visualizzabili
sull'asse X
int yTiles; //numero di celle visualizzabili
sull'asse Y
Image[] tileStore = new Image[3];
//una mappa di 6x6 celle
int[][] tileMap = {
{0, 0, 0, 0, 0, 0},
{1, 0, 0, 0, 0, 0},
{1, 0, 0, 0, 0, 0},
{1, 0, 0, 0, 0, 0},
{2, 2, 2, 2, 2, 0},
{2, 2, 2, 2, 2, 0}
};
public GameCoreImpl_2(Display d) {
super(d);
//carica le immagini
try {
tileStore[0] = Image.createImage("/tile0.png");
tileStore[1] = Image.createImage("/tile1.png");
tileStore[2] = Image.createImage("/tile2.png");
}
catch(IOException
e) {
//gestione eccezione...
}
screenWidth = getWidth();
screenHeight = getHeight();
xTiles = screenWidth / TILE_WIDTH;
yTiles = screenHeight / TILE_HEIGHT;
}
public void dTimeEngineOperations(long dTime)
{
}
//operazioni di rendering del motore di
gioco
public void renderingOperations(Graphics
g) {
for(int y = 0; y <= yTiles;
y++) {
for(int x = 0; x
<= xTiles; x++) {
//calcola
le coordinate x e y per disegnare la
//cella
corrente
int
screenX = x * TILE_WIDTH;
int
screenY = y * TILE_HEIGHT;
//determina
l'indice dell'immagine
//estraendolo
dalla tile-map
int
tileIndex = tileMap[y][x];
//disegna
una singola cella
g.drawImage(tileStore[tileIndex],screenX,screenY,ANCHOR);
}
}
}
}
Per
poter visualizzare il risultato è sufficiente
modificare la seconda linea del costruttore della classe
GameApp come segue:
...
//gameCore
= new GameCoreImpl(display);
gameCore = new GameCoreImpl_2(display);
...
Figura 4 - una tile-map in azione
Un
passo avanti rispetto alle mappe a schermo singolo è
rappresentato dalle mappe a scorrimento (orizzontale,
verticale o lungo entrambi gli assi). Dal punto di vista
della matrice degli indici, la trasformazione è
molto semplice, si tratta solamente di definire una
mappa più grande.
Image[]
tileStore = new Image[3];
int[][] tileMap = {
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{1, 2, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1},
{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}
};
Cambia
la logica del ciclo di visualizzazione: la mappa a scorrimento
permette al giocatore di muoversi "oltre"
lo schermo singolo. L'effetto si ottiene collegando
il ciclo di rendering della mappa, ed in particolare
l'indice delle celle da visualizzare, alla posizione
del giocatore sullo schermo. Per definire l'insieme
delle celle che devono essere disegnate è possibile
sfruttare le proprietà della divisione intera,
resituisce un valore intero arrotondato verso il basso.
Data la posizione del giocatore playerX, playerY, in
pixel, relativa all'area definita dalla mappa, la cella
corrispondente a quella posizione si ottiene dividendo
playerX ed playerY per la larghezza e l'altezza di ciascuna
cella. Ottenuta tale posizione all'interno della griglia
è possibile determinare quali siano le celle
da disegnare, effettuando un controllo preventivo per
evitare l'eventualità che lo scrolling proceda
oltre il settore effettivamente definito dalla mappa.
La costruzione "materiale" dell'immagine che
rappresenta la mappa può essere realizzata in
un doppio ciclo for piuttosto semplice:
for(int
y = 0; y < yTiles; y++) {
for(int x = minX; x < maxX; x++) {
g.drawImage(tileStore[tileMap[y][x]],x*TILE_WIDTH
- offsetX,y*TILE_HEIGHT,
ANCHOR);
}
}
In
questo casi si tratta di uno scorrimento esclusivamente
orizzontale, facilmente estensibile per aggiungere una
seconda dimensione.
yTiles
rappresenta il numero massimo di celle visualizzabili
sullo schermo, valore ottenuto dividendo la larghezza
di quest'ultimo per la dimensione verticale di una cella.
Il valore di "offsetX" verrà trattato
poche righe più sotto. Per il "range"
orizzontale occorre (xMin, xMax) occorre, come accennavamo,
ottenere la posizione del giocatore nella griglia, a
partire dalla posizione sullo schermo virtuale (playerX).
int
tilePlayerX = playerX / TILE_WIDTH;
//controlla che non sia stato raggiunto il limite della
//mappa, per il rendering delle celle
if(tilePlayerX + xTiles < tileMap[0].length) {
minX = tilePlayerX;
offsetX = playerX;
}
maxX
= minX + xTiles + 1; //una colonna in più per
evitare
//che il margine destro appaia
//in costruzione
dove
xTiles rappresenta l'analogo di yTiles, lungo l'asse
X. Il condizionale si occupa di verificare che la porzione
di mappa selezionata rientri all'interno della matrice
originale."offsetX" corrisponde alla posizione
del giocatore nella tile-map, considerata come un rettangolo
di dimensioni X (0 -> TILE_WIDTH * tileMap[0].length)
e Y (0 -> TILE_HEIGHT * tileMap.length) ed è
usato per ottenere la proiezione delle coordinate dal
sistema definito nella tile-map allo schermo del dispositivo.
Si è scelto di separare i valori di offsetX e
playerX all'interno del ciclo di rendering, pur essendo
possibile usare il solo playerX, al fine di separare
il compito del ciclo di rendering da quello di calcolo
delle restrizioni, che sarà esaminato in un articolo
successivo.
Per
verificare l'effettiva funzionalità dei componente
esaminati è possibile creare una terza estensione
di GameCore, all'interno della quale "simulare",
per brevità, l'interazione tra la posizione del
giocatore e la mappa:
import
javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import java.io.*;
public
class GameCoreImpl_3 extends GameCore {
final int ANCHOR = Graphics.TOP | Graphics.LEFT;
final int TILE_WIDTH = 24;
final int TILE_HEIGHT = 24;
int screenWidth;
int screenHeight;
int xTiles;
int yTiles;
Image[] tileStore = new Image[3];
int[][] tileMap = {
{1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1},
{1, 2, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1},
{1, 2, 0, 0, 0, 1, 0, 1, 0,
1, 1, 1},
{2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2},
{2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2},
{2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2}
};
//larghezza ed altezza della mappa.
int mapWidth = tileMap[0].length * TILE_WIDTH;
int mapHeight = tileMap.length * TILE_HEIGHT;
//una simulazione dello sprite player
boolean playerLeft = false;
boolean playerRight = false;
int playerX = 0;
int playerY = 0;
long playerUpdateTime = 10L;
long timeLine = 0L;
public GameCoreImpl_3(Display d) {
super(d);
//carica le immagini
try {
tileStore[0] = Image.createImage("/tile0.png");
tileStore[1] = Image.createImage("/tile1.png");
tileStore[2] = Image.createImage("/tile2.png");
}
catch(IOException e) {
//gestione eccezione...
}
screenWidth = getWidth();
screenHeight = getHeight();
xTiles = (screenWidth / TILE_WIDTH)
+ 1;
yTiles = (screenHeight / TILE_HEIGHT)
+ 1;
}
long timetime = 0L;
public void dTimeEngineOperations(long dTime)
{
timetime = dTime;
timeLine += dTime;
if(timeLine >= playerUpdateTime)
{
timeLine -= playerUpdateTime;
if(playerLeft ==
true) {
playerX
= (playerX - 1) > 0 ? (playerX - 1) : playerX;
}
else if(playerRight ==
true) {
playerX++;
}
}
}
int minX = 0;
int maxX = 0;
int offsetX = 0;
public
void renderingOperations(Graphics g) {
int tilePlayerX = playerX /
TILE_WIDTH;
// controlla che non sia
stato raggiunto il limite della
// mappa, per il rendering
delle celle
if(tilePlayerX + xTiles <
tileMap[0].length) {
minX = tilePlayerX;
offsetX = playerX;
}
maxX = minX + xTiles + 1; //una colonna
in più per evitare
//che il margine destro appaia
//in costruzione
for(int y = 0; y < yTiles; y++)
{
for(int x = minX; x <
maxX; x++) {
g.drawImage(tileStore[tileMap[y][x]],x*TILE_WIDTH
- offsetX,y*TILE_HEIGHT,
ANCHOR);
}
}
}
//gestione della pressione dei tasti ridotta
al minimo
public void keyPressed(int keyCode) {
if(keyCode == Canvas.KEY_NUM6)
{
playerRight = true;
}
else if(keyCode == Canvas.KEY_NUM4)
{
playerLeft = true;
}
}
public void keyReleased(int keyCode)
{
if(keyCode == Canvas.KEY_NUM6)
{
playerRight
= false;
}
else if(keyCode == Canvas.KEY_NUM4)
{
playerLeft
= false;
}
}
}
|