Creating a Tile-Based Map

In a 2D platform game, the map of an entire level, or the game map, is usually several screens wide. Some maps are 20 screens wide; others are 100 or more. As the main character walks across the screen, the map scrolls. As you can imagine, using one huge image for the game map is not the best idea in this situation. This would take up so much memory that most machines wouldn't be capable of loading the map. Plus, using a huge image doesn't help define which parts of the map the player can and cannot walk through—in other words, which parts are "solid" and which are "empty." Instead of using a huge image for the entire map, you'll create a tile-based map. Tile-based maps break down the map into a grid, as shown in Screenshot. Each cell in the grid contains either a small tile image or nothing.

Screenshot A tile-based map is composed of several image tiles on a grid.

Java graphics 05fig02.gif


Tile-based maps are like creating a game with building blocks. Only a few different block colors are used, but you have an unlimited amount of each color. The tile map contains references to which image belongs to each cell in the grid. This way, you need only a few small images for the tiles, and you can make the maps as big as you want without worrying too much about memory constraints. How big the tiles are is up to you. Most tile-based games use a tile size that is a power of 2, such as 16 or 32. In this game, you'll use a tile size of 64. Screenshot shows the nine tiles used in the game.

Screenshot This tile-based game has nine different tiles.

Java graphics 05fig03.gif


Tile-based maps also have the nice side effect of being able to easily determine what's "solid" and what's "empty" in a map. That way, you know which part of the map the player can jump on, and you can make sure the player can't magically walk through walls. You will use this later in this chapter, in the "Collision Detection" section. But first, let's implement the tile map.

Implementing the Tile Map

The TileMap class in Listing 5.1 contains the tile map you'll use in the game. It holds a two-dimensional array of Image objects that define the tile map. Empty tiles are null. Keep in mind that each entry in the Image array isn't a unique object; it's just a reference to an existing object. If one tile is in the map 12 times, the same Image object will be in the array 12 times. Object references are only 4 bytes on a 32-bit Java VM, so a one-dimensional 5,000-Image array takes up only about 20KB. (Actually, because Java doesn't really have two-dimensional arrays and has only arrays of arrays, a "two-dimensional" version adds up to about 30KB. Either way, it's a very small amount.)

Listing 5.1 TileMap.java
package com.brackeen.javagamebook.tilegame;
import java.awt.Image;
import java.util.LinkedList;
import java.util.Iterator;
import com.brackeen.javagamebook.graphics.Sprite;
/**
 The TileMap class contains the data for a tile-based
 map, including Sprites. Each tile is a reference to an
 Image. Of course, Images are used multiple times in the tile
 map.
*/
public class TileMap {
 private Image[][] tiles;
 private LinkedList sprites;
 private Sprite player;
 /**
 Creates a new TileMap with the specified width and
 height (in number of tiles) of the map.
 */
 public TileMap(int width, int height) {
 tiles = new Image[width][height];
 sprites = new LinkedList();
 }
 /**
 Gets the width of this TileMap (number of tiles across).
 */
 public int getWidth() {
 return tiles.length;
 }
 /**
 Gets the height of this TileMap (number of tiles down).
 */
 public int getHeight() {
 return tiles[0].length;
 }
 /**
 Gets the tile at the specified location. Returns null if
 no tile is at the location or if the location is out of
 bounds.
 */
 public Image getTile(int x, int y) {
 if (x < 0 || x >= getWidth() ||
 y < 0 || y >= getHeight())
 {
 return null;
 }
 else {
 return tiles[x][y];
 }
 }
 /**
 Sets the tile at the specified location.
 */
 public void setTile(int x, int y, Image tile) {
 tiles[x][y] = tile;
 }
 /**
 Gets the player Sprite.
 */
 public Sprite getPlayer() {
 return player;
 }
 /**
 Sets the player Sprite.
 */
 public void setPlayer(Sprite player) {
 this.player = player;
 }
 /**
 Adds a Sprite object to this map.
 */
 public void addSprite(Sprite sprite) {
 sprites.add(sprite);
 }
 /**
 Removes a Sprite object from this map.
 */
 public void removeSprite(Sprite sprite) {
 sprites.remove(sprite);
 }
 /**
 Gets an Iterator of all the Sprites in this map,
 excluding the player Sprite.
 */
 public Iterator getSprites() {
 return sprites.iterator();
 }
}


Besides the tiles, the TileMap contains the sprites in the game. Sprites can be anywhere on the map, not just on tile boundaries. See Screenshot for a list of the sprites in the game.

Screenshot Besides the player, other sprites include two bad guys and three power-ups.

Java graphics 05fig04.gif


The TileMap class also treats the player as a special sprite because you usually want to treat it quite differently from the rest of the sprites.

Loading Tile Maps

Now that you have a place to store the tile map, you need to come up with a reasonable way to actually create the map. Tile-based games always have more than one map or level, and this game is no exception. You'll want an easy way to create multiple maps so that when the player finishes one map, the player can then start the next map. You could create maps by calling TileMap's addTile() and addSprite() methods for every tile and sprite in the game. As you can imagine, this technique isn't very flexible. It makes editing levels far too difficult, and the code itself would not be very pretty to look at. Many tile-based games have their own map editors for creating maps. These tile-based editors enable you to visually add tiles and sprites to the game, and are quick and easy to use. They usually store maps in an intermediate map file that the game can parse. Creating a map editor is a bit of overkill in this case. Instead, you'll just define a text-based map file format that can be edited in an everyday text editor. Because tile maps are defined on a grid, each character in the text file will be either a tile or a sprite, as shown in the example in Listing 5.2.

Listing 5.2 map.txt
# Map file for tile-based game
# (Lines that start with '#' are comments)
# The tiles are:
# (Space) Empty tile
# A..Z Tiles A through Z
# o Star
# ! Music Note
# * Goal
# 1 Bad Guy 1 (grub)
# 2 Bad Guy 2 (fly)
AD IIIIIII IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII AD o o o I E AD IIIIIII I C AD I o o C AD 2 ! 2 IIIII C AD IIIII C AD III 1 1 1 2 C AD 1 IIIIIIIIIIIII C AD 1 EBBBBBBBBBBBBBBBF o o o o o o * C AHBBBBBBBBBBBBBBGAAAAAAAAAAAAAAAHBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBG


This map file is both visually understandable and easy to edit. Lines that start with # are comments, and all other lines define a row of tiles. The size of the map isn't fixed, so you can make maps bigger by adding more lines or making the lines longer. Parsing the map file is easy and basically takes three steps:

  1. Read every line, ignoring commented lines, and put each line in a list.

  2. Create a TileMap object. The width of the TileMap is the length of the longest line in the list, and the height is the number of lines in the list.

  3. Parse each character in each line, and add the appropriate tile or sprite to the map, depending on that character.

If a character is encountered that is "illegal," the tile is considered to be empty. Here's all the parsing code, wrapped up in a loadMap() method in Listing 5.3.

Listing 5.3 loadMap() Method
private TileMap loadMap(String filename) throws IOException {
 ArrayList lines = new ArrayList();
 int width = 0;
 int height = 0;
 // read every line in the text file into the list
 BufferedReader reader = new BufferedReader(
 new FileReader(filename));
 while (true) {
 String line = reader.readLine();
 // no more lines to read
 if (line == null) {
 reader.close();
 break;
 }
 // add every line except for comments
 if (!line.startsWith("#")) {
 lines.add(line);
 width = Math.max(width, line.length());
 }
 }
 // parse the lines to create a TileEngine
 height = lines.size();
 TileMap newMap = new TileMap(width, height);
 for (int y=0; y<height; y++) {
 String line = (String)lines.get(y);
 for (int x=0; x<line.length(); x++) {
 char ch = line.charAt(x);
 // check if the char represents tile A, B, C, etc.
 int tile = ch - 'A';
 if (tile >= 0 && tile < tiles.size()) {
 newMap.setTile(x, y, (Image)tiles.get(tile));
 }
 // check if the char represents a sprite
 else if (ch == 'o') {
 addSprite(newMap, coinSprite, x, y);
 }
 else if (ch == '!') {
 addSprite(newMap, musicSprite, x, y);
 }
 else if (ch == '*') {
 addSprite(newMap, goalSprite, x, y);
 }
 else if (ch == '1') {
 addSprite(newMap, grubSprite, x, y);
 }
 else if (ch == '2') {
 addSprite(newMap, flySprite, x, y);
 }
 }
 }
 // add the player to the map
 Sprite player = (Sprite)playerSprite.clone();
 player.setX(TileMapRenderer.tilesToPixels(3));
 player.setY(0);
 newMap.setPlayer(player);
 return newMap;
}


One thing to note is the special case of adding sprites to the TileMap. For starters, you need to create a different Sprite object for each sprite in the game. To do this, you can clone sprites from a "host" sprite. Second, a sprite might not necessarily be the same size as the tile size. So, in this case, you'll center and bottom-justify each sprite in the tile it's in. All this is taken care of in the addSprite() method in Listing 5.4.

Listing 5.4 addSprite() Method
private void addSprite(TileMap map,
 Sprite hostSprite, int tileX, int tileY)
{
 if (hostSprite != null) {
 // clone the sprite from the "host"
 Sprite sprite = (Sprite)hostSprite.clone();
 // center the sprite
 sprite.setX(
 TileMapRenderer.tilesToPixels(tileX) +
 (TileMapRenderer.tilesToPixels(1) -
 sprite.getWidth()) / 2);
 // bottom-justify the sprite
 sprite.setY(
 TileMapRenderer.tilesToPixels(tileY + 1) -
 sprite.getHeight());
 // add it to the map
 map.addSprite(sprite);
 }
}


In earlier demos in the tutorial, the sprite's position was relative to the screen, but in this game, the sprite's position is relative to the tile map. You can use the TileMapRender.tilesToPixels() static function to convert tile positions to pixel positions in the map. This function multiplies the number of tiles by the tile size:

int pixelSize = numTiles * TILE_SIZE;


This way, sprites can move around to any position in the map and don't have to be justified with the tile boundaries. That's about it. Now you have a flexible way to create maps and parse them to create TileMap objects. In this game, all the maps live in the map folder and are named map1.txt, map2.txt, and so on. Whenever you advance to the next map, the code just looks for the next map file; if it's not found, the code starts over with the first map file. This means you can just drop new map files in the map folder without having to tell the game how many maps exist.

Drawing Tile Maps

As mentioned before, the tile maps are much larger than the screen, so only a portion of the map is shown on the screen at a time. As the player moves, the map scrolls to keep the player in the middle of the screen, as shown in Screenshot.

Screenshot Only a small portion of the tile map is onscreen at a time, and the map scrolls to keep the player in the middle of the screen.

Java graphics 05fig05.gif


Therefore, before you draw the tiles, you need to figure out the position of the map onscreen. Start off by keeping the player in the middle of the screen, like this:

int offsetX = screenWidth / 2 -
 Math.round(player.getX()) - TILE_SIZE;


This formula assigns offsetX the horizontal position of the map onscreen. It's an easy formula, but you also need to make sure that when the player is near the far left or far right edges of the map, the scrolling stops so that the "void" beyond the edges of the map isn't shown. To do this, give the offsetX value a limit:

int mapWidth = tilesToPixels(map.getWidth());
offsetX = Math.min(offsetX, 0);
offsetX = Math.max(offsetX, screenWidth - mapWidth);


For convenience, also create the offsetY variable for the vertical scroll position. It keeps the map flush with the bottom of the screen, no matter how big the screen is:

int offsetY = screenHeight - tilesToPixels(map.getHeight());


Now you're ready to draw the tiles. You could just draw every single tile in the map, but instead, you just need to draw the visible tiles. Here, you get the first horizontal tile to draw based on offsetX and then calculate the last horizontal tile to draw based on the width of the screen. Then you draw the visible tiles:

int firstTileX = pixelsToTiles(-offsetX);
int lastTileX = firstTileX +
 pixelsToTiles(screenWidth) + 1;
for (int y=0; y<map.getHeight(); y++) {
 for (int x=firstTileX; x <= lastTileX; x++) {
 Image image = map.getTile(x, y);
 if (image != null) {
 g.drawImage(image,
 tilesToPixels(x) + offsetX,
 tilesToPixels(y) + offsetY,
 null);
 }
 }
}


That's it! The tile map is now drawing on the screen and scrolling smoothly wherever the player goes. But it would probably help if you actually draw the player, right? You'll do that next—and while you're at it, you'll draw all the other sprites, too.

Drawing Sprites

After drawing the tiles, you'll draw the sprites. Because you drew only the visible tiles, let's look into only drawing the visible sprites as well. Here are a few ideas:

  • Partition the sprites into screen-size sections. Draw only the sprites that are in the sections visible on the screen. As the sprites move, make sure they are stored in the appropriate section.
  • Keep the sprites in a list ordered by the sprites' horizontal position, from left to right. Keep track of the first visible sprite in the list, which can change as bad guys die or the screen scrolls. Draw sprites in the list from the first visible sprite until one is found that is not visible. As the sprites move, make sure the list is sorted.
  • Implement the brute-force method of running through every sprite in the list, checking whether it's visible.

The first two ideas no doubt are useful if there are a lot of sprites in the map. However, in this case, there are not very many sprites in each map, so you can use the brute-force method and check to see if each sprite is visible. Actually, you can just run through the list, drawing every sprite:

Iterator i = map.getSprites();
while (i.hasNext()) {
 Sprite sprite = (Sprite)i.next();
 int x = Math.round(sprite.getX()) + offsetX;
 int y = Math.round(sprite.getY()) + offsetY;
 g.drawImage(sprite.getImage(), x, y, null);
}


This way, the graphics engine does the work, checking to see whether each image is visible before drawing it. It's not the most efficient solution, but it works.

Parallax Scrolling

Now that you've got the tiles and sprites drawn, you just need to draw one more thing: the background. When you draw the background, you need to decide how the background is drawn in comparison to the map. Here are a few ways to do this:

  • Keep the background static so it doesn't scroll when the map scrolls.
  • Scroll the background at the same rate as the map.
  • Scroll the background at a slower rate than the map so the background appears to be farther away.

The third method is called parallax scrolling and is the method you'll use for the game. Parallax is the apparent change in position of an object when seen from a different point of view. For example, when you're driving in a car and look out the side window, the nearby objects, such as traffic signs, fly by rather quickly, but farther objects, such as mountains, slowly creep by. The farther away an object is, the less it appears to move as you move. If you make the background move slower than the map, it will appear farther away and will give the game a bit of perspective. As mentioned before, you don't want to use a huge image for the entire map. Likewise, you don't want to use a huge image for the entire background of the map. Because you're using parallax scrolling, you don't need to. You'll create a background image that is two screens wide, and it will scroll from the first screen to the second screen as the map scrolls from the left to the right. This is the key to implementing parallax scrolling in the game. When the player is on the left part of the map, the leftmost part of the map is shown and the left part of the background is drawn (see Screenshot).

Screenshot When the player is on the left part of the map, the leftmost part of the background is drawn.

Java graphics 05fig06.gif


Drawing the left part of the background on the screen is easy enough:

int backgroundX = 0;
g.drawImage(background, backgroundX, 0, null);


Likewise, when the player is on the far right side of the map, the rightmost part of the background is shown (see Screenshot).

Screenshot When the player is on the right part of the map, the rightmost part of the background is drawn.

Java graphics 05fig07.gif


Here's how you draw the right part of the background on the screen:

int backgroundX = screenWidth - background.getWidth(null);
g.drawImage(background, backgroundX, 0, null);


Now, what about all the points in between the left and right parts of the map? Well, previously you calculated offsetX, which is the position of the map that the screen is drawing, so you just need a formula to convert offsetX to backgroundX. The range for offsetX is from 0 (left part of map drawn) to screenWidthmapWidth (right part of map drawn). This matches with the range of backgroundX from 0 to screenWidthbackground.getWidth(null). So, all you have to do interpolate these two discrete points:

int backgroundX = offsetX *
 (screenWidth - background.getWidth(null)) /
 (screenWidth - mapWidth);
g.drawImage(background, backgroundX, 0, null);


That was more simple than you thought, huh? With this formula, the background scrolls slowly as the player moves across the map. This formula assumes two things, however:

  • The background is wider than the screen.
  • The map is wider than the background.

One last thing to note is that you don't have to use an image as a background. You could just as easily use another TileMap so that you could be free to create as large a background as you want. Also, there doesn't have to be just one scrolling background. There could be two or more, each scrolling at different speeds, with the front layers having transparency so the back layers show through. The background image itself could also be tiled. If you do this, make sure the right edge of the background matches up with the left edges, so the background appears seamless.

Power-Ups

Now that you have a formula for drawing the background, you can draw everything the game requires. The next step is to implement the sprites. The first type of sprite you'll implement is the power-up. A power-up is a sprite that the player can pick up, often giving the player points, extra powers, or the ability to perform some other action. The PowerUp class in Listing 5.5 defines the power-ups. It is an abstract class that extends PowerUp, but it contains static inner subclasses for each power-up in the game: the star, the music note, and the goal.

Listing 5.5 PowerUp.java
package com.brackeen.javagamebook.tilegame.sprites;
import java.lang.reflect.Constructor;
import com.brackeen.javagamebook.graphics.*;
/**
 A PowerUp class is a Sprite that the player can pick up.
*/
public abstract class PowerUp extends Sprite {
 public PowerUp(Animation anim) {
 super(anim);
 }
 public Object clone() {
 // use reflection to create the correct subclass
 Constructor constructor = getClass().getConstructors()[0];
 try {
 return constructor.newInstance(
 new Object[] {(Animation)anim.clone()});
 }
 catch (Exception ex) {
 // should never happen
 ex.printStackTrace();
 return null;
 }
 }
 /**
 A Star PowerUp. Gives the player points.
 */
 public static class Star extends PowerUp {
 public Star(Animation anim) {
 super(anim);
 }
 }
 /**
 A Music PowerUp. Changes the game music.
 */
 public static class Music extends PowerUp {
 public Music(Animation anim) {
 super(anim);
 }
 }
 /**
 A Goal PowerUp. Advances to the next map.
 */
 public static class Goal extends PowerUp {
 public Goal(Animation anim) {
 super(anim);
 }
 }
}


One of the things you need to implement in all the sprites in the game is the clone() method, which is a way to make many copies of the same sprite. Instead of implementing a clone() method for every subclass of PowerUp, the PowerUp class contains a generic clone() method that uses reflection to clone the object, even if it is a subclass. It selects the first constructor of the object's class and then creates a new instance of that object using that constructor. Notice that subclasses of PowerUp don't really do anything. They're merely placeholders so that you can tell which power-up is which. But what are they supposed to do?

  • When the player acquires a star, a sound is played, but no other action is taken.
  • When the player acquires a music note, the drum track in the background MIDI music is toggled on or off.
  • Finally, when the player acquires the "goal" power-up, the next map is loaded.

All of the power-up actions take place in the collision-detection code (which we discuss later in this chapter). Whenever the player collides with a PowerUp, the acquirePowerUp() method is called to determine what to do with it:

public void acquirePowerUp(PowerUp powerUp) {
 // remove it from the map
 map.removeSprite(powerUp);
 if (powerUp instanceof PowerUp.Star) {
 // do something here, like give the player points
 soundManager.play(prizeSound);
 }
 else if (powerUp instanceof PowerUp.Music) {
 // change the music
 soundManager.play(prizeSound);
 toggleDrumPlayback();
 }
 else if (powerUp instanceof PowerUp.Goal) {
 // advance to next map
 soundManager.play(prizeSound,
 new EchoFilter(2000, .7f), false);
 map = resourceManager.loadNextMap();
 }
}


NOTE Note that this is only one way to implement power-ups. Another way is to provide an acquire() method in the PowerUp class that is called when the player acquires the power-up. This way, each power-up class takes care of its own actions. However, the way this game is designed, the PowerUp object does not have access to things such as the sound manager or the map; the PowerUp objects do not perform any actions.


Simple Baddies

A game wouldn't be any fun without bad guys, right? Hopefully, they don't have to be smart baddies because the two baddies in this game, the fly and the grub, aren't. All they do is move forward until they bump into a wall. Then they turn around and move forward again. And they do it over and over.

Animations

Before you implement the baddies, let's talk about their animations. You might notice, way back in Screenshot, that the player and the two baddies are facing only left. Instead of creating more PNG files for the players facing right, you can just make mirror images of the left-facing images. Back in , "2D Graphics and Animation," you used the AffineTransform class to mirror an image whenever it is drawn. This time, you'll use the AffineTransform class to create mirror images on startup, saving them to another image. While you're at it, you'll create code to make horizontally mirrored, or flipped, images as well, as shown in Listing 5.6.

Listing 5.6 Creating Transformed Images
public Image getMirrorImage(Image image) {
 return getScaledImage(image, -1, 1);
}
public Image getFlippedImage(Image image) {
 return getScaledImage(image, 1, -1);
}
private Image getScaledImage(Image image, float x, float y) {
 // set up the transform
 AffineTransform transform = new AffineTransform();
 transform.scale(x, y);
 transform.translate(
 (x-1) * image.getWidth(null) / 2,
 (y-1) * image.getHeight(null) / 2);
 // create a transparent (not translucent) image
 Image newImage = gc.createCompatibleImage(
 image.getWidth(null),
 image.getHeight(null),
 Transparency.BITMASK);
 // draw the transformed image
 Graphics2D g = (Graphics2D)newImage.getGraphics();
 g.drawImage(image, transform, null);
 g.dispose();
 return newImage;
}


Now you can make sprites for when the player and baddies are moving to the right. Another sprite you might want is the one that shows a "dead" baddie. Again, you could create more PNG images for this, but instead, just use a flipped, or upside-down, version of the image to represent a dead baddie. That's where the getFlippedImage() method comes in.

Creature Class

Each baddie in the game can have four different animations:

  • Moving left
  • Moving right
  • Dead, facing left
  • Dead, facing right

To accommodate this, you need a new type of sprite object that can change its underlying animation whenever it changes direction or dies. The Creature class, in Listing 5.7, accommodates this. Besides the four different animations, it keeps track of the state of the player, which is either STATE_NORMAL, STATE_DYING, or STATE_DEAD. It takes one second for a Creature to move from the "dying" state to the "dead" state.

Listing 5.7 Creature.java
package com.brackeen.javagamebook.tilegame.sprites;
import java.lang.reflect.Constructor;
import com.brackeen.javagamebook.graphics.*;
/**
 A Creature is a Sprite that is affected by gravity and can
 die. It has four Animations: moving left, moving right,
 dying on the left, and dying on the right.
*/
public abstract class Creature extends Sprite {
 /**
 Amount of time to go from STATE_DYING to STATE_DEAD.
 */
 private static final int DIE_TIME = 1000;
 public static final int STATE_NORMAL = 0;
 public static final int STATE_DYING = 1;
 public static final int STATE_DEAD = 2;
 private Animation left;
 private Animation right;
 private Animation deadLeft;
 private Animation deadRight;
 private int state;
 private long stateTime;
 /**
 Creates a new Creature with the specified Animations.
 */
 public Creature(Animation left, Animation right,
 Animation deadLeft, Animation deadRight)
 {
 super(right);
 this.left = left;
 this.right = right;
 this.deadLeft = deadLeft;
 this.deadRight = deadRight;
 state = STATE_NORMAL;
 }
 public Object clone() {
 // use reflection to create the correct subclass
 Constructor constructor = getClass().getConstructors()[0];
 try {
 return constructor.newInstance(new Object[] {
 (Animation)left.clone(),
 (Animation)right.clone(),
 (Animation)deadLeft.clone(),
 (Animation)deadRight.clone()
 });
 }
 catch (Exception ex) {
 // should never happen
 ex.printStackTrace();
 return null;
 }
 }
 /**
 Gets the maximum speed of this Creature.
 */
 public float getMaxSpeed() {
 return 0;
 }
 /**
 Wakes up the creature when the Creature first appears
 on screen. Normally, the creature starts moving left.
 */
 public void wakeUp() {
 if (getState() == STATE_NORMAL && getVelocityX() == 0) {
 setVelocityX(-getMaxSpeed());
 }
 }
 /**
 Gets the state of this Creature. The state is either
 STATE_NORMAL, STATE_DYING, or STATE_DEAD.
 */
 public int getState() {
 return state;
 }
 /**
 Sets the state of this Creature to STATE_NORMAL,
 STATE_DYING, or STATE_DEAD.
 */
 public void setState(int state) {
 if (this.state != state) {
 this.state = state;
 stateTime = 0;
 if (state == STATE_DYING) {
 setVelocityX(0);
 setVelocityY(0);
 }
 }
 }
 /**
 Checks if this creature is alive.
 */
 public boolean isAlive() {
 return (state == STATE_NORMAL);
 }
 /**
 Checks if this creature is flying.
 */
 public boolean isFlying() {
 return false;
 }
 /**
 Called before update() if the creature collided with a
 tile horizontally.
 */
 public void collideHorizontal() {
 setVelocityX(-getVelocityX());
 }
 /**
 Called before update() if the creature collided with a
 tile vertically.
 */
 public void collideVertical() {
 setVelocityY(0);
 }
 /**
 Updates the animation for this creature.
 */
 public void update(long elapsedTime) {
 // select the correct Animation
 Animation newAnim = anim;
 if (getVelocityX() < 0) {
 newAnim = left;
 }
 else if (getVelocityX() > 0) {
 newAnim = right;
 }
 if (state == STATE_DYING && newAnim == left) {
 newAnim = deadLeft;
 }
 else if (state == STATE_DYING && newAnim == right) {
 newAnim = deadRight;
 }
 // update the Animation
 if (anim != newAnim) {
 anim = newAnim;
 anim.start();
 }
 else {
 anim.update(elapsedTime);
 }
 // update to "dead" state
 stateTime += elapsedTime;
 if (state == STATE_DYING && stateTime >= DIE_TIME) {
 setState(STATE_DEAD);
 }
 }
}


The Creature class contains several methods to add functionality to the Sprite class:

  • The wakeUp() method can be called when the baddie first appears on screen. In this case, it calls setVelocityX(-getMaxSpeed()) to start the baddie moving, so baddies don't move until you first see them.
  • The isAlive() and isFlying() methods are convenience methods to check the state of the baddie. Baddies that aren't alive don't hurt the player, and gravity doesn't apply to baddies that are flying.
  • Finally, the methods collideVertical() and collideHorizontal() are called when the baddie collides with a tile. In the case of a vertical collision, the vertical velocity of the baddie is set to 0. In the case of a horizontal collision, the baddie simply changes direction. That is the extent of the baddies' intelligence.

You'll notice that Creature is an abstract class, so you'll need to subclass it to get any use out of it. The first subclass you'll create is the Grub in Listing 5.8.

Listing 5.8 Grub.java
package com.brackeen.javagamebook.tilegame.sprites;
import com.brackeen.javagamebook.graphics.Animation;
/**
 A Grub is a Creature that moves slowly on the ground.
*/
public class Grub extends Creature {
 public Grub(Animation left, Animation right,
 Animation deadLeft, Animation deadRight)
 {
 super(left, right, deadLeft, deadRight);
 }
 public float getMaxSpeed() {
 return 0.05f;
 }
}


The code in the Grub class is very difficult to understand, so try to follow along. Okay, it's not difficult! The only thing it does is override the getMaxSpeed() method to set the speed of the grub to its slow pace. The next baddie you'll create is the fly, in Listing 5.9.

Listing 5.9 Fly.java
package com.brackeen.javagamebook.tilegame.sprites;
import com.brackeen.javagamebook.graphics.Animation;
/**
 A Fly is a Creature that flies slowly in the air.
*/
public class Fly extends Creature {
 public Fly(Animation left, Animation right,
 Animation deadLeft, Animation deadRight)
 {
 super(left, right, deadLeft, deadRight);
 }
 public float getMaxSpeed() {
 return 0.2f;
 }
 public boolean isFlying() {
 return isAlive();
 }
}


Just like the Grub class, Fly is a subclass of Creature. Besides defining the speed of the fly, the isFlying() method is overridden to return true as long as the fly is alive. That way, gravity applies to the fly only when it is dying or dead. There's just one more subclass of Creature: the Player class in Listing 5.10. Although the player isn't a baddie, it needs the features of Creature, such as multiple animations and states.

Listing 5.10 Player.java
package com.brackeen.javagamebook.tilegame.sprites;
import com.brackeen.javagamebook.graphics.Animation;
/**
 The Player.
*/
public class Player extends Creature {
 private static final float JUMP_SPEED = -.95f;
 private boolean onGround;
 public Player(Animation left, Animation right,
 Animation deadLeft, Animation deadRight)
 {
 super(left, right, deadLeft, deadRight);
 }
 public void collideHorizontal() {
 setVelocityX(0);
 }
 public void collideVertical() {
 // check if collided with ground
 if (getVelocityY() > 0) {
 onGround = true;
 }
 setVelocityY(0);
 }
 public void setY(float y) {
 // check if falling
 if (Math.round(y) > Math.round(getY())) {
 onGround = false;
 }
 super.setY(y);
 }
 public void wakeUp() {
 // do nothing
 }
 /**
 Makes the player jump if the player is on the ground or
 if forceJump is true.
 */
 public void jump(boolean forceJump) {
 if (onGround || forceJump) {
 onGround = false;
 setVelocityY(JUMP_SPEED);
 }
 }
 public float getMaxSpeed() {
 return 0.5f;
 }
}


Basically, the only feature Player adds to Creature is the ability to jump. Most of time, you don't want the player to be able jump if the player is not on the ground. The methods setY() and collideVertical() are overridden to keep track of whether the player is on the ground. If the player is on the ground, the player can jump. Alternatively, you can force a player to jump in midair by calling jump(true).

Screenshot


   
Comments