JaVa
   

Tile Walker Example

Now that we have a scrollable tile engine, the aim for this section is to get a character walking about on the tiles. However, to make it a little cooler, we are going to make it so that a character of any size can walk about on tiles of any size, and all the collision detection will work without any code changes, apart from some static definitions such as the tile width and height. The character will also be able to move at any speed. As with the Tile Scroller example, we will be using the framework, so all we will be looking at here is the code for the pluggable screen, which will be the core of our tile walker. So, let's look at the complete source code for this example before we look at it in detail and see how it all works:

Code Listing 12-17: Tile Walker 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()
 {
 try
 {
 // load the tiles sheet
 tileSheet = ImageIO.read(new File("tilesheet.gif"));
 // load the player direction image (8 directions)
 playerSheet = ImageIO.read(new File("playersheet.gif"));
 }
 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 150 trees
 if(mapArray[x][y] != TREE_TILE)
 mapArray[x][y] = TREE_TILE;
 else
 i--;
 }
 // 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); // set the default scroll position...
 setScrollX(0);
 setScrollY(0);
 // set the players starting position...
 playerWorldX = 3 * TILE_WIDTH;
 playerWorldY = 3 * TILE_HEIGHT;
 updatePlayerScreenPosition();
 }
 public boolean isClearTile(int x, int y)
 {
 return mapArray[x][y]==GRASS_TILE;
 }
 public boolean isValidRow(int x1, int x2, int row)
 {
 for(int i=x1; i<=x2; i++)
 if(!isClearTile(i, row))
 return false;
 return true;
 }
 public boolean isValidColumn(int y1, int y2, int column)
 {
 for(int j=y1; j<=y2; j++)
 if(!isClearTile(column, j))
 return false;
 return true;
 }
 public boolean moveLeft()
 {
 int newPosX = playerWorldX-1;
 // check out of map bounds
 if(newPosX < 0)
 return false;
 // check for blocked tiles int leftColumn = newPosX / TILE_WIDTH;
 int topTile = playerWorldY / TILE_HEIGHT;
 int bottomTile = (playerWorldY+PLAYER_HEIGHT-1) / TILE_HEIGHT;
 if(isValidColumn(topTile, bottomTile, leftColumn))
 {
 playerWorldX--;
 return true;
 }
 else
 return false;
 }
 public boolean moveRight()
 {
 int newPosX = playerWorldX+1;
 // check out of map bounds
 if(newPosX+PLAYER_WIDTH > MAP_PIXEL_WIDTH)
 return false;
 // check for blocked tiles int rightColumn = (newPosX+PLAYER_WIDTH-1) / TILE_WIDTH; int topTile = playerWorldY / TILE_HEIGHT;
 int bottomTile = (playerWorldY+PLAYER_HEIGHT-1) / TILE_HEIGHT;
 if(isValidColumn(topTile, bottomTile, rightColumn))
 {
 playerWorldX++;
 return true;
 }
 else return false;
 }
 public boolean moveUp()
 {
 int newPosY = playerWorldY-1;
 // check out of bounds
 if(newPosY < 0)
 return false;
 // check for blocked tiles int topRow = newPosY / TILE_HEIGHT; int leftTile = playerWorldX / TILE_WIDTH;
 int rightTile = (playerWorldX+PLAYER_WIDTH-1) / TILE_WIDTH;
 if(isValidRow(leftTile, rightTile, topRow))
 {
 playerWorldY--;
 return true;
 }
 else
 return false;
 }
 public boolean moveDown()
 {
 int newPosY = playerWorldY+1;
 // check out of bounds
 if(newPosY+PLAYER_HEIGHT > MAP_PIXEL_HEIGHT)
 return false;
 // check for blocked tiles int bottomRow = (newPosY+PLAYER_HEIGHT-1) / TILE_HEIGHT; int leftTile = playerWorldX / TILE_WIDTH;
 int rightTile = (playerWorldX+PLAYER_WIDTH-1) / TILE_WIDTH;
 if(isValidRow(leftTile, rightTile, bottomRow))
 {
 playerWorldY++;
 return true;
 }
 else
 return false;
 }
 public void updateScrollPosition()
 {
 int newPlayerScreenX = playerWorldX - scrollPosX;
 if(newPlayerScreenX < SCROLL_THRESHOLD) // check left
 {
 setScrollX(scrollPosX - (SCROLL_THRESHOLD - newPlayerScreenX));
 }
 else if(newPlayerScreenX > VIEW_LIMIT_X - SCROLL_THRESHOLD - PLAYER_WIDTH) // check right
 {
 setScrollX(scrollPosX + (newPlayerScreenX - (VIEW_LIMIT_X
 - SCROLL_THRESHOLD - PLAYER_WIDTH)));
 } int newPlayerScreenY = playerWorldY - scrollPosY;
 if(newPlayerScreenY < SCROLL_THRESHOLD) // check top
 {
 setScrollY(scrollPosY - (SCROLL_THRESHOLD - newPlayerScreenY));
 }
 else if(newPlayerScreenY > VIEW_LIMIT_Y - SCROLL_THRESHOLD - PLAYER_HEIGHT) // check bottom
 {
 setScrollY(scrollPosY + (newPlayerScreenY - (VIEW_LIMIT_Y
 - SCROLL_THRESHOLD - PLAYER_HEIGHT)));
 }
 }
 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 updatePlayerScreenPosition()
 {
 playerScreenX = playerWorldX - scrollPosX;
 playerScreenY = playerWorldY - scrollPosY;
 }
 public void movePlayer()
 {
 int playerVectorX = (Globals.keyboard.keyState
 [KeyEvent.VK_LEFT]?-1:0) + (Globals.keyboard.keyState
 [KeyEvent.VK_RIGHT]?1:0);
 int playerVectorY = (Globals.keyboard.keyState
 [KeyEvent.VK_UP]?-1:0) + (Globals.keyboard.keyState
 [KeyEvent.VK_DOWN]?1:0);
 if(playerVectorX==0 && playerVectorY==0)
 return;
 // update player direction frame
 if(playerVectorY<0) playerDir = 1+playerVectorX;
 else if(playerVectorY>0) playerDir = 4+playerVectorX;
 else if(playerVectorX<0) playerDir = 6;
 else if(playerVectorX>0) playerDir = 7;
 boolean blockedMoveX = true;
 boolean blockedMoveY = true;
 for(int i=0; i<playerSpeed; i++)
 {
 if(playerVectorX<0) blockedMoveX = !moveLeft();
 else if(playerVectorX>0) blockedMoveX = !moveRight();
 if(playerVectorY<0) blockedMoveY = !moveUp();
 else if(playerVectorY>0) blockedMoveY = !moveDown();
 if(blockedMoveX && blockedMoveY) // if can't move further
 break;
 }
 updateScrollPosition();
 updatePlayerScreenPosition();
 }
 public void process()
 {
 movePlayer();
 }
 public void render(Graphics g)
 {
 // rendering code goes here...
 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++;
 }
 // draw the player (above the tiles) srcX = playerDir*PLAYER_WIDTH;
 g.drawImage(playerSheet, playerScreenX, playerScreenY,
 playerScreenX+PLAYER_WIDTH, playerScreenY+PLAYER_HEIGHT,
 srcX, 0, srcX+PLAYER_WIDTH, PLAYER_HEIGHT, null);
 // draw players bounding box
 g.setColor(Color.yellow);
 g.drawRect(playerScreenX, playerScreenY, PLAYER_WIDTH-1,
 PLAYER_HEIGHT-1);
 g.drawString("Tile Walker Demo", 10, 15);
 g.drawString("Player Speed: "+playerSpeed, 10, 30);
 }
 public void handleEvent(AWTEvent e)
 {
 switch(e.getID())
 {
 case KeyEvent.KEY_PRESSED:
 {
 KeyEvent keyEvent = (KeyEvent) e;
 int key = keyEvent.getKeyCode();
 // handle key events here...
 if(key == KeyEvent.VK_PAGE_UP)
 {
 if(playerSpeed < MAX_MOVE_SPEED)
 {
 playerSpeed++;
 }
 }
 else if(key == KeyEvent.VK_PAGE_DOWN)
 {
 if(playerSpeed > MIN_MOVE_SPEED)
 {
 playerSpeed--;
 } }
 break;
 }
 }
 }
 // player variables Image playerSheet;
 int playerWorldX;
 int playerWorldY;
 int playerScreenX;
 int playerScreenY;
 int playerDir;
 int playerSpeed = 8;
 static final int PLAYER_WIDTH = 32;
 static final int PLAYER_HEIGHT = 32;
 static final int MIN_MOVE_SPEED = 1;
 static final int MAX_MOVE_SPEED = 50;
 // tile variables
 Image tileSheet;
 int tileOffsetX;
 int tileOffsetY;
 int startTileX;
 int startTileY;
 static final int TILE_WIDTH = 32;
 static final int TILE_HEIGHT = 32;
 static final int GRASS_TILE = 0;
 static final int WALL_TILE = 1;
 static final int TREE_TILE = 2;
 // map variables static final int MAP_WIDTH = 50;
 static final int MAP_HEIGHT = 50;
 static final int MAP_PIXEL_WIDTH = TILE_WIDTH * MAP_WIDTH;
 static final int MAP_PIXEL_HEIGHT = TILE_HEIGHT * MAP_HEIGHT;
 int mapArray[][] = new int[MAP_WIDTH][MAP_HEIGHT];
 final int VIEW_LIMIT_X; // set in the constructor...
 final int VIEW_LIMIT_Y; // set in the constructor...
 int scrollPosX = 0;
 int scrollPosY = 0;
 static final int SCROLL_THRESHOLD = 4 * TILE_WIDTH;
 static final int MAX_TREES = 150;
}


Java End example

When we execute this code with the game framework, we should be able to see something similar to the following:

Java Click To expand
Screenshot-18: The Tile Walker

Let's first look at what extra definitions we have added to our tile engine to incorporate the player into it. First, we have added a new image called playerSheet, which will hold the player character tile sheet that contains the character facing eight different directions. The playerSheet image that we are going to load (called playersheet.gif on the companion CD-ROM) can be seen here:

Java Click To expand
Screenshot-19: The player's tile sheet

Note here that the order of the images is not random, but we will see why they are in this order when we look at the movement code later in this section. Next we have declared two integer variables to hold the actual world position where the player is located. This is simply an x, y coordinate in pixels of the actual position on the map (not the screen). The declaration for these two variables can be seen here:

int playerWorldX;
int playerWorldY;


As the world position of the player is not the same as the screen position, we also are going to create two screen coordinate variables to store the current screen x, y position of the player, which will be worked out from the player's world coordinates when we make any movements. The declaration for these two variables can be seen here:

int playerScreenX;
int playerScreenY;


Next we have two variables to record the current direction of the player (which will relate to the playerSheet graphic to get the correct image direction) and also the current movement speed of the player in pixels. These two can be seen here:

int playerDir;
int playerSpeed = 8;


Finally, we have four new final static variables, which define the width and height of the player (remember that we are coding this so it will work for any size of player) and also the minimum and maximum movement speeds (as we will also be able to change this). These four static variables can be seen here:

static final int PLAYER_WIDTH = 32;
static final int PLAYER_HEIGHT = 32;
static final int MIN_MOVE_SPEED = 1;
static final int MAX_MOVE_SPEED = 50;


Now that we have looked at the additional member variables that we have added to our engine, let's look at what we have changed and added into the constructor. First off, we have added an extra line of code into the image loading part so we can load our player tile sheet, which we saw earlier in our playerSheet image. The complete image loading section can be seen here:

try
{
 // load the tiles sheet
 tileSheet = ImageIO.read(new File("tilesheet.gif"));
 // load the player direction image (8 directions)
 playerSheet = ImageIO.read(new File("playersheet.gif"));
}
catch(IOException e)
{
 System.out.println(e); }


The next change is that we have added two lines of code to specify the player's initial world position (i.e., the x, y coordinate, in pixels, where the player is located in our virtual world). Initially, we are going to place the player on tile (3, 3), which is the fourth down from the top and the fourth across from the left, remembering the tile at the very top left is denoted by (0, 0). If you remember also that our world x, y positions are in pixels, you will realize that we also need to multiply the tiles on which we wish the player to start by the TILE_WIDTH and TILE_HEIGHT variables. This can be seen here:

playerWorldX = 3 * TILE_WIDTH;
playerWorldY = 3 * TILE_HEIGHT;


So now that we have the player placed at the correct position in our world coordinates, we need to get the actual screen position of the player (as this may be different from the world coordinates if the map has been scrolled in any direction). We have created a method called updatePlayerScreenPosition(), which will do this for us. Let's look at this method now:

public void updatePlayerScreenPosition()
{
 playerScreenX = playerWorldX - scrollPosX;
 playerScreenY = playerWorldY - scrollPosY;
}


As you can see, this method only deducts the current scroll positions from both the x and y coordinates, giving us the screen position of the player, relative to the current scrolled position of the map. Next we are going to look at moving the player. We handle this in the process method, which, as we know from developing the framework, is called every frame before the render method is called. In the process method, we simply make a call to a method called movePlayer(). Let's look at this method step by step now. In the movePlayer method, we first check the key states for the cursor keys. For the left and right keys, if the left key is pressed, it will return –1; otherwise it will return 0. If the right key is pressed, it will return 1; otherwise it will return 0. So if we add these two results together, we will obtain one of the following results.

Left Key

 

Right Key

Result

Not Pressed (0)

+

Not Pressed (0)

0

Pressed (–1)

+

Not Pressed (0)

–1

Not Pressed (0)

+

Pressed (1)

1

Pressed (–1)

+

Pressed (1)

0

So as you can see from the table, if the left key is down but the right is not, we will get a value of –1. If the right is down but the left is not, we will get the value 1. If they are either both up or both down, we will get the result 0. The line of code that does this for us can be seen here:

int playerVectorX = (Globals.keyboard.keyState
 [KeyEvent.VK_LEFT]?-1:0) + (Globals.keyboard.keyState
 [KeyEvent.VK_RIGHT]?1:0);


Notice how we store this result in a variable called playerVectorX. This variable will now hold which x direction the player should move—to the right (1), to the left (–1), or not at all (0). We use this exact technique again for the up and down keys to find out the movement required in the y direction. The line of code that does this can be seen here:

int playerVectorY = (Globals.keyboard.keyState
 [KeyEvent.VK_UP]?-1:0) + (Globals.keyboard.keyState
 [KeyEvent.VK_DOWN]?1:0);


Next we have a special case for checking to see if both the playerVectorX variable and the playerVectorY variable are equal to 0. If this is the case, either all the keys are in a released state or all the keys are being held down (see the previous table). If this is so, the player will not be moving this frame and we can return from this method. This if statement can be seen here:

if(playerVectorX==0 && playerVectorY==0)
 return;


Now we are going to assign the correct value to the playerDir variable (declared as a member of our class), which is used to reference the correct image within the playerSheet to basically make the player face the direction that he or she is moving. Let's first look at this section of code, and then we will look into it in more detail, as it is not obvious at first glance.

if(playerVectorY<0) playerDir = 1+playerVectorX;
else if(playerVectorY>0) playerDir = 4+playerVectorX;
else if(playerVectorX<0) playerDir = 6;
else if(playerVectorX>0) playerDir = 7;


The first line of the four lines of code checks to see if the playerVectorY is less than 0 (i.e., the player is going to be moving in an upward direction). So if we look at the following player sheet image:

Java Click To expand
Screenshot-20:

...we can see that tile 1 refers to the player image facing upward (remembering that the first tile is 0). So we set the base tile to 1 and proceed by adding the playerVectorX, which will be –1 if we are also moving left as well as up; this will then access tile 0 when we add it to the initial value 1, which is an image of the player moving diagonally up to the left. If the player is not moving in the x direction, we will add 0 to the initial 1 value, meaning it will use tile 1, which is the player facing upward. Finally, if the player is also moving right (i.e., the playerVectorX value is 1), we will get 1+1, meaning that we will then be referencing tile 2, which is the player facing diagonally up to the right. This process is then repeated if the playerVectorY is positive (i.e., the player is moving downward). For this we use the initial value of 4 to add playerVectorX, so the span of the tiles in the tile sheet would be as follows:

Java Click To expand
Screenshot-21:

The final two lines of code in this are used to handle the cases where the player is just simply going left or right (i.e., playerVectorX is negative or positive), and we set these values directly as 6 and 7, which reference the last two tiles in the sheet, respectively. Another approach to this would have been to use a 3x3 tile sheet to represent the player as follows:

Java ScreenShot
Screenshot-22: Another approach to the player's tile sheet

By using this style of sheet, our playerVectorX and playerVectorY values would have mapped to the playerSheet easier. However, we have the disadvantage of the blank tile in the middle. Anyway, next we declare two variables to determine whether a player can move in the x and y directions, respectively. These two declarations can be seen here:

boolean blockedMoveX = true;
boolean blockedMoveY = true;


Notice how we set the initial values to true; this is because if the player is not moving in either of the directions, we will assume the player is blocked, as we will not need to perform any calculations anyway. Next we create a for loop, which starts at 0 and loops until the player's speed is reached (defined as playerSpeed). This can be seen here:

for(int i=0; i<playerSpeed; i++)
{
 …
} 


So what we are really saying here is that for each pixel that the player is going to move, we want to check for any collisions. Within the for loop, we are only going to need to check for x and y movements if the player is actually moving in that direction. This can be seen for the movement along the x-axis as follows:

if(playerVectorX<0) blockMoveX = !moveLeft();
else if(playerVectorX>0) blockMoveX = !moveRight();


This first checks if the playerVectorX is less than 0 (i.e., the player is moving left) and then if the player cannot move left (we will look at the moveLeft method in a moment; for now, note that it returns true if the player can move left and false if it cannot). Therefore, if the player is moving left and moveLeft returns false, blockedMoveX will then be equal to true, meaning the player cannot move in the x direction. The other part of this statement is just the same, except we are checking the right direction. Let's have a look at the moveLeft method to see how it works. Here is a listing of the complete moveLeft method:

public boolean moveLeft()
{
 int newPosX = playerWorldX-1;
 // check out of map bounds
 if(newPosX < 0)
 return false;
 // check for blocked tiles int leftColumn = newPosX / TILE_WIDTH;
 int topTile = playerWorldY / TILE_HEIGHT;
 int bottomTile = (playerWorldY+PLAYER_HEIGHT-1) / TILE_HEIGHT;
 if(isValidColumn(topTile, bottomTile, leftColumn))
 {
 playerWorldX--;
 return true;
 }
 else
 return false;
}


First we work out the player's new world position, newPosX, which will be the current world position playerWorldX minus one (as we are moving left one pixel). Next we check that this is still within the bounds of the map—in this case checking that newPosX is greater than 0, as the leftmost border of the world has an x coordinate of 0. Then we can perform a check for blocked tiles. For moving left, we first work out on which "column" of tiles the player will be moving (i.e., basically just the new tile x position that can be worked out by dividing the newPosX world position by the width of the tile's TILE_WIDTH). We then store this value in the leftColumn variable. Now, if you have a look at the following diagram of the player moving left, you should see an instant problem:

Java ScreenShot
Screenshot-23: Tile collisions

We in fact need to check two tiles. What we can do to check this is first find the tile that the top-left pixel is over, using the following line of code:

int topTile = playerWorldY / TILE_HEIGHT;


Then we can find out the bottom tile that the player is covering using this next line of code:

int bottomTile = (playerWorldY+PLAYER_HEIGHT-1) / TILE_HEIGHT;


Note that these are only the y positions; however, remember we also worked out the column that it was going to cover. We use these two new values, as well as the column, by passing them into another method now called isValidColumn, which we will look at now. The isValidColumn method is really simple and, in fact, adds a lot to our engine, as it allows us to have any size of player with respect to testing collisions. This method loops from the top tile that was passed in, down every tile in the column, until it reaches the bottom tile, testing each tile to see if it is clear. In the previous example, it would test the tiles as follows:

Java ScreenShot
Screenshot-24:

If you can imagine having a larger player, however, the testing would look as follows:

Java ScreenShot
Screenshot-25:

So let's actually have a look at the isValidColumn method. Here it is in full:

public boolean isValidColumn(int y1, int y2, int column)
{
 for(int j=y1; j<=y2; j++)
 if(!isClearTile(column, j))
 return false;
 return true;
}


So for each row in the column, starting at y1, we simply call the isClearTile method, which returns true or false depending on whether the tile at the position passed in is either clear or blocked. If any of the tiles are not clear, during the for loop the method returns false; otherwise, we return true, meaning the column was valid and the player can make the move successfully. Let's have a quick look at the isClearTile method now.

public boolean isClearTile(int x, int y)
{
 return mapArray[x][y]==GRASS_TILE;
}


As you can see, all we are doing is looking at the mapArray as the specified tile position to check if the tile is a GRASS_TILE, which in our map is the only tile you can walk on. Of course, if you have more tiles that you could move about on (such as sand tiles or gravel tiles), you could adapt this method accordingly. That's all there is to checking the horizonal movement. If you have a quick look over the vertical movement, it is exactly the same idea, except we are checking the rows instead of the columns. If we now go back to where we were in the movePlayer method, we can see that the next part checks the y movement (which as we mentioned a minute ago is pretty much the same as the x movement). Then we have a simple check that finds out if either of the x and y movements is blocked. If it is, we break out of the for loop. This can be seen here:

if(blockedMoveX && blockedMoveY) // if can't move further
 break;


Next, after the for loop, we need to update the scroll position of the map based upon the player's new world position. Basically, this method will allow us to scroll the map when the player goes within a certain distance of one of the edges of the app window. Let's first have a look at the complete method here:

public void updateScrollPosition()
{
 int newPlayerScreenX = playerWorldX - scrollPosX;
 if(newPlayerScreenX < SCROLL_THRESHOLD) // check left
 {
 setScrollX(scrollPosX - (SCROLL_THRESHOLD - newPlayerScreenX));
 }
 else if(newPlayerScreenX > VIEW_LIMIT_X - SCROLL_THRESHOLD - PLAYER_WIDTH) // check right
 {
 setScrollX(scrollPosX + (newPlayerScreenX - (VIEW_LIMIT_X - SCROLL_THRESHOLD - PLAYER_WIDTH)));
 } int newPlayerScreenY = playerWorldY - scrollPosY;
 if(newPlayerScreenY < SCROLL_THRESHOLD) // check top
 {
 setScrollY(scrollPosY - (SCROLL_THRESHOLD - newPlayerScreenY));
 }
 else if(newPlayerScreenY > VIEW_LIMIT_Y - SCROLL_THRESHOLD - PLAYER_HEIGHT) // check bottom
 {
 setScrollY(scrollPosY + (newPlayerScreenY - (VIEW_LIMIT_Y
 - SCROLL_THRESHOLD - PLAYER_HEIGHT)));
 }
}


First we work out what the player's new x position on the screen will be by subtracting the current x scroll position from the player's new world position, playerWorldX. Then we check the left-hand side of the screen and see if the player's new x screen position is less than the SCROLL_ THRESHOLD, which we have defined at the bottom of the class to be 4*TILE_WIDTH, meaning that if the player goes within four tiles of any edge on the screen, it will start to scroll. If the player is within the threshold of the left-hand side, we simply call the setScrollX method that we created in the previous example to be the current scroll position scrollPosX minus the SCROLL_THRESHOLD, less the player's new screen position. Note that the variables newPlayerScreenX and newPlayerScreenY are local variables and used to work out where the player has moved, simply for adapting the scrolling to the movement. The player's real new screen position is worked out after this method is called, after the scroll position has been worked out properly. We then simply repeat this process for the right-hand side and also the top and bottom of the screen. The final part of the movePlayer method makes a call to the method updatePlayerScreenPosition, which updates the player's real screen position based on the player's world map position and the newly updated scroll position, as we just discussed. Now that we have looked at player movement, let's take a look in the render method and see how the player is actually drawn to the screen. This is actually very simple to do now that we have performed all of the working out in the movePlayer method. First we get the source x position in the playerSheet, which we can work out from the playerDir variable that we set in the movePlayer method. This can be seen here:

srcX = playerDir*PLAYER_WIDTH;


Then we just call the drawImage method of the Graphics object g, using the player's playerScreenX and playerScreenY variables to denote the position at which to draw the player. This can be seen here:

g.drawImage(playerSheet, playerScreenX, playerScreenY,
 playerScreenX+PLAYER_WIDTH, playerScreenY+PLAYER_HEIGHT,
 srcX, 0, srcX+PLAYER_WIDTH, PLAYER_HEIGHT, null);


And that's it! Let's do a few experiments now with our tile engine to see how it will easily adapt to different player and screen sizes.

Changing the Player Size

Okay, so what if our player has a big brother? Let's see how our engine handles a character that is three times the size of our original player, making each of our player images 96x96 pixels. You can either resize the playersheet.gif image yourself to try this or you can use the supplied one on the companion DVD called bigplayersheet.gif (remember to change the filename of the image loaded in the constructor appropriately). As well as the image, the only other thing to change is the PLAYER_WIDTH and PLAYER_HEIGHT variables to 96 and reduce the tree count to 10 (i.e., set MAX_TREES to 10) as he is a big boy. With these changes, if you compile the code, you should get something that looks similar to the following:

Java Click To expand
Screenshot-26: The red man's big brother Bungle

Move him about a bit...see how the collisions work perfectly? It's great isn't it? Go on, admit it.

Changing the app Size

Although this would have worked in the Tile Scroller example as well, we just thought it would be nice to show it here. First, put the player back to the original size and put the tree count back to 150 (although you don't have to if you don't want to!). Then simply change both the DISPLAY_ WIDTH and DISPLAY_HEIGHT to 200 (note that we won't be able to use the full-screen mode, as it is not a supported full-screen resolution). Then just change the SCROLL_THRESHOLD to 1*TILE_WIDTH (or just set it to be TILE_WIDTH) so the map will scroll if the player is within one tile of the edge of the screen. When we run the Tile Walker with these few changes, it should look as follows.

Java ScreenShot
Screenshot-27: A mini Tile Walker app

Changing the Map Size

Our final little experiment will be to change the map to a smaller size than the viewable area, so there will be no scrolling involved. All we need to do is change the MAP_WIDTH and MAP_HEIGHT variables to 10 instead of 50. Also, we need to reduce the number of trees, so change MAX_TREES to be 3 instead of 150. When you make these changes, you will see a screen shot similar to Screenshot-28 on the following page when you run the Tile Walker app.

Java Click To expand
Screenshot-28: A small 10x10 tile map

So, as you can see from these three little experiments, the tile engine can easily adapt to changes. Of course, there are many, many more features that you could implement into this tile engine, but we will leave the research and implementation of these features to you. However, you have got a good base to start from now!

JaVa
   
Comments