Wednesday, December 30, 2009

Managing the World









Managing the World


WorldDisplay manages:


  • The moving tile floor, represented by a single GIF

  • No-go areas on the floor

  • Blocks occupying certain tiles

  • Pickups occupying certain tiles

  • Communication between the player and aliens sprites


The communication between the player and sprites in the game is rudimentary, mainly involving the transmission of position information and the number of pickups left. However, the coding technique of passing this information through the WorldDisplay is a useful one since it allows WorldDisplay to monitor and control the interactions between the sprites. WorldDisplay utilizes three main data structures:


  • An obstacles[][] Boolean array specifying which tiles are no-go's or contain blocks

  • A WorldItems object that stores details on blocks, pickups, and sprites in tile row order to make them easier to draw with the correct z-ordering

  • A numPickups counter to record how many pickups are still left to be picked up


These are simply declared as variables in the class:



private boolean obstacles[][];
private WorldItems wItems;
private int numPickups;



WorldDisplay's methods fall into five main groups, which I'll consider in detail in the following subsections:


  • The loading of floor information, which describes where the tiles, rows, and columns are located on the floor

  • The loading of world entity information, which gives the tile coordinates of the no-go areas, blocks, and pickups

  • Pickup-related methods

  • Player-related methods

  • Drawing the world



Loading Floor Information


The floor image is a single GIF, so additional information must state where the odd and even tile rows are located and give the dimensions for a tile (a diamond). These details are shown in Figure 13-9.



Figure 13-9. Floor information



The relevant information is stored in worldInfo.txt in the World/ subdirectory and read in by loadWorldInfo( ). The file contains the following:



// name of the GIF (surface.gif) holding the floor image
image surface

// number of tiles (x,y)
numTiles 16 23

// pixel dimensions of a single tile (width, height)
dimTile 56 29

// 'start of first even row' (x,y) coordinate
evenRow 12 8

// 'start of first odd row' (x,y) coordinate
oddRow 40 23



Lines beginning with // are comments.



The image used is surface.gif, which should be in the Images/ subdirectory below the AlienTiles/ directory. There are 16 columns of tiles, and 23 rows. Each tile is 56 pixels wide, at its widest point, and 29 pixels high. The first even row (row 0) starts at pixel coordinate (12,8), the first odd row (row 1) at (40,23). The starting point is taken to be the top-left corner of the rectangle that surrounds the diamond. With this information, translating any tile coordinate into a pixel location in the floor image is possible.



Storing floor data

The data read in by loadFloorInfo( ) and its secondary methods are stored in a series of globals in WorldDisplay:



// world size in number of tiles
private int numXTiles, numYTiles;

// max pixel width/height of a tile
private int tileWidth, tileHeight;

// 'start of first even row' coordinate
private int evenRowX, evenRowY;

// 'start of first odd row' coordinate
private int oddRowX, oddRowY;



Most of them are used only to initialize the WorldItems object:



WorldItems wItems = new WorldItems(tileWidth, tileHeight,
evenRowX, evenRowY, oddRowX, oddRowY);



The WorldItems object organizes details about the surface entities (blocks, pickups, and sprites) by tile row to ensure they are drawn to the JPanel with the correct z-ordering. The floor information is required so an entity's tile coordinates can be translated to pixel locations.




Creating obstacles

The number of tiles on the surface is used to initialize the obstacles[][] array:



private void initObstacles( )
// initially there are no obstacles in the world
{
obstacles = new boolean[numXTiles][numYTiles];
for(int i=0; i < numXTiles; i++)
for(int j=0; j < numYTiles; j++)
obstacles[i][j] = false;
}



Obstacles are registered (i.e., particular cells are set to true) as WorldDisplay loads entity information (see the next section for details).


Sprites utilize validTileLoc( ) to check if a particular tile (x, y) can be entered:



public boolean validTileLoc(int x, int y)
// Is tile coord (x,y) on the tile map and not contain an obstacle?
{
if ((x < 0) || (x >= numXTiles) || (y < 0) || (y >= numYTiles))
return false;
if (obstacles[x][y])
return false;
return true;
}






Loading World Entity Information


Rather than specify the entity positions as constants in the code, the information is read in by loadWorldObjects( ) from the file worldObjs.txt in the subdirectory World/.


The data come in three flavorsno-go areas, blocks, and pickupsplaced at a given tile coordinate and unable to move. Sprites aren't included in this category since their position can change during game play. Consequently, worldObjs.txt supports three data formats:



// no-go coordinates
n <x1>-<y1> <x2>-<y2> .....
.... #

// block coordinates for blockName
b <blockName>
<x1>-<y1> <x2>-<y2> .....
.... #

// pickup coordinate for pickupName
p <pickupName> <x>-<y>



An n is for no-go, followed by multiple lines of (x, y) coordinates defining which tiles are inaccessible. The sequence of coordinates is terminated with a #. A b line starts with a block name, which corresponds to the name of the GIF file for the block, and is followed by a sequence of tile coordinates where the block appears. The name on a p line is mapped to a GIF file name but is followed only by a single coordinate. A pickup is assumed to only appear once on the floor.


The GIFs referenced in this file should be in the subdirectory Images/ below the AlienTiles/ directory.



Here is a fragment of worldObjs.txt:



// bottom right danger zone (red in the GIF)
n 12-13 12-14 13-14 12-15 #

// blocks
b column1
9-3 7-7 7-18 #

b pyramid
1-12 5-16 #

b statue
14-13 #

// pickups
p cup 1-8



A quick examination of the Images/ subdirectory will show the presence of column1.gif, pyramid.gif, statue.gif, and cup.gif.



As the information is parsed by loadWorldObjects( ) and its helper methods, the obstacles[][] array and the WorldItems objects are passed through the entity details. For instance, in getsBlocksLine( ), the following code fragment is executed when a (x, y) coordinate for a block has been found:



wItems.addItem( blockName+blocksCounter, BLOCK, coord.x, coord.y, im);
obstacles[coord.x][coord.y] = true;



addItem( ) adds information about the block to the WorldItems object. The relevant obstacles[][] cell is set to true.


Similar code is executed for a pickup in getPickup( ):



wItems.addItem( pickupName, PICKUP, coord.x, coord.y, pickupIm);
numPickups++;



The obstacles[][] array is not modified since a sprite must be able to move to a tile occupied by a pickup (so it can pick it up). BLOCK, PICKUP, and SPRITE are constants used by WorldItems to distinguish between tile entities.




Pickup Methods


WorldDisplay offers a range of pickup-related methods used by the sprites. For example, the PlayerSprite object calls removePickup( ) to pick up a named item:



public void removePickup(String name)
{ if (wItems.removePickup(name)) { // try to remove it
numPickups;
if (numPickups == 0) // player has picked up everything
atPanel.gameOver( );
}
else
System.out.println("Cannot delete unknown pickup: " + name);
}



WorldDisplay communicates with its WorldItems object to attempt the removal and decrements of its numPickups counter. If the counter reaches 0, then the player has collected all the pickups and AlienTilesPanel (atPanel) can be told the game is over.




Player Methods


The player sprite and the aliens don't communicate directly; instead, their interaction is handled through WorldDisplay. This allows code in WorldDisplay the potential to modify, add, or delete information. For example, WorldDisplay might not pass the player's exact position to the aliens, thereby making it harder for them to find him. This version of the application doesn't change or limit information transfer, but that sort of behavior could be introduced without much difficulty.


One of the more complicated player methods is playerHasMoved( ) called by the PlayerSprite object when it moves to a new tile.



public void playerHasMoved(Point newPt, int moveQuad)
{
for(int i=0; i < aliens.length; i++)
aliens[i].playerHasMoved(newPt); // tell the aliens
updateOffsets(moveQuad); // update world's offset
}



The player passes in a Point object holding its new tile coordinate, as well as the quadrant direction that brought the sprite to the tile. The moveQuad value can be the constant NE, SE, SW, NW, or STILL, which correspond to the four possible compass directions that a sprite can use, plus the no-movement state.


The new tile location is passed to the aliens, which can use it to modify their intended destination. The quadrant direction is passed to updateOffsets( ) to change the surface image's offset from the enclosing JPanel.


As mentioned earlier, the player sprite doesn't move at all. A careful examination of AlienTiles during execution shows that the sprite always stays at the center of the game's JPanel. The floor image and its contents (blocks, pickups, aliens) move instead. For instance, when the player sprite is instructed to move northwest (the quadrant direction NW), the sprite does nothing, but the floor and its contents shifts southeast.


The floor offset is maintained in two globals:



private int xOffset = 0;
private int yOffset = 0;



xOffset and yOffset hold the pixel offsets for drawing the top-left corner of the floor image (and its contents) relative to the top-left corner (0,0) of the JPanel, as shown in Figure 13-10. The offsets may have negative values.


The offsets are the final part of the mapping required to translate a tile coordinate into an on-screen pixel location.


This approach means that a stationary block or pickup, always positioned on the same tile, will be drawn at different places inside the JPanel as the xOffset and yOffset values change.


The offsets are adjusted by updateOffsets( ):



private void updateOffsets(int moveQuad)
{
if (moveQuad == TiledSprite.SW) { // offset to NE
xOffset += tileWidth/2;
yOffset -= tileHeight/2;
}




Figure 13-10. The floor offset from the JPanel




else if (moveQuad == TiledSprite.NW) { // offset to SE
xOffset += tileWidth/2;
yOffset += tileHeight/2;
}
else if (moveQuad == TiledSprite.NE) { // offset to SW
xOffset -= tileWidth/2;
yOffset += tileHeight/2;
}
else if (moveQuad == TiledSprite.SE) { // offset to NW
xOffset -= tileWidth/2;
yOffset -= tileHeight/2;
}
else if (moveQuad == TiledSprite.STILL) { // do nothing
}
else
System.out.println("moveQuad error detected");
}





Drawing the World


AlienTilesPanel delegates the world drawing task to draw( ) in WorldDisplay:



public void draw(Graphics g)
{
g.drawImage(floorIm, xOffset, yOffset, null); // draw floor image
wItems.positionSprites(player, aliens); // add the sprites
wItems.draw(g, xOffset, yOffset); // draw entities
wItems.removeSprites( ); // remove sprites
}



WorldDisplay draws the floor GIF, suitably offset, but the entities resting on the floor (the blocks, pickups, and sprites) are left to WorldItems to render.


During WorldDisplay's loading phase, the WorldItems object is initialized with the locations of the blocks and pickups, but not sprites. The reason is that sprites move about at run time, so they would have to be reordered repeatedly in WorldItems' internal data structures.


Instead, whenever the game surface needs to be drawn, the sprites' current positions are recorded temporarily in WorldItems by calling positionSprites( ). After the drawing is completed, the sprite data are deleted with removeSprites( ).


This approach simplifies the housekeeping tasks carried out by WorldItems, as you'll soon see. A drawback to this approach, though, is the need for repeated insertions and deletions of sprite information. However, there are only five sprites in AlienTiles, so the overhead isn't excessive.


If the number of sprites were considerably larger, then you might have to rethink this approach, as the cost of adding and removing the sprites would become significant. The data structures used by WorldItems would need to be made more sophisticated, so moveable items could be permanently stored there and found quickly.











    No comments: