Managing the BricksBricksManager is separated into five broad groups of methods:
BricksManager reads a bricks map and creates a Brick object for each brick. The data structure holding the Brick objects is optimized so drawing and collision detection can be carried out quickly. Moving and drawing the bricks map is analogous to the moving and drawing of an image by a Ribbon object. However, the drawing process is complicated by the ribbon consisting of multiple bricks instead of a single GIF. Jack, the JumperSprite object, uses BricksManager methods to determine if its planned moves will cause it to collide with a brick. Loading Bricks InformationBricksManager calls loadBricksFile( ) to load a bricks map; the map is assumed to be in bricksInfo.txt from Images/. The first line of the file (ignoring comment lines) is the name of the image strip: s tiles.gif 5 This means that tiles.gif holds a strip of five images. The map is a series of lines containing numbers and spaces. Each line corresponds to a row of tiles in the game. A number refers to a particular image in the image strip, which becomes a tile. A space means that no tile is used in that position in the game.
bricksInfo.txt is: // bricks information s tiles.gif 5 // ----------- 44444 222222222 111 2222 11111 444 444 22222 444 111 1111112222222 23333 2 33 44444444 00 000111333333000000222222233333 333 2222222223333301 00000000011100000000002220000000003300000111111222222234 // ----------- The images strip in tiles.gif is shown in Figure 12-13. Figure 12-13. The images strip in tiles.gifThe images strip is loaded with an ImagesLoader object, and an array of BufferedImages is stored in a global variable called brickImages[]. This approach has several drawbacks. One is the reliance on single digits to index into the images strip. This makes it impossible to utilize strips with more than 10 images (images can only be named from 0 to 9), which is inadequate for a real map. The solution probably entails moving to a letter-based scheme (using A-Z and/or a-z) to allow up to 52 tiles. loadBricksFile( ) calls storeBricks( ) to read in a single map line, adding Brick objects to a bricksList ArrayList: private void storeBricks(String line, int lineNo, int numImages) { int imageID; for(int x=0; x < line.length( ); x++) { char ch = line.charAt(x); if (ch == ' ') // ignore a space continue; if (Character.isDigit(ch)) { imageID = ch - '0'; // Assume a digit is 0-9 if (imageID >= numImages) System.out.println("Image ID "+imageID+" out of range"); else // make a Brick object bricksList.add( new Brick(imageID, x, lineNo) ); } else System.out.println("Brick char " + ch + " is not a digit"); } } A Brick object is initialized with its image ID (a number in the range 0 to 9); a reference to the actual image is added later. The brick is passed its map indices (x, lineNo). lineNo starts at 0 when the first map line is read and is incremented with each new line. Figure 12-14 shows some of the important variables associated with a map, including example map indices. Initializing the Bricks Data StructuresOnce the bricksList ArrayList has been filled, BricksManager calls initBricksInfo( ) to extract various global data from the list and to check if certain criteria are met. For instance, the maximum width of the map should be greater than the width of the Figure 12-14. Brick map variablespanel (width pWidth). initBricksInfo( ) calls checkForGaps( ) to check that no gaps are in the maps bottom row. The presence of a gap would allow Jack to fall down a hole while running around, which would necessitate more complex coding in JumperSprite. If checkForGaps( ) finds a gap, the game terminates after reporting the error. The bricksList ArrayList doesn't store its Brick objects in order, which makes finding a particular Brick time-consuming. Unfortunately, searching for a brick is a common task and must be performed every time that Jack is about to move to prevent it from hitting something. A more useful way of storing the bricks map is ordered by column, as illustrated in Figure 12-15. Figure 12-15. Bricks stored by columnThis data structure is excellent for brick searches where the column of interest is known beforehand since the array allows constant-time access to a given column. A column is implemented as an ArrayList of Bricks in no particular order, so a linear search looks for a brick in the selected column. However, a column contains few bricks compared to the entire map, so the search time is acceptable. Since no gaps are in the bottom row of the map, each column must contain at least one brick, guaranteeing that none of the column ArrayLists in columnBricks[] is null. The columnBricks[] array is built by BricksManager calling createColumns( ). Moving the Bricks MapThe BricksManager uses the same approach to moving its bricks map as the Ribbon class does for its GIF. The isMovingRight and isMovingLeft flags determine the direction of movement for the bricks map (or if it is stationary) when its JPanel position is updated. The flags are set by the moveRight( ), moveLeft( ), and stayStill( ) methods: public void moveRight( ) { isMovingRight = true; isMovingLeft = false; } update( ) increments an xMapHead value depending on the movement flags. xMapHead is the x-coordinate in the panel where the left edge of the bricks map (its head) should be drawn. xMapHead can range between -width to width (where width is the width of the bricks map in pixels): public void update( ) { if (isMovingRight) xMapHead = (xMapHead + moveSize) % width; else if (isMovingLeft) xMapHead = (xMapHead - moveSize) % width; } Drawing the BricksThe display( ) method does the hard work of deciding where the bricks in the map should be drawn in the JPanel. As in the Ribbon class, several different coordinate systems are combined: the JPanel coordinates and the bricks map coordinates. The bad news is that the bricks map uses two different schemes. One way of locating a brick is by its pixel position in the bricks map; the other is by using its map indices (see Figure 12-14). This means that three coordinate systems are utilized in display( ) and its helper method drawBricks( ): public void display(Graphics g) { int bCoord = (int)(xMapHead/imWidth) * imWidth; // bCoord is the drawing x-coord of the brick containing xMapHead int offset; // offset is distance between bCoord and xMapHead if (bCoord >= 0) offset = xMapHead - bCoord; // offset is positive else // negative position offset = bCoord - xMapHead; // offset is positive if ((bCoord >= 0) && (bCoord < pWidth)) { drawBricks(g, 0-(imWidth-offset), xMapHead, width-bCoord-imWidth); // bm tail drawBricks(g, xMapHead, pWidth, 0); // bm start } else if (bCoord >= pWidth) drawBricks(g, 0-(imWidth-offset), pWidth, width-bCoord-imWidth); // bm tail else if ((bCoord < 0) && (bCoord >= pWidth-width+imWidth)) drawBricks(g, 0-offset, pWidth, -bCoord); // bm tail else if (bCoord < pWidth-width+imWidth) { drawBricks(g, 0-offset, width+xMapHead, -bCoord); // bm tail drawBricks(g, width+xMapHead, pWidth, 0); // bm start } } // end of display( ) The details of drawBricks( ) will be explained later in the chapter. For now, it's enough to know the meaning of its prototype: void drawBricks(Graphics g, int xStart, int xEnd, int xBrick); drawBricks( ) draws bricks into the JPanel starting at xStart, ending at xEnd. The bricks are drawn a column at a time. The first column of bricks is the one at the xBrick pixel x-coordinate in the bricks map. display( ) starts by calculating a brick coordinate (bCoord) and offset from the xMapHead position. These are used in the calls to drawBricks( ) to specify where a brick image's left edge should appear. This should become clearer as you consider the four drawing cases. Case 1. Bricks map moving right and bCoord is less than pWidthThis is the relevant code snippet in display( ): if ((bCoord >= 0) && (bCoord < pWidth)) { drawBricks(g, 0-(imWidth-offset), xMapHead, width-bCoord-imWidth); // bm tail drawBricks(g, xMapHead, pWidth, 0); // bm start } // bm means bricks map Figure 12-16 illustrates the drawing operations: Case 1 occurs as the bricks map moves right since the sprite is apparently moving left. xMapHead will have a value between 0 and pWidth (the width of the JPanel). Two groups of bricks will need to be drawn, requiring two calls to drawBricks( ). The first group starts near the left edge of the JPanel, and the second starts at the xMapHead position. I've indicated these groups by drawing the bricks map area occupied by the left group in gray in Figure 12-16 and the righthand group's area with stripes. The positioning of the bricks in the gray area of the bricks map in Figure 12-16 poses a problem. The drawing of a column of bricks requires the x-coordinate of the column's Figure 12-16. Case 1 in BricksManager's display( )left edge. What is that coordinate for the first column drawn in the gray area of the bricks map? The left edge of that column will usually not line up with the left edge of the panel, most likely occurring somewhere to its left and off screen. The required calculation (width-bCoord-imWidth) is shown in Figure 12-16, next to the leftmost arrow at the bottom of the figure. The drawing of a group of bricks is packaged up in drawBricks( ). The second and third arguments of that method are the start and end x-coordinates for a group in the JPanel. These are represented by arrows pointing to the JPanel box at the top of Figure 12-16. The fourth argument is the x-coordinate of the left column of the group in the bricks map. These coordinates are represented by the arrows at the bottom of Figure 12-16. drawBricks( ) is called twice in the code snippet shown earlier: once for the group in the lefthand gray area of the bricks map in Figure 12-16, and once for the group in the righthand striped area. Case 2. Bricks map moving right and bCoord is greater than pWidthHere's the code piece: if (bCoord >= pWidth) drawBricks(g, 0-(imWidth-offset), pWidth, width-bCoord-imWidth); // bm tail Figure 12-17 shows the operation. Figure 12-17. Case 2 in BricksManager's display( )Case 2 happens some time after Case 1, when xMapHead has moved farther right, beyond the right edge of the JPanel. The drawing task becomes simpler since only a single call to drawBricks( ) is required to draw a group of columns taken from the middle of the bricks map. I've indicated that group's area in gray in the bricks map in Figure 12-17. Case 2 has the same problem as Case 1 in determining the x-coordinate of the left column of the gray group in the bricks map. The value is shown next to the leftmost bottom arrow in Figure 12-17. Case 3. Bricks map moving left and bCoord is greater than (pWidth-width+imWidth)The relevant code fragment is shown here: if ((bCoord < 0) && (bCoord >= pWidth-width+imWidth)) drawBricks(g, 0-offset, pWidth, -bCoord); // bm tail Figure 12-18 illustrates the drawing operation. Case 3 applies when the bricks map is moving left, as the sprite is apparently traveling to the right. xMapHead goes negative, as does bCoord, but the calculated offset is adjusted to be positive. Until bCoord drops below (pWidth-width+imWidth), the bricks map will only require one drawBricks( ) call to fill the JPanel. Figure 12-18. Case 3 in BricksManager's display( )Case 4. Bricks map moving left and bCoord is less than (pWidth-width+ imWidth)Here's the code: if (bCoord < pWidth-width+imWidth) { drawBricks(g, 0-offset, width+xMapHead, -bCoord); // bm tail drawBricks(g, width+xMapHead, pWidth, 0); // bm start } Figure 12-19 shows the operations. Case 4 occurs after xMapHead has moved to the left of (pWidth-width+imWidth). Two drawBricks( ) calls are needed to render two groups of columns to the JPanel. The group's areas are shown in solid gray and striped in the bricks map in Figure 12-19. The drawBricks( ) methoddrawBricks( ) draws bricks into the JPanel between xStart and xEnd. The bricks are drawn a column at a time, separated by imWidth pixels. The first column of bricks drawn is the one at the xBrick pixel x-coordinate in the bricks map: private void drawBricks(Graphics g, int xStart, int xEnd, int xBrick) { int xMap = xBrick/imWidth; // get column position of the brick // in the bricks map ArrayList column; Brick b; for (int x = xStart; x < xEnd; x += imWidth) { column = columnBricks[ xMap ]; // get the current column for (int i=0; i < column.size( ); i++) { // draw all bricks Figure 12-19. Case 4 in BricksManager's display( ) b = (Brick) column.get(i); b.display(g, x); // draw brick b at JPanel posn x } xMap++; // examine the next column of bricks } } drawBricks( ) converts the xBrick value, a pixel x-coordinate in the bricks map, into a map x index. This index is the column position of the brick, so the entire column can be accessed immediately in columnBricks[]. The bricks in the column are drawn by calling the display( ) method for each brick. Only the JPanel's x-coordinate is passed to display( ) with the y-coordinate stored in the Brick object. This is possible since a brick's y-axis position never changes as the bricks map is moved horizontally over the JPanel. JumperSprite-Related MethodsThe BricksManager has several public methods used by JumperSprite to determine or check its position in the bricks map. The prototypes of these methods are: int findFloor(int xSprite); boolean insideBrick(int xWorld, int yWorld); int checkBrickBase(int xWorld, int yWorld, int step); int checkBrickTop(int xWorld, int yWorld, int step); Finding the floorWhen Jack is added to the scene, his x-coordinate is in the middle of the JPanel, but what should his y-coordinate be? His feet should be placed on the top-most brick at or near the given x-coordinate. findFloor( ) searches for this brick, returning its y-coordinate: public int findFloor(int xSprite) { int xMap = (int)(xSprite/imWidth); // x map index int locY = pHeight; // starting y pos (largest possible) ArrayList column = columnBricks[ xMap ]; Brick b; for (int i=0; i < column.size( ); i++) { b = (Brick) column.get(i); if (b.getLocY( ) < locY) locY = b.getLocY( ); // reduce locY (i.e., move up) } return locY; } Matters are simplified by the timing of the call: findFloor( ) is invoked before the sprite has moved and, therefore, before the bricks map has moved. Consequently, the sprite's x-coordinate in the JPanel (xSprite) is the same x-coordinate in the bricks map. xSprite is converted to a map x index to permit the relevant column of bricks to be accessed in columnBricks[]. Testing for brick collisionJumperSprite implements collision detection by calculating its new position after a proposed move and by testing if that point (xWorld, yWorld) is inside a brick. If it is, then the move is aborted and the sprite stops moving. The point testing is done by BricksManager's insideBrick( ), which uses worldToMap( ) to convert the sprite's coordinate to a brick map index tuple: public boolean insideBrick(int xWorld, int yWorld) // Check if the world coord is inside a brick { Point mapCoord = worldToMap(xWorld, yWorld); ArrayList column = columnBricks[ mapCoord.x ]; Brick b; for (int i=0; i < column.size( ); i++) { b = (Brick) column.get(i); if (mapCoord.y == b.getMapY( )) return true; } return false; } // end of insideBrick( ) worldToMap( ) returns a Point object holding the x and y map indices corresponding to (xWorld, yWorld). The relevant brick column in columnBricks[] can then be searched for a brick at the y map position. The conversion carried out by worldToMap( ) can be understood by referring to Figure 12-14. Here's the code: private Point worldToMap(int xWorld, int yWorld) // convert world coord (x,y) to a map index tuple { xWorld = xWorld % width; // limit to range (width to -width) if (xWorld < 0) // make positive xWorld += width; int mapX = (int) (xWorld/imWidth); // map x-index yWorld = yWorld - (pHeight-height); // relative to map int mapY = (int) (yWorld/imHeight); // map y-index if (yWorld < 0) // above the top of the bricks mapY = mapY-1; // match to next 'row' up return new Point(mapX, mapY); } xWorld can be any positive or negative value, so it must be restricted to the range (0 to width), which is the extent of the bricks map. The coordinate is then converted to a map a index. The yWorld value uses the JPanel's coordinate system, so it is made relative to the y-origin of the bricks map (some distance down from the top of the JPanel). The conversion to a map y index must take into account the possibility that the sprite's position is above the top of the bricks map. This can occur by having the sprite jump upward while standing on a platform at the top of the bricks map. Jumping and hitting your headWhen Jack jumps, his progress upward will be halted if he is about to pass through the base of a brick. The concept is illustrated in Figure 12-20. The sprite hopes to move upward by a step amount, but this will cause it to enter the brick. Instead, it will travel upward by a smaller step, step-(imHeight-topOffset), placing its top edge next to the bottom edge of the brick. checkBrickBase( ) is supplied with the planned new position (xWorld, yWorld)labeled as (x, y) in Figure 12-20and the step. It returns the step distance that the sprite can move without passing into a brick: public int checkBrickBase(int xWorld, int yWorld, int step) { if (insideBrick(xWorld, yWorld)) { int yMapWorld = yWorld - (pHeight-height); int mapY = (int) (yMapWorld/imHeight); // map y- index int topOffset = yMapWorld - (mapY * imHeight); return (step - (imHeight-topOffset)); // a smaller step } return step; // no change } Figure 12-20. A rising sprite hitting a brickFalling and sinking into the groundAs a sprite descends, during a jump or after walking off the edge of a raised platform, it must test its next position to ensure that it doesn't pass through a brick on its way down. When a brick is detected beneath the sprite's feet, the descent is stopped, ensuring that the Jack lands on top of the brick. Figure 12-21 illustrates the calculation. The sprite moves downward by a step amount on each update, but when a collision is detected, the step size is reduced to step-topOffset so it comes to rest on top of the brick: public int checkBrickTop(int xWorld, int yWorld, int step) { if (insideBrick(xWorld, yWorld)) { int yMapWorld = yWorld - (pHeight-height); int mapY = (int) (yMapWorld/imHeight); // map y- index int topOffset = yMapWorld - (mapY * imHeight); return (step - topOffset); // a smaller step } return step; // no change } The intended new position for the sprite (xWorld, yWorld) is passed to checkBrickTop( ), along with the step size. The returned value is the step the sprite should take to avoid sinking into a brick. Figure 12-21. A falling sprite hitting a brick |