Chapter 16. Managing Your Sprites

IN THIS CHAPTER

This chapter shows you how to create a sprite manager. The SpriteManager class is an important part of any game, acting as a container in which to add sprites. This makes it easy to retrieve, manipulate, move, and draw groups of sprites.

This chapter will also cover advanced sprite techniques, such as retrieving images from the network, and creating more advanced collision detection to make games more accurate and realistic.

Networked Game Components

A game does not only consist of the code. Rather, the game code is usually just an engine that loads up all sorts of other data and manipulates it.

Because most mobile devices place severe limitations on the size of JAR files, it makes a whole lot of sense to retrieve game components separately, over the network. In fact, this might be part of your game's business model: You might give away the first level, but then charge players to download further levels. Each level could come with its own sounds and audio.

A typical game includes the following:

  • Images—Different mobile devices can handle different types of images. Some devices have large color screens. Others only have tiny black and white displays. By retrieving your files over the network, you can grab optimized images that are appropriate for the device being used.

  • Sounds—Audio files are currently not supported by MIDP. Some extension profiles, however, such as the Siemens Game API and the DoCoMo iAppli API support audio files. Eventually, it might even be possible to stream MP3 files over the network. For more information, check out Chapter 22, “iAppli: Micro Java with a Twist,” and Chapter 23, “Siemens Game API.”

  • Levels, missions, or tracks—Some mobile devices such as the Siemens SL45i store applications on multimedia memory cards. This is a great plus for users, because they can store a few thousand applications on each card. However, this portability has a downside for the game developer. A user can easily upload a commercial game from a memory card onto a personal computer, then illegally distribute the game to other users. To solve this problem, you can distribute your game without any levels. Each level must then be downloaded over the network separately. Each download can be monitored and charged.

The process of downloading any of these data types is similar:

  1. Connect to a server.

  2. Download the image, audio, or game level as a byte array.

  3. Convert it into the format your game requires.

Downloading Images

Listing 16.1 shows how to load images over a network. Because every MIDP phone supports the HTTP protocol, simply place your images on any public Web server. When your application starts, it connects to the server and grabs the needed images.

Example 16.1. Loading Images from Afar

public static Image carImage;
static
{
  try
  {
    carImage = loadImage(
        "http://www.foo.com/images/car.png");
  }  catch (Exception ex) {}
}

public static Image loadImage(String url)
{
  byte buffer[] = null;
  try
  {
    HttpConnection conn = (HttpConnection)
        Connector.open(url);
    try
    {
      int length = (int)conn.getLength();
      buffer = new byte[length];
      DataInputStream in = conn.openDataInputStream();
      in.read(buffer);
      in.close();
      return Image.createImage(buffer, 0,
          buffer.length);
    }
    finally
    {
      conn.close();
    }
  }
  catch (Exception ex) {}
  return null;
}

The loadImage() method is relatively simple: An HTTP connection to a Web server is created. The server returns the length of the image. A byte array of that length is then created, and the image is downloaded into the array. This array is then passed into the Image.createImage() method, and a new Java image is returned.

More information about the Connector class can be found in Chapter 20, “Connecting Out: Wireless Networking.”

Downloading Other Media Types

Grabbing other media types can work in a similar fashion. Pretty much every API allows you to construct sounds, images, or other objects using a simple byte array.

When you create your game components, be sure to write a game engine or level editor that can input and output byte arrays as necessary. Ideally, your entire game state should be able to be compressed into a byte array.

Additionally, your images and other multimedia can be saved onto the device's storage memory for future games. Chapter 19, “Be Persistent: MIDP Data Storage,” shows you how to achieve this.

Advanced Collision Detection

In the previous chapter we talked about a way of detecting sprite collision using overlapping rectangles. This type of collision detection is very inaccurate. For instance, if the transparent corners of a ball sprite touch the corners of another ball sprite, your game will think they have collided. The player, however, will see two balls move past each other without touching. Luckily, there are several advanced detection techniques that can be used to eliminate this problem:

  • Collision detection with multiple levels—A sprite is divided into different areas, called levels. The largest area is called the root level, and has no parent. Every other level can contain other levels, and also lies within a parent level. This enables you to create various zones within your sprite. For example, you will be able to tell when a missile hits the side, edge, or center of an enemy barracks. Or, if you just need to know whether the sprite was hit at all, you merely look at the parent level.

  • Collision detection with multiple areas—A nonrectangular sprite is divided into different rectangular parts. Detection within each of these small rectangles occurs separately.

  • Bitmasked collision detection—This involves two images: The original sprite image and a sprite mask. The mask image is a two-colored (black and white) bitmap, wherein the white color represents the presence of the sprite, and the black color its absence. This approach is the most accurate, but also the slowest.

Each of the above solutions will be discussed in the following sections.

Solution 1: Multiple Levels

Figure 16.1 shows how the image of a ball can be separated into two levels. The larger rectangle surrounds the entire ball. The smaller rectangle, which is a child of the larger rectangle, denotes the center of the ball. When a collision occurs, the game is told which level was hit.

Multiple levels of collision.

Figure 16.1. Multiple levels of collision.

Each level is represented by a quadruplet that holds the upper-left coordinate (x, y) of the area, as well as the width and height. In Listing 16.2 the quadruplets are hard-coded, but they could just as easily have been provided as parameters in the Sprite's constructor.

Example 16.2. Creating Collision Levels

protected int areas[][] = {{0, 0, 10, 14} ,
    {3, 2, 4, 11}};

public int collide(Sprite sprite)
{
  for (int i = 0; i < areas.length; i++)
  {
    if ((areas[i][0] + areas[i][2]) > sprite.getX() &&
        areas[i][0] < sprite.getX() &&
        (areas[i][1] + areas[i][3]) > sprite.getY() &&
        areas[i][1] < sprite.getY())
            return i;
  }
  return -1;
}

The code presumes that levels in the array are presented in order from the outer area to the inner one. The collide() method tests whether a given sprite has collided with a specific area. The method returns an integer value that represents the level number. The root level has a value 0. If there is no collision, the value -1 is returned. This can let you know whether a sprite has hit the outer edge of another sprite, or the direct center. You can then react accordingly.

The example shows how basic area detection can occur. In reality, you would need to check each area of one sprite against each area of another sprite. If one sprite has m areas, and the other one has n areas, the game would need to make m×n comparisons for one collision. That can slow down your game considerably.

Solution 2: Multiple Areas

Any digital nonrectangular image can be partitioned into a finite number of rectangular parts. For example, the circle in Figure 16.2 has been divided into three rectangles. If your image has any extra pixels along its edges, they can be divided into a rectangle that is only 1 pixel square.

Multiple areas of collision.

Figure 16.2. Multiple areas of collision.

When your program tries to detect whether two sprites have collided, it can check every rectangle of one sprite against every rectangle of another. This type of collision detection is 100% accurate. However, the more rectangles your sprite is made up of, the more checking iterations there are that must be performed.

Area collision detection can be implemented similarly to level detection. Simply create an array of quadruplets (x, y, width and height), as in Listing 16.3.

Example 16.3. Creating Collision Areas

protected int areas[][] = {{2, 1, 6, 3} , {3, 4, 4, 5} ,
    {0, 9, 10, 14}};

public boolean collide(Sprite sprite)
{
  for (int i = 0; i < areas.length; i++)
  {
    if ((areas[i][0] + areas[i][2]) > sprite.getX() &&
        areas[i][0] < sprite.getX() &&
        (areas[i][1] + areas[i][3]) > sprite.getY() &&
        areas[i][1] < sprite.getY())
             return true;
  }
  return false;
}

The collide() method returns true if at least one of the parts collide with another sprite.

When you actually build your game, you should experiment with various types of collision detection and various numbers of areas. Because current Java devices are quite slow, you might want to limit your sprites to one or two areas at most.

The Sprite Manager

A sprite manager's main jobs are to manage a list of different sprites and to provide the capability to manipulate those sprites.

When defining the manager, the following methods should be implemented:

  • addSprite(Sprite sprite)—. Adds a new sprite into the manager at the end of the list.

  • insertSprite(Sprite sprite, int position)—. Adds a new sprite into the manager at the given position.

  • getSpritePosition(Sprite sprite)—. Returns the sprite's position number associated to the given sprite. The first sprite is at the position 0. The value –1 is returned if the sprite was not found.

  • getSprite(int index)—. Returns the sprite associated with the given index.

  • deleteSprite(Sprite sprite)—. Deletes the given sprite from the manager.

  • deleteSprite(int position)—. Deletes a sprite from the given position from the manager.

  • paint(Graphics g)—. Draws all the sprites onto the given Graphics object. The sprites are drawn in order from 0 to getLength()-1.

  • size()—. Returns the number of sprites in the manager.

Listing 16.4 shows the implementation of the SpriteManager class, using all the preceding methods.

Example 16.4. The SpriteManager Class

import java.util.*;
import javax.microedition.lcdui.*;

public class SpriteManager
{
  private Vector list;
  public SpriteManager()
  {
    list = new Vector();
  }

  public void addSprite(Sprite sprite)
  {
    list.addElement(sprite);
  }

  public void insertSprite(Sprite sprite, int position)
  {
    list.insertElementAt(sprite, position);
  }

  public int getSpritePosition(Sprite sprite)
  {
    for (int i = 0; i < list.size(); i++)
    {
      Sprite comparableSprite = (Sprite)list.elementAt(i);
      if (sprite.equals(comparableSprite))
          return i;
    }
    return -1;
  }

  public Sprite getSprite(int index)
  {
    return (Sprite)list.elementAt(index);
  }

  public void deleteSprite(Sprite sprite)
  {
    deleteSprite(getSpritePosition(sprite));
  }

  public void deleteSprite(int position)
  {
    list.removeElementAt(position);
  }

  public int size()
  {
    return list.size();
  }

  public void paint(Graphics g)
  {
    for (int i = 0; i < list.size(); i++)
    {
      Sprite sprite = (Sprite)list.elementAt(i);
      sprite.paint(g);
    }
  }
}

The manager uses a Vector object for storing sprites. If sprites need to have unique IDs, Vector could be replaced with Hashtable. Your game can use several managers to hold each group of sprites. For example, you might want to organize things so that you have one manager for opponent vehicles, one for obstacles, one for background elements, and so on.

Sprites are always added at the end of the list. All searches are linear, and are fast enough for small amount of sprites. For larger games, you can implement more advanced search mechanisms such as sorting via binary trees. The paint() method simply draws all the sprites in the list in order as they're put in the list.

Drawing Optimizations

Look closely at the paint() method in Listing 16.4. The SpriteManager class is drawing sprite images, even if they are located beyond the bounds of the current screen. This will slow down the game, because a huge number of unnecessary actions are invoked. Your game might have hundreds of sprites in memory, but only one or two drawn on the screen at any given time. Instead, painting should be done as in Listing 16.5.

Example 16.5. Improved Painting

import java.util.*;
import javax.microedition.lcdui.*;
public class SpriteManager
{
  private Vector list;
  private int width;
  private int height;

  public SpriteManager(int width, int height)
  {
    this.width = width;
    this.height = height;
    list = new Vector();
  }

  public void paint(Graphics g)
  {
    for (int i = 0; i < list.size(); i++)
    {
      Sprite sprite = (Sprite)list.elementAt(i);
      if ((sprite.getX() + sprite.getWidth() > 0) &&
          (sprite.getX() < width) &&
          (sprite.getY() + sprite.getHeight() > 0) &&
          (sprite.getY() < height))
          sprite.paint(g);
    }
  }
  ...
}

The sprite manager stores the values of the screen's width and height. Both values are very important, because they represent boundaries. The paint() method walks through the manager's sprite list and checks whether each sprite lies inside the screen.

Enhancing Sprite Collision

To apply the sprite manager to our Micro Racer game, you can create one SpriteManager group for all the enemy cars. You can then figure out whether your player sprite has collided with an enemy by creating collision detection within the SpriteManager class, as illustrated in Listing 16.6.

Example 16.6. Adding Collision Detection

public boolean collide(Sprite sprite)
{
  for (int i = 0; i < list.size(); i++)
  {
    Sprite comparableSprite = (Sprite)list.elementAt(i);
    if (sprite.collide(comparableSprite))
        return true;
  }
  return false;
}

The collide() method checks whether the player sprite (which should be passed in as a parameter) has collided with any of the enemy sprites. If there is a collision, the method returns true.

Moving through the list with a larger number of sprites can significantly slow down the game. One possible optimization would be to put the sprites in a correct order within the list, and only check the sprites that are on the screen. The ones that are offscreen could never collide with the player sprite.

Summary

Now we're getting someplace! We have a fully functional game with a hero, a group of enemies, and a means of figuring out whether any fender-benders have occurred. However, the game is still pretty simplistic. After all, none of our sprites are actually moving yet! The next chapter finally makes sprites spritely, showing you how to actually animate and move them around within a game loop.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.166.31