MokaByte 83 - Marzo 2004 
Titolo
III parte J2ME: Tile Map Rendering
di
Pierluigi
Grassi
Le tecniche di tile map rendering consistono nella costruzione "dinamica" dell'immagine visualizzata dal dispositivo, ottenuta dall'inserimento di una serie di immagini più piccole, generalmente di dimensioni uniforme, in una griglia virtuale

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;
     }
   }
}

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