JaVa
   

Tile Scroller Example

Possibly the most essential structure for most two-dimensional games is a tiled engine. The levels of almost every two-dimensional platform game will be made up of many screen tiles, all put together as a 2D grid like the structure of the two-dimensional animation sheets that we saw at the beginning of this chapter. If you recall games such as Super Mario Bros. and Bomberman, you will see that the sections of the screen are divided into tiled regions. There are many important advantages to using a level that is tile based, as the level is in a fixed structure. Collision detection is simple to handle in a tiled structure, and large maps can be created from very few graphics. Since we do not need a graphic the size of our map, we can build the map from individual tiles, where each tile can be reused elsewhere in the map. Now that we have a good solid game framework in place, we are going to use it to create a simple, yet robust tile engine. The aim of this engine is to handle the following key features:

So where do we start then? Well, since we are using the framework that we created in the previous section, there would be no point in regurgitating the code in the tutorial, so we will just look at the game screen that will easily "plug in" to the game framework with a mere few lines of code. So let's now look at the complete code listing of the tile engine and then see a sample screen shot of how it will look when we run it.

Listing 12-16: Tile Scroller example (works with the framework)
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.imageio.*;
import java.io.*;
import java.util.*;
public class MainScreen extends TemplateScreen
{
 public MainScreen()
 {
 // set max map viewable screen size
 VIEW_LIMIT_X = Math.min(bounds.width, MAP_PIXEL_WIDTH);
 VIEW_LIMIT_Y = Math.min(bounds.height, MAP_PIXEL_HEIGHT);
 // load the tiles...
 try
 {
 tileSheet = ImageIO.read(new File("tilesheet.jpg"));
 }
 catch(IOException e)
 {
 System.out.println(e);
 } // place the walls around the edge...
 // horizontal walls
 for(int i=0; i<MAP_WIDTH; i++)
 {
 mapArray[i][0] = WALL_TILE;
 mapArray[i][MAP_HEIGHT-1] = WALL_TILE;
 }
 // vertical walls
 for(int i=0; i<MAP_HEIGHT; i++)
 {
 mapArray[0][i] = WALL_TILE; mapArray[MAP_WIDTH-1][i] = WALL_TILE;
 }
 // fill in middle with grass tiles...
 for(int i=1; i<MAP_WIDTH-1; i++)
 for(int j=1; j<MAP_HEIGHT-1; j++)
 mapArray[i][j] = GRASS_TILE;
 int numTrees = Math.min(MAX_TREES, (MAP_WIDTH-2) * (MAP_HEIGHT-2));
 // place random trees in grass area... Random r = new Random();
 int x, y;
 for(int i=0; i<numTrees; i++)
 {
 x = r.nextInt(MAP_WIDTH-2)+1;
 y = r.nextInt(MAP_HEIGHT-2)+1;
 // make sure we set the full amount of trees
 if(mapArray[x][y] != TREE_TILE)
 mapArray[x][y] = TREE_TILE;
 else
 i--;
 }
 // set the default scroll position...
 setScrollX(0);
 setScrollY(0); }
 public void process()
 {
 // handle scroll key states...
 // vertical scrolling
 if(Globals.keyboard.keyState[KeyEvent.VK_LEFT] && !Globals.keyboard.keyState[KeyEvent.VK_RIGHT])
 {
 setScrollX(scrollPosX - scrollSpeed);
 }
 else if(Globals.keyboard.keyState[KeyEvent.VK_RIGHT] && !Globals.keyboard.keyState[KeyEvent.VK_LEFT])
 {
 setScrollX(scrollPosX + scrollSpeed);
 }
 // horizontal scrolling
 if(Globals.keyboard.keyState[KeyEvent.VK_UP] && !Globals.keyboard.keyState[KeyEvent.VK_DOWN])
 {
 setScrollY(scrollPosY - scrollSpeed);
 }
 else if(Globals.keyboard.keyState[KeyEvent.VK_DOWN] && !Globals.keyboard.keyState[KeyEvent.VK_UP])
 {
 setScrollY(scrollPosY + scrollSpeed);
 }
 }
 public void setScrollX(int x)
 {
 scrollPosX = x;
 scrollPosX = Math.max(scrollPosX, 0);
 scrollPosX = Math.min(scrollPosX, MAP_PIXEL_WIDTH - VIEW_LIMIT_X);
 tileOffsetX = scrollPosX % TILE_WIDTH;
 startTileX = scrollPosX / TILE_WIDTH;
 }
 public void setScrollY(int y)
 {
 scrollPosY = y;
 scrollPosY = Math.max(scrollPosY, 0);
 scrollPosY = Math.min(scrollPosY, MAP_PIXEL_HEIGHT - VIEW_LIMIT_Y);
 tileOffsetY = scrollPosY % TILE_HEIGHT;
 startTileY = scrollPosY / TILE_HEIGHT;
 }
 public void render(Graphics g)
 {
 // rendering code goes here...
 g.setColor(Color.black);
 g.fillRect(0, 0, bounds.width, bounds.height);
 int srcX;
 int tileX = startTileX;
 int tileY;
 for(int x=-tileOffsetX; x<VIEW_LIMIT_X; x+=TILE_WIDTH)
 {
 tileY = startTileY;
 for(int y=-tileOffsetY; y<VIEW_LIMIT_Y; y+=TILE_HEIGHT)
 { srcX = mapArray[tileX][tileY]*TILE_WIDTH;
 g.drawImage(tileSheet, x, y, x+TILE_WIDTH,
 y+TILE_HEIGHT,
 srcX, 0, srcX+TILE_WIDTH, TILE_HEIGHT, null);
 tileY++;
 }
 tileX++;
 }
 g.setColor(Color.yellow);
 g.drawString("Tile Scroller Demo", 10, 15);
 g.drawString("Scroll Speed: "+scrollSpeed, 10, 30);
 }
 public void handleEvent(AWTEvent e)
 {
 // handle specific non-flagging key events
 if(e.getID() == KeyEvent.KEY_PRESSED)
 {
 KeyEvent keyEvent = (KeyEvent) e;
 if((keyEvent.getKeyCode() == KeyEvent.VK_PAGE_DOWN) && (scrollSpeed > MIN_SCROLL_SPEED))
 scrollSpeed--;
 else if((keyEvent.getKeyCode() == KeyEvent.VK_PAGE_UP) && (scrollSpeed < MAX_SCROLL_SPEED))
 scrollSpeed++;
 }
 }
 // array to store the tile id's...
 int mapArray[][] = new int[MAP_WIDTH][MAP_HEIGHT];
 // map and tile sizes
 static final int MAP_WIDTH = 50;
 static final int MAP_HEIGHT = 50;
 static final int TILE_WIDTH = 32;
 static final int TILE_HEIGHT = 32;
 static final int MAP_PIXEL_WIDTH = TILE_WIDTH * MAP_WIDTH;
 static final int MAP_PIXEL_HEIGHT = TILE_HEIGHT * MAP_HEIGHT;
 int VIEW_LIMIT_X; // set in the constructor...
 int VIEW_LIMIT_Y; // set in the constructor...
 // scroll position of the map...
 int scrollPosX = 0;
 int scrollPosY = 0;
 int scrollSpeed = 10; static final int MIN_SCROLL_SPEED = 1;
 static final int MAX_SCROLL_SPEED = 50;
 // keyboard flag states
 boolean keyState[] = new boolean[256];
 // tile definitions...
 static final int GRASS_TILE = 0;
 static final int WALL_TILE = 1;
 static final int TREE_TILE = 2;
 static final int MAX_TILES = 3;
 // Tile sheet to store the tiles...
 Image tileSheet;
 static final int MAX_TREES = 150;
 int tileOffsetX;
 int tileOffsetY;
 int startTileX;
 int startTileY;
}


Java End example

As we are using the framework, when we run the tile scroller demo, we will first be given the option of full-screen or windowed mode. The following figure is a screen shot of it in windowed mode, but for the best results, you should run it yourself. Remember that you will need to create an instance of the screen in the gameInit method of the framework and store a reference to the screen in the Globals class.

Java Click To expand
Screenshot-16: The Tile Scroller

Now that we have seen the tile scroller in action, let's delve into the inner workings of the code to see what makes it what it is. First, let's look over the member variables that we have defined and find out what each of them are for (although most are pretty self-explanatory).

int mapArray[][] = new int[MAP_WIDTH][MAP_HEIGHT];


First we have the mapArray two-dimensional array, which will hold integer values that will relate to the tile. So this array will directly map to our map coordinates (i.e., if we accessed array position 0, 0, we could find out what tile "type" was at the top-left position of the map). Note that we have declared the array using the variables MAP_WIDTH and MAP_HEIGHT which are defined as follows:

static final int MAP_WIDTH = 50;
static final int MAP_HEIGHT = 50;


These two variables simply specify how many tiles across and down are used to make up the complete map. Next we declare the width and height in pixels of each individual tile in the map. This can be seen here:

static final int TILE_WIDTH = 32;
static final int TILE_HEIGHT = 32;


Note that by changing these values, the engine will adapt accordingly and work with any specified tile sizes (just ensure that the images that you supply for the tiles are the same dimensions). So now that we have the size of the map in tiles and the size of each tile, we can work out the actual width and height of the map in pixels by simply multiplying the width of the tiles by the width in tiles of the map and the same for the height. Store the results in the variables called MAP_PIXEL_WIDTH and MAP_PIXEL_HEIGHT. This can be seen here:

static final int MAP_PIXEL_WIDTH = TILE_WIDTH * MAP_WIDTH;
static final int MAP_PIXEL_HEIGHT = TILE_HEIGHT * MAP_HEIGHT;


Then we declare two variables to hold the maximum extents of the map that can be seen on the screen, which were at one time called VIEW_ LIMIT_X and VIEW_LIMIT_Y. However, these will be assigned in the constructor. Next up we have the actual pixel scroll position (x, y) that we are currently looking at on the map. If we can't physically view the entire map within the screen size, we need to note the position on the map that we are actually looking at, so we store this in scrollPosX and scrollPosY. We'll see these values get used later in this example when we get to moving around and rendering the map to the screen. Because we want to create a robust tile engine, it would be nice to be able to change the speed in pixels at which the map scrolls, so we need to store the current scroll speed and also the minimum and maximum limits. This can be seen here:

int scrollSpeed = 10; static final int MIN_SCROLL_SPEED = 1;
static final int MAX_SCROLL_SPEED = 50;


Then we have our tile definitions, which are simply integer values that will be used within our engine to reference different types of tiles in an easy-to-read manner. These definitions can be seen here:

static final int GRASS_TILE = 0;
static final int WALL_TILE = 1;
static final int TREE_TILE = 2;
static final int MAX_TILES = 3;


Once we have defined our tile types, we need storage for the tile sheet, which will represent our tiles, so we create an Image reference for this. This can be seen in the following line of code:

Image tileSheet;


Next we have an integer value to determine the maximum number of trees that are to be placed on the map called MAX_TREES. Then finally, we have declared variables to store the current offset and start tiles of the map (don't worry about these for now). The next logical step is to look at the constructor for the screen where we set everything up. First we assign the VIEW_LIMIT_X and VIEW_LIMIT_Y variables to be the maximum value of either the bounds of the screen or the pixel width of the map.

VIEW_LIMIT_X = Math.min(bounds.width, MAP_PIXEL_WIDTH);
VIEW_LIMIT_Y = Math.min(bounds.height, MAP_PIXEL_HEIGHT);


Why? This is really just to handle if our map is smaller than the dimensions of the screen. We will see these values being used later when we perform the scrolling. Next we load in the tile sheet, which is available on the companion DVD and called tilesheet.gif. The image loading code can be seen here:

try
{
 tileSheet = ImageIO.read(new File("tilesheet.jpg"));
}
catch(IOException e)
{
 System.out.println(e);
} 


Next, we place walls around the edge of the map by simply setting the appropriate values of the two-dimensional mapArray array to be WALL_TILE. This can be seen in the following segment of code:

for(int i=0; i<MAP_WIDTH; i++)
{
 mapArray[i][0] = WALL_TILE;
 mapArray[i][MAP_HEIGHT-1] = WALL_TILE;
}
for(int i=0; i<MAP_HEIGHT; i++)
{
 mapArray[0][i] = WALL_TILE; mapArray[MAP_WIDTH-1][i] = WALL_TILE;
}


Now that we have the walls, the rest of the map does not have any tiles set, so then we can fill in the rest of the map with grass tiles using the following code:

for(int i=1; i<MAP_WIDTH-1; i++)
 for(int j=1; j<MAP_HEIGHT-1; j++)
 mapArray[i][j] = GRASS_TILE;


Then finally, we place trees randomly across the map to the limit of MAX_TREES or the area of grass, whichever is least, and then we set the starting scroll position to the top-left corner (0, 0). We'll look at the methods setScrollX and setScrollY shortly. So now that we have our map set up, let's look at the code to scroll it. All we do here is check if one of the arrow keys is down and then adjust the scroll position (either x or y, depending on the arrow key) by the current scroll speed (defined as scrollSpeed). The magic, however, actually happens within the setScrollX and setScrollY methods, so let's have a look at these now.

public void setScrollX(int x)
{
 scrollPosX = x;
 scrollPosX = Math.max(scrollPosX, 0);
 scrollPosX = Math.min(scrollPosX, MAP_PIXEL_WIDTH - VIEW_LIMIT_X);
 tileOffsetX = scrollPosX % TILE_WIDTH;
 startTileX = scrollPosX / TILE_WIDTH;
}


To set a new x scroll position, first assign the new value to our member variable scrollPosX. Then check if the scroll position is greater than zero by assigning it to be the maximum value of itself or zero. Then check if it is within the right edge of the map. Do this by picking the minimum value of itself and the MAP_PIXEL_WIDTH minus the VIEW_ LIMIT_X. If you think about it, you will only be able to scroll far enough right to display the whole map, so this takes what you can actually see on the screen (VIEW_LIMIT_X) into account. Once the new scrollPosX value is checked for validity, we can then assign our tileOffsetX and startTileX values ready for the rendering process. The tileOffsetX is how many pixels the tile at the left of the screen is "off" the screen by. startTileX is simply the starting horizontal tile that the renderer should begin drawing. We work out the offset by taking the remainder of the current scroll position divided by the tile width. If the tiles are 32 pixels wide and the current scroll position is 40, we could work out that the leftmost tile should be drawn 8 pixels to the left of the visible area, making it appear as if the map had scrolled by 40 pixels. As for the start tile, a simple division does it, giving us the leftmost tile from that current scroll position (this discards the remainder since we are using integer values, not floating-point). Going back to our previous example, the starting tile would be "1", as tile "0" would not be visible and only (32 – 8) = 24 pixels of tile "1" would be visible. If you take a quick look back at the code for setScrollY, you'll notice the code is pretty much the same with the x's replaced with y's, so nothing exciting there. Finally, we need to look at the actual rendering part, which is not that complex now that we have seen how the map scrolls. First we set the current tileX position to be the startTileX position, which we set in our setScrollX method. Therefore, this will be the leftmost tile that is drawn (from the mapArray). Next we perform the rendering with two for loops to fill the screen with tiles at the current scroll position in the map. Let's have a look at the conditions for this loop now:

for(int x=-tileOffsetX; x<VIEW_LIMIT_X; x+=TILE_WIDTH)


We start by setting the x position to the negative value of the tileOffsetX, which if we remember from before is simply how many pixels of the leftmost tile cannot be seen due to the scrolling. The termination condition for the loop is that we go past the VIEW_LIMIT_X, meaning we do not draw outside the right-hand side of the screen. Finally, we move along by the width of a tile, so we are ready to place the next one. Once inside the first for loop, set the current tileY to be equal to startTileY, which we assigned the setScrollY method. Note that because this is placed here, after every cycle through the first for loop, the tileY variable will be reset to the start. Thus, we will draw the tiles from top to bottom and then move along a column and repeat the process. Let's look at the second for loop now.

for(int y=-tileOffsetY; y<VIEW_LIMIT_Y; y+=TILE_HEIGHT)


As you can see, this is the same as the first for loop, except it is in respect to y this time. Within these two for loops are the two magical lines of code that draw our map. This can be seen here:

srcX = mapArray[tileX][tileY]*TILE_WIDTH;
g.drawImage(tileSheet, x, y, x+TILE_WIDTH, y+TILE_HEIGHT,
 srcX, 0, srcX+TILE_WIDTH, TILE_HEIGHT, null);


This draws the tile referenced by the ID contained within the location in the mapArray. Notice how we use the tileX and tileY values to retrieve the correct tile type from the map and then multiply this value by the TILE_WIDTH to find the correct source x position in the tile sheet (this is the same technique that we saw in the animation section earlier in this chapter). Then specify the x and y as the position we draw (as this takes into account our x and y tile offsets). After this, simply increment the tileY variable, and then outside the inner for loop, increment the tileX variable. Here is the complete render method for you to refer to:

 public void render(Graphics g)
 {
 // rendering code goes here...
 g.setColor(Color.black);
 g.fillRect(0, 0, bounds.width, bounds.height);
 int srcX;
 int tileX = startTileX;
 int tileY;
 for(int x=-tileOffsetX; x<Globals.DISPLAY_WIDTH;
 x+=TILE_WIDTH)
 {
 tileY = startTileY;
 for(int y=-tileOffsetY; y<Globals.DISPLAY_HEIGHT;
 y+=TILE_HEIGHT)
 { srcX = mapArray[tileX][tileY]*TILE_WIDTH;
 g.drawImage(tileSheet, x, y, x+TILE_WIDTH,
 y+TILE_HEIGHT,
 srcX, 0, srcX+TILE_WIDTH, TILE_HEIGHT, null);
 tileY++;
 }
 tileX++;
 }
 g.setColor(Color.yellow);
 g.drawString("Tile Scroller Demo", 10, 15);
 g.drawString("Scroll Speed: "+scrollSpeed, 10, 30);
 }


And that's it! Look over the code again and make sure you understand what's happening before moving on to the next example, as we are going to be building upon this simple, yet robust tile engine. Just before we move on though, let's see how robust this baby is! If you grab the oddtilesheet.gif image off the CD-ROM, it will give you the same three tiles in a sheet. However, each tile in the sheet is 32x64 pixels, giving us a rectangular tile rather than a square one. To use these new tiles in the engine, all we need to do is change the TILE_HEIGHT variable to 64 and change the part in the constructor that loads the image. We will get something that looks like the following figure when we run it.

Java Click To expand
Screenshot-17: The Tile Scroller with 32x64 pixel tiles instead of 32x32 pixel tiles

As you can see, it looks awful, as all we have done is stretch the original images in a paint package. But the actual tile engine works perfectly for any sized tiles.

JaVa
   
Comments