Loading Tile Map Data From a File

In case you haven't seen it yet, there's a push-puzzle demo game in the "Various Games"/"Various Games for MIDP 2.0" sample mobile project included with The NetBeans Mobility Pack. The push-puzzle game demo also shows how to load map data from text files. But the one thing I remember most about that sample game is that the code confused me a lot, heh.

Here's my attempt to make things a bit easier for you so you won't have to go through all that code jumping. But instead of loading map data from a text file, we're going to pull the map data from a binary file.

In this tutorial, we're going to make use of the project code from the tutorial Using the TiledLayer Class to Display Tile Maps. So head over there first to get the code and to have a better understanding of what's going on. That tutorial displays a user controlled sprite on a map with obstacles. We're going to modify the code so that the obstacle layer data is loaded from a map file.

You can get the map file here in SMP format:
samplemap.smp (155 bytes) - Download Link

Create a new folder called "maps" in the "src" folder of the project and save the map file there.

You can view or edit the contents of the map with the map editor which you can get here: Simple Tile Map Editor Info. and Download Page.
The link includes instructions and some more info.

To preview the map in the map editor, Click on the "Open" icon and choose the "samplemap.smp" file you saved earlier. In the next dialog, select the "tileset1.png" image file inside the "images" folder within the "src" folder of the project. You should see something like this:

Click to Enlarge


When you're ready, open the project in NetBeans and navigate your way to the clsCanvas code. Add the following code below the loadBlockMap() method:


public TiledLayer getMap(String fpath, Image ftiles){
TiledLayer tMap = null;
try {
// open the file
InputStream is = this.getClass().getResourceAsStream(fpath);
DataInputStream ds = new DataInputStream(is);
try {
// skip the descriptor
ds.skipBytes(8);

// read map width
int mW = ds.readByte();

// read map height
int mH = ds.readByte();

// read tile width
int tW = ds.readByte();

// read tile height
int tH = ds.readByte();

// create a new tiled layer
tMap = new TiledLayer(mW, mH, ftiles, tW, tH);

// loop through the map data
for (int rCtr = 0; rCtr < mH; rCtr++){
for (int cCtr = 0; cCtr < mW; cCtr++){
// read a tile index
byte nB = ds.readByte();

// if tile index is non-zero
// tile index 0 is usually a blank tile
if (nB > 0) {
// assign (tile index + 1) to the current cell
// TiledLayer objects start tile index at 1
// instead of 0
tMap.setCell(cCtr, rCtr, nB + 1);
}
}
}

} catch (Exception ex) {
tMap = null;
System.err.println("map loading error : " + ex.getMessage());
}
// close the file
ds.close();
ds = null;
is = null;
} catch (Exception ex) {
tMap = null;
System.err.println("map loading error : " + ex.getMessage());
}

// return the newly created map or null if loading failed
return tMap;
}



Make sure to press ALT+Shift+F so NetBeans can add the missing import statements.

The code we just added is the same getMap() method you will find the Simple Tile Map Editor page. We will use it here to serve as an example of it's usage.

We'll make a new method called loadBlockMapFile() that uses the getMap() method to load the map data and initialize the blockMap TiledLayer. This will let you preserve the previous loadBlockMap() method code for future reference. Add the following code below the loadBlockMap() method:


public void loadBlockMapFile(){
//initialize blockMap
blockMap = getMap("/maps/samplemap.smp", imgTileset);
}



We now have to replace the call to loadBlockMap() inside the load() method to call loadBlockMapFile() instead:


loadRoadMap();
loadBlockMapFile();


}



You should see something like this when you run the project:

Screen shot of project running in the emulator


There's not much difference between the output of the previous tutorial and this one except for the position of a few blocks and their colors.

Here's the completed clsCanvas source code so you can check your work:


package MyGame;

import java.io.DataInputStream;
import java.io.InputStream;

import java.util.Random;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.game.Sprite;
import javax.microedition.lcdui.game.TiledLayer;

public class clsCanvas extends GameCanvas implements Runnable {
// key repeat rate in milliseconds
public static final int keyDelay = 250;

//key constants
public static final int upKey = 0;
public static final int leftKey = 1;
public static final int downKey = 2;
public static final int rightKey = 3;
public static final int fireKey = 4;

//key states for up, left, down, right, and fire key
private boolean[] isDown = {
false, false, false, false, false
};
//last time the key changed state
private long[] keyTick = {
0, 0, 0, 0, 0
};
//lookup table for key constants :P
private int[] keyValue = {
GameCanvas.UP_PRESSED, GameCanvas.LEFT_PRESSED,
GameCanvas.DOWN_PRESSED, GameCanvas.RIGHT_PRESSED,
GameCanvas.FIRE_PRESSED
};

private boolean isRunning = true;
private Graphics g;
private midMain fParent;

private Image imgVan;
private Sprite Van;

private int vanSpeed = 2;

private Image imgTileset;
private TiledLayer roadMap;
private TiledLayer blockMap;

/** Creates a new instance of clsCanvas */
public clsCanvas(midMain m) {
super(true);
fParent = m;
setFullScreenMode(true);
}

public void start(){
Thread runner = new Thread(this);
runner.start();
}

public void loadRoadMap(){
//initialize the roadMap
roadMap = new TiledLayer(11, 13, imgTileset, 16, 16);

// Create a new Random for randomizing numbers
Random Rand = new Random();

//loop through all the map cells
for (int y = 0; y < 13; y++){
for (int x = 0; x < 11; x++){
// get a random tile index between 2 and 5
int index = (Math.abs(Rand.nextInt()>>>1) % (3)) + 2;

// set the tile index for the current cell
roadMap.setCell(x, y, index);
}
}

Rand = null;
}

public void loadBlockMap(){
// define the tile indexes to be used for each map cell
byte[][] blockData = {
{10, 8 , 7 , 6 , 10, 9 , 8 , 7 , 6 , 10, 9 },
{6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },
{7 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 7 },
{8 , 0 , 0 , 10, 6 , 0 , 0 , 7 , 0 , 0 , 6 },
{9 , 0 , 0 , 0 , 0 , 0 , 0 , 8 , 0 , 0 , 10},
{10, 0 , 0 , 0 , 0 , 0 , 0 , 9 , 0 , 0 , 9 },
{6 , 0 , 0 , 8 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },
{7 , 0 , 0 , 7 , 0 , 0 , 0 , 0 , 0 , 0 , 7 },
{8 , 0 , 0 , 6 , 0 , 0 , 0 , 10, 0 , 0 , 6 },
{9 , 0 , 0 , 10, 0 , 0 , 7 , 6 , 0 , 0 , 10},
{10, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 9 },
{6 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },
{7 , 8 , 9 , 10, 6 , 7 , 8 , 9 , 10, 6 , 7 }
};

//initialize blockMap
blockMap = new TiledLayer(11, 13, imgTileset, 16, 16);

//loop through all the map cells
for (int y = 0; y < 13; y++){
for (int x = 0; x < 11; x++){
// set the tile index for the current cell
// take note of the reversed indexes for blockData
blockMap.setCell(x, y, blockData[y][x]);
}
}

blockData = null;
}

public void loadBlockMapFile(){
//initialize blockMap from a binary file
blockMap = getMap("/maps/samplemap.smp", imgTileset);
}

public TiledLayer getMap(String fpath, Image ftiles){
TiledLayer tMap = null;
try {
// open the file
InputStream is = this.getClass().getResourceAsStream(fpath);
DataInputStream ds = new DataInputStream(is);
try {
// skip the descriptor
ds.skipBytes(8);

// read map width
int mW = ds.readByte();

// read map height
int mH = ds.readByte();

// read tile width
int tW = ds.readByte();

// read tile height
int tH = ds.readByte();

// create a new tiled layer
tMap = new TiledLayer(mW, mH, ftiles, tW, tH);

// loop through the map data
for (int rCtr = 0; rCtr < mH; rCtr++){
for (int cCtr = 0; cCtr < mW; cCtr++){
// read a tile index
byte nB = ds.readByte();

// if tile index is non-zero
// tile index 0 is usually a blank tile
if (nB > 0) {
//assign (tile index + 1) to the current cell
tMap.setCell(cCtr, rCtr, nB + 1);
}
}
}

} catch (Exception ex) {
tMap = null;
System.err.println("map loading error : " + ex.getMessage());
}
// close the file
ds.close();
ds = null;
is = null;
} catch (Exception ex) {
tMap = null;
System.err.println("map loading error : " + ex.getMessage());
}

// return the newly created map or null if loading failed
return tMap;
}


public void load(){
try{
// load the images here
imgVan = Image.createImage("/images/van.png");
imgTileset = Image.createImage("/images/tileset1.png");
}catch(Exception ex){
// exit the app if it fails to load the image
isRunning = false;
return;
}

// initialize the Sprite object
Van = new Sprite(imgVan, 18, 18);

// show the frame 1 - the second frame
Van.setFrame(1);

// move to 50, 50 (X, Y)
Van.setPosition(16, 16);

loadRoadMap();
loadBlockMapFile();


}

public void unload(){
// make sure the object gets destroyed
blockMap = null;
roadMap = null;
Van = null;
imgTileset = null;
imgVan = null;
}

public void checkKeys(int iKey, long currTick){
long elapsedTick = 0;
//loop through the keys
for (int i = 0; i < 5; i++){
// by default, key not pressed by user
isDown[i] = false;
// is user pressing the key
if ((iKey & keyValue[i]) != 0){
elapsedTick = currTick - keyTick[i];
//is it time to toggle key state?
if (elapsedTick >= keyDelay){
// save the current time
keyTick[i] = currTick;
// toggle the state to down or pressed
isDown[i] = true;
}
}
}
}

public void run() {
int iKey = 0;
int screenW = getWidth();
int screenH = getHeight();
long lCurrTick = 0; // current system time in milliseconds;

load();
g = getGraphics();
while(isRunning){

lCurrTick = System.currentTimeMillis();
iKey = getKeyStates();

checkKeys(iKey, lCurrTick);

if (isDown[fireKey]){
isRunning = false;
}

// get the current position of the van
int cx = Van.getX();
int cy = Van.getY();

// save the current position in temporary vars
// so we can restore it when we hit a block
int tx = cx;
int ty = cy;

if ((iKey & GameCanvas.UP_PRESSED) != 0){
// show the van facing up
Van.setFrame(0);

// move the van upwards
cy -= vanSpeed;
} else if ((iKey & GameCanvas.DOWN_PRESSED) != 0){
// show the van facing down
Van.setFrame(1);

// move the van downwards
cy += vanSpeed;
} else if ((iKey & GameCanvas.LEFT_PRESSED) != 0){
// show the van facing left
Van.setFrame(2);

// move the van to the left
cx -= vanSpeed;
} else if ((iKey & GameCanvas.RIGHT_PRESSED) != 0){
// show the van facing right
Van.setFrame(3);

// move the van to the right
cx += vanSpeed;
}

// update the vans position
Van.setPosition(cx, cy);

//check if the van hits a block
if (Van.collidesWith(blockMap, true)){
//reset the van to the original position
Van.setPosition(tx, ty);
}

//restore the clipping rectangle to full screen
g.setClip(0, 0, screenW, screenH);

/* comment out or remove this code

set drawing color to black
g.setColor(0x000000);

fill the screen with blackness
g.fillRect(0, 0, screenW, screenH);

*/

//draw the road
roadMap.paint(g);

//draw the blocks
blockMap.paint(g);

// draw the sprite
Van.paint(g);

flushGraphics();

try{
Thread.sleep(30);
} catch (Exception ex){

}
}
g = null;
unload();
fParent.destroyApp(false);
fParent = null;
}
}



The way you define the map data depends entirely on you. I chose to use a binary file because of it's size and extracting the data is pretty straight forward. No need for custom parsers and such.

You can also find a bunch of tile map editors online with more powerful features like saving the map data to an XML file and more.

Links to some 2d Map Editors:

Finally, you can also have a look at the new Game Builder that comes with the latest NetBeans 6 and Mobility bundle. Here's a preview from a tutorial in the NetBeans Community Docs: Using Game Builder for Java ME development.

Good luck and have fun!

You know a better way? Found an error? Got a question? Please don't hesitate to post a comment.

1 comments   |   post a comment
said...
Hello, first of all i would like to thank you for this great tutorial, second i have a problem when i run this project, all i get is a white screen, can u help me????? Thanks one more tine.