Table of ContentsSprite BasicsConclusion

Advanced Sprites

Sprites are one of the most important and time-consuming parts of MID game development. Organizing images, extracting frames, setting up animation timing, and then drawing the sprites on the screen are all routine tasks when you are putting together a game. Star Assault is no different. For a simple game, there is a surprising number of sprites.

In the next section you'll look at more advanced sprite techniques and tools, such as using image sets, handling multiple state sprites, and doing proper animation.

States Versus Frames

Using arrays of images for all your sprite frames works quite well, but imagine if your ship had many different animations, such as one for when the guns fire, a custom explosion animation, and maybe even an after-burner jet. These all require different sequences of frames, possibly of different sizes, each with their own animation speeds. Each of these animations represents a different state the ship sprite can have.

Another good example of a sprite having multiple states is the MR-313 high-impact radial plasma shells your ship fires. (Okay, we'll just call them bullets from now on....) These bullets have two quite distinct statesflying, in which they cycle through a little sparkle animation, and exploding, which is a short animation played when the bullets hits something. You can see the frames for these two states in Figure 9.8.

Figure 9.8. The two sets of image frames we use for bullets corresponding to the flying and exploding states.

graphic/09fig08.gif


Coding sprites with multiple states, each with distinct animations, quickly becomes a confusing mess if you're dealing with a large number of sprites (or even a small number sometimes). Therefore, you should encapsulate the many states of a sprite into a single image set. You can think of this as a set of sprite states.

NOTE

Tip

The more sophisticated your sprites become, the more important states will get. If you were coding a game such as Diablo, for example, you could use a separate state for the walking, attacking, and blocking animation sequences used by the player character. Each of these states can have different image sequences, frame sizes, and animation speeds.

Creating Image Sets

To encapsulate the concept of image sets, you can use the rather appropriately named ImageSet class. The primary purpose of this class is to manage the array of frame images (including details on the frame width and height), as well as the speed at which the frames for that state should animate. There is, however, another important reason why you need this class. This might be something you've wondered already: Why have ImageSet classes at all; why not just have a Sprite class? What more is there to a sprite than states and animation?

There's a good reason why you separate image sets from sprites. Unfortunately, I can't remember what it was at the moment, so let's move on to something else and you can just trust me, okay?

Had you going for a second there! All right, to understand why you separate image sets from sprites, I'd like you to think about the underlying images. MIDs have such a limited amount of memory that you obviously can't afford to have a set of images for every copy of a sprite. Take a look at the bullets as an example. In Star Assault, the ships can all fire bullets. This can quickly result in many bullets flying around during big fights, which in turn requires many bullet objects. Although you need to have each bullet animate independently of all the other bullet objects, you don't want to require a copy of all the image frames as well. It would be a disastrous waste of memory!

The ImageSet class, therefore, acts as a container for the various arrays of images required for each state. A Sprite class, on the other hand, contains all the data for a particular instance of a sprite, such as the state it's in, the current frame, and how long before it should switch to the next frame in the animation. As you can see in Figure 9.9, the three bullet sprite instances share this same ImageSet. This allows you to have only a single copy of the images in memory, yet still have independent sprites utilizing these graphics.

Figure 9.9. The separation of images from sprites lets you share graphics among sprites, even if they are of different types.

graphic/09fig09.gif


All right, take a look at the complete ImageSet class. Thankfully there's nothing too complicated in there.

import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.Graphics;
import java.io.IOException;

/**
 * A container for sets of image frames; typically sprites. A single set
 * is made up of one or more states. Each state represents an animation sequence
 * including Image objects for the frames, animation timing and frame
 * dimensions. An example use of this class would be to animate a little dude.
 * If he had two states of existence, standing (which has short breathing
 * animation) and walking (which has a much longer animation), this would be
 * implemented by creating an ImageSet object and then adding two states, each
 * with their own Image array for all the animation frames (use the static
 * methods at the end of this class to load a clipped file image and then
 * extract the image frame array from it). You can then use a Sprite class
 * associated with this ImageSet to draw the character to the screen as well as
 * keep track of animation frames.
 * @author Martin J. Wells
 */
public class ImageSet
{
   private int totalStates;             // incremented by addState method only
   private Image[][] stateFrames;
   private int[] stateAnimTime, stateFrameWidth, stateFrameHeight;

   /**
    * Constructor for a new image. You will need to call addState to add the
    * various states (and their associated images and animation data) before
    * the object is really usable.
    * @param numStates The initial number of states (since ImageSet uses an
    * array internally (for speed) it costs a lot to expand the internal size
    * beyond this number -- typically you'll know the exact number of states
    * beforehand anyway. If not, the addState code will expand the array when
    * required.
    */
   public ImageSet(int numStates)
   {
      stateAnimTime = new int[numStates];
      stateFrameWidth = new int[numStates];
      stateFrameHeight = new int[numStates]; 
      stateFrames = new Image[numStates][];
   }

   /**
    * Adds a new state to the ImageSet including an array of images and the
    * number of milliseconds to animate the entire state (NOT each frame). If
    * this is not an animating set then just use 0 for the animtime. To animate
    * you will need to create a Sprite object linked to this ImageSet.
    * @param frames An array of javax.microedition.lcdui.Image objects.
    * @param animTime The number of milliseconds delay to animate ALL frames.
    */
   public final void addState(Image frames[], int animTime)
   {
      int state = totalStates++;

      if (state >= stateFrames.length)
      {
         // expand the number of states
         stateAnimTime = Tools.expandArray(stateAnimTime, 1);
         stateFrameWidth = Tools.expandArray(stateFrameWidth, 1);
         stateFrameHeight = Tools.expandArray(stateFrameHeight, 1);
         stateFrames = Tools.expandArray(stateFrames, 1);
      }

      stateAnimTime[state] = animTime;
      stateFrameWidth[state] = frames[0].getWidth();
      stateFrameHeight[state] = frames[0].getHeight();
      stateFrames[state] = frames;
   }

   /**
    * Gets the number of frames for a particular state in the ImageSet.
    * @param state The state you want to know about.
    * @return The number of frames in that state.
    */
   public final int getTotalFrames(int state)
   {
      return stateFrames[state].length;
   }

   /**
    * Gets the total amount time to animate all the frames of a given state. 
    * Note this is not the delay per frame.
    * @param state The state you want to know about.
    * @return The animation delay (in milliseconds) corresponding to the given
    * state.
    */
   public final int getAnimTime(int state)
   {
      return stateAnimTime[state];
   }

   /**
    * Gets the amount of time to spend on each frame of a set to animate it.
    * This is just a convenience method that returns the animation time for
    * a state divided by the total number of frames.
    * @param state The state you want to know about.
    * @return The number of milliseconds delay for each frame of a given state.
    */
   public final int getAnimTimePerFrame(int state)
   {
      return stateAnimTime[state] / stateFrames[state].length;
   }

   /**
    * Draws a specific frame of this sprite onto a graphics context.
    * @param target The target graphics context to draw on.
    * @param state The state corresponding to the frame being drawn.
    * @param frame The number of the frame you want to draw.
    * @param targetX The x-position to draw the frame.
    * @param targetY The y-position to draw the frame.
    */
   public final void draw(Graphics target, int state, int frame, int targetX, int tar
getY)
   {
      if (stateFrames[state][frame] != null)
         target.drawImage(stateFrames[state][frame],
                            targetX, targetY, Tools.GRAPHICS_TOP_LEFT);
   }

   /**
    * Extract an Image corresponding to a particular state and frame.
    * @param state The state you're after.
    * @param frame The frame you're after. 
    * @return The image corresponding to the given frame in the given state.
    */
   public final Image getFrame(int state, int frame)
   {
      return stateFrames[state][frame];
   }

   //
   // STATIC IMAGE TOOLS
   //

   /**
    * Static utility method to load up a portion of an image from a file. Note
    * that the file must be in your JAR (or otherwise accessible) by the MID in
    * order to be loaded.
    * @param filename The name of the resource file to load.
    * @param originX The starting x position of the file image you want to load.
    * @param originY The starting y position of the file image you want to load.
    * @param width The width of the image you want to clip to.
    * @param height The height of the image you want to clip to.
    * @return A new Image object containing only the rectangle of originX,
    * originY, width and depth.
    */
   public final static Image loadClippedImage(String filename, int originX, int originY,
int width,
                                                  int height)
   {
      try
      {
         // load full image from file and create a mutable version
         Image fileImage = Image.createImage(filename);
         // use the getImageRegion method to extract out only the bit we want.
         return getImageRegion(fileImage, originX, originY, width, height);
      }

      catch (IOException ioe)
      {
         System.out.println("can't load file: " + filename);
         return null;
      }
   } 
   /**
    * Static utility method to load up a portion of an image from a file. Note
    * that the file must be in your JAR (or otherwise accessible) by the MID in
    * order to be loaded. The width and height of the portion is assumed to be
    * the size of the file image.
    * @param filename The name of the resource file to load.
    * @param originX The starting x position of the file image you want to load.
    * @param originY The starting y position of the file image you want to load.
    * @return A new Image object containing only the rectangle starting at
    * originX, originY.
    */
   public final static Image loadClippedImage(String filename, int originX, int originY)
   {
      try
      {
         // load full image from file and create a mutable version
         Image fileImage = Image.createImage(filename);

         // shortcut out of here so we can avoid creating another image if they
         // are just using this method to load an image normally (no clipping).
         if (originX == 0 && originY == 0) return fileImage;

         // use the getImageRegion method to extract out only the bit we want.
         return getImageRegion(fileImage, originX, originY, fileImage.getWidth(),
fileImage.getHeight());
      }

      catch (IOException ioe)
      {
         System.out.println("can't load file: " + filename);
         return null;
      }
   }

   /**
    * Extracts a portion of an image using clipping.
    * @param source The source image.
    * @param x The starting x position of the clipping rectangle.
    * @param y The starting y position of the clipping rectangle.
    * @param width The width of the clipping rectangle.
    * @param height The height of the clipping rectangle.
    * @return A new Image object containing only the portion of the image 
    * within the x, y, width, height rectangle.
    */
   public final static Image getImageRegion(Image source, int x, int y, int width, int
height)
   {
      // create a placeholder for our resulting image region
      Image result = Image.createImage(width, height);

      if (x + width > source.getWidth() || y + height > source.getHeight())
         System.out.println("Warning: attempting extract using (" +
                              x + "," + y + "," + width + "," + height + ") when image
is " +
                              "(" + source.getWidth() + "," + source.getHeight() +
")");

      // draw the image, offset by the region starting position
      result.getGraphics().drawImage(source, -x, -y, Tools.GRAPHICS_TOP_LEFT);

      return result;
   }

   /**
    * Gets an array of images by breaking a larger image into smaller frames.
    * @param sourceImage The image to extract the frames from.
    * @param sourceX The starting x position in the source image to use.
    * @param sourceY The starting y position in the source image to use.
    * @param framesWide The number of frames across the source image to extract.
    * @param framesHigh The number of frames down the source image to extract.
    * @param frameWidth The width of each of those frames.
    * @param frameHeight The height of each of those frames.
    * @return An array containing an image for each frame.
    */
   public final static Image[] extractFrames(Image sourceImage, int sourceX,
                                                 int sourceY,
                                                 int framesWide, int framesHigh,
                                                 int frameWidth, int frameHeight)
   {
      // extract all the frames from the source image
      Image[] frames = new Image[framesWide * framesHigh];
      int frameCount = 0;

      for (int fy = 0; fy < framesHigh; fy++) 
         for (int fx = 0; fx < framesWide; fx++)
            frames[frameCount++] =
                     getImageRegion(sourceImage, sourceX + (fx * frameWidth),
                                     sourceY + (fy * frameHeight),
                                     frameWidth, frameHeight);
      return frames;
   }
}

As you can see, the ImageSet class manages a two-dimensional array of images, which includes all the frames for all the states, along with the animation delay time (in milliseconds) and the width and height of the frames in each state.

NOTE

Tip

I recommend you also move the image extraction methods from earlier in this chapter into this class (as statics) because this is their logical home.

Using Image Sets

When you construct an ImageSet, you can specify the number of states it initially has. Then you just call addState for each new state you have, passing in the array of images for the frames and the animation speed. For example, here's the code to construct the bullet ImageSet:

bulletImageSet = new ImageSet(2);
Image sourceImage = ImageSet.loadClippedImage("/general.png", 0, 0)
Image[] flyingFrames = ImageSet.extractFrames(sourceImage, 0, 0, 1, 1, 3, 3);
Image[] explodingFrames = ImageSet.extractFrames(sourceImage, 0, 3, 4, 1, 3, 3);

int flyingState = bulletImageSet.addState(flyingFrames, 200);
int explodingState = bulletImageSet.addState(explodingFrames, 500);

The addState method returns an assigned integer for each of the new states you add. You can use this to later reference these different states. For example, when your bullet object hits something, you can then draw images from the exploding state (rather than flying state) using the ImageSet draw method.

bulletImageSet.draw(graphics, explodingState, 0, 100, 100);

This will draw the first frame of the bullet in the exploding state. To complete this properly, you'd need to add timing code to change the frame according to the animation speed. That's something I'll get to soon.

Okay, now you've seen how to use image sets to create different arrays of images to represent the various states of an object. This all works quite well; however, there's a fair amount of work you'll need to do to animate the frames and keep track of various states. Because all of your graphics-based objects are going to be doing this, you should take a look at bringing this all together in the Sprite class.

Before you get to that, though, you need to take care of a little housekeeping.

The Tools Class

You might have noticed that the previous class used a few utility methods from a mysterious Tools class. This is just a repository for any static utility methods used in your game classes. You'll continue to add little things you need as you go along, so here's the foundation of this class and the methods used by ImageSet:

import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.Graphics;

/**
 * A bunch of static tools.
 * @author Martin J. Wells
 */
public final class Tools
{
   /**
    * Commonly used so we precalc it.
    */
   public static final int GRAPHICS_TOP_LEFT = Graphics.LEFT | Graphics.TOP;

   /**
    * Take an array of existing objects and expand its size by a given number
    * of elements.
    * @param oldArray The array to expand.
    * @param expandBy The number of elements to expand the array by.
    * @return A new array (which is a copy of the original with space for more
    * elements).
    */
   public final static int[] expandArray(int[] oldArray, int expandBy)
   {
      int[] newArray = new int[oldArray.length + expandBy];
      System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
      return newArray;
   } 
   /**
    * Take a 2D array of existing objects and expand its size by a given number
    * of elements.
    * @param oldArray The array to expand.
    * @param expandBy The number of elements to expand the array by.
    * @return A new array (which is a copy of the original with space for more
    * elements).
    */
   public final static Image[][] expandArray(Image[][] oldArray, int expandBy)
   {
      Image[][] newArray = new Image[oldArray.length + expandBy][];
      System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
      return newArray;
   }

}

The two forms of expandArray are used by the addState method of the ImageSet class to increase the size of the array of states if the initial capacity is not large enough. It's not the most efficient of methods, but because you generally know the number of states you have in the first place, you won't be calling it often.

I've also included a GRAPHICS_TOP_LEFT static integer. This is just a pre-calculation of a very commonly used setting to save a little processing time.

The Sprite Class

Image sets give you the frames and animation timing data for a sprite; however, to make the sprite come alive, you need to implement the animation code and keep track of which state and frame you're currently viewing. It would be silly to put all this into each of the game objects (such as Bullet and Ship), and the code doesn't belong in the ImageSet class because it's shared by all instances of the sprite. The logical place for this is, of course, in a Sprite class.

Aside from handling images and states, you only have one major thing left to do (and that's really what the Sprite class is here to care of)animation. Since you already have all your sprite states, frames, and timing data ready to go, it's much easier to tackle animation. All you need to do now is add timing.

You first saw game timing in Chapter 7; in that example, you learned that the basics for timing in RoadRun came from cycling through a loop once for each frame in your game. On each pass you only calculated the total time that had elapsed since the last cycle. All movement (actor cycling) was then factored by this amount of time, resulting in the proper motion of the cars and trucks. You simply moved an amount of space relative to the time that had passed.

Animation is exactly the same. You have a number of frames to cycle through, so all you need to do is step through each frame at a rate equal to the animation speed. For example, if the bullet explosion state had four frames and an animation speed of 500 ms per frame the animation would complete a full explosion cycle in 2000 milliseconds.

The Sprite class's job is to keep track of the current state and frame and then handle the animation based on the associated ImageSet animation timing. Take a look at the code:

import javax.microedition.lcdui.Graphics;

/**
 * A state manager for a game sprite. Use in conjunction with an ImageSet in
 * order to draw animated, multi-state sprites. For each instance of an animated
 * graphic you should create one corresponding Sprite object. Graphics are
 * shared using a common ImageSet. To animate you must call this class's cycle
 * method.
 * @author Martin J. Wells
 */
public class Sprite
{
   private int currentFrame;
   private int currentState;
   private long currentStateBegan;            // time this currentState started
   private ImageSet imageSet;
   private long lastFrameChange;
   private int totalCycles;

   /**
    * Constructor for a Sprite object requiring the source image set and the
    * starting state and frame.
    * @param is The imageSet which is the source of graphics for this Sprite.
    * @param startingState The starting state (normally 0).
    * @param startingFrame The starting frame (normally 0).
    */
   public Sprite(ImageSet is, int startingState, int startingFrame)
   {
      imageSet = is;
      setState(startingState, true);
      currentFrame = startingFrame;
   }

   /**
    * Change to a specific frame.
    * @param f The frame to change to. 
    */
   public final void setFrame(int f)
   {
      currentFrame = f;
   }

   /**
    * Change to a different state.
    * @param s The state to change to.
    * @param force Normally we won't change if that is already the current
    * state. However this has the effect of not resetting the state began time
    * and totalCycles counter. Set this to true to force those to be reset.
    */
   public final void setState(int s, boolean force)
   {
      if (currentState != s || force)
      {
         currentState = s;
         currentFrame = 0;
         totalCycles = 0;
         currentStateBegan = System.currentTimeMillis();
      }
   }

   /**
    * Resets all state information such as the current animation frame, total
    * number of completed cycles and the time the state began.
    */
   public final void reset()
   {
      currentFrame = 0;
      totalCycles = 0;
      currentStateBegan = 0;
      lastFrameChange = 0;
   }

   /**
    * Get the time the last state change occurred.
    * @return Time last state was changed in milliseconds since epoch.
    */
   public final long getWhenStateBegan()
   {
      return currentStateBegan; 
   }

   /**
    * @return The total time spent in the current state.
    */
   public final long getTimeInCurrentState()
   {
      return (System.currentTimeMillis() - currentStateBegan);
   }

   /**
    * @return The current state.
    */
   public final int getCurrentState()
   {
      return currentState;
   }

   /**
    * @return The current frame number.
    */
   public final int getCurrentFrame()
   {
      return currentFrame;
   }

   /**
    * Draws the current sprite frame onto a specified graphics context.
    * @param target The target to draw the image frame onto.
    * @param targetX The target x position.
    * @param targetY The target y position.
    */
   public final void draw(Graphics target, int targetX, int targetY)
   {
      imageSet.draw(target, currentState, currentFrame, targetX, targetY);
   }

   /**
    * Cycles the current sprite's animation and goes forward by the number of
    * frames corresponding to the amount of time that has elapsed.
    * @param deltaMS The amount of time that has passed in milliseconds.
    */
   public final void cycle(long deltaMS) 
   {
      // change frame if we are animating (and enough time has passed)
      if (imageSet.getTotalFrames(currentState) > 1 &&
               imageSet.getAnimTime(currentState) > 0)
      {
         long deltaTime = System.currentTimeMillis() - lastFrameChange;
         if (deltaTime > imageSet.getAnimTimePerFrame(currentState))
         {
            currentFrame++;
            lastFrameChange = System.currentTimeMillis();
            if (currentFrame >= imageSet.getTotalFrames(currentState))
            {
               currentFrame = 0;
               totalCycles++;
            }
         }
      }
   }

   /**
    * @return The total number of cycles this sprite has animated through.
    * Very useful for determining if a sprite has finished its animation.
    */
   public final int getTotalCycles()
   {
      return totalCycles;
   }
}

Using the Sprite class is pretty easy. Once you have an ImageSet ready, you can simply construct a Sprite by passing it in along with a starting state and frame. For example:

bulletSprite = new Sprite(bulletImageSet, flyingState, 0);

This creates a new Sprite using bulletImageSet for images and animation timing. It also sets the current state to be the flyingState reference you previously recorded.

The Sprite class also has convenience methods to change the current state or frame, although generally the cycle method will switch frames for you.

You'll notice that I've also made the Sprite object keep track of exactly when a state change occurs. Game classes can then use this data to find out how long a sprite has been in a given state. For example, the bullets in Star Assault have a limited lifetime (about three seconds). Instead of tracking the lifetime of the bullet in the Bullet class, you can check the time the bullet has been in the flying state using the getTimeInCurrentState method. Using a simple test each cycle, you can determine whether a bullet is old enough to be removed from the game world.

The Sprite class also tracks the total number of times it has cycled through the current animation. This is very useful if you want to know when an animation has gone through a certain number of iterations. For example, using the getTotalCycles method, the Bullet class can determine when its explosion animation has completed. After it has gone through once, you remove the bullet from the screen. Again, this saves you from doing the same sort of thing inside higher-level game object classes.

More Action

Now that you have a fully operational Sprite class, you can put it to use. In the following example, I've created a few alien proximity mine sprites and let them spin through their animations. Notice how I'm using the same image set for three distinct sprites. I'm also starting the animation at slightly different frames to demonstrate the sprites' independence.

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.io.IOException;

/**
 * A demonstration of the ImageSet and Sprite classes.
 * @author Martin J. Wells
 */
public class AdvancedSpriteTest extends MIDlet implements CommandListener,
         Runnable
{
   private static int MINE_FRAME_WIDTH = 16;
   private static int MINE_FRAME_HEIGHT = 16;

   private MyCanvas myCanvas;
   private Command quit;
   private boolean running;
   private Sprite mineSprite1;
   private Sprite mineSprite2;
   private Sprite mineSprite3;

   /**
    * An inner class canvas used to draw the three animating mines.
    */
   class MyCanvas extends Canvas
   {
      /** 
       * Paints the mine sprites onto a blank canvas.
       * @param graphics The graphics context to draw onto.
       */
      protected void paint(Graphics graphics)
      {
         // Set the color to black and draw a complete rectangle in order to
         // clear the screen.
         graphics.setColor(0);
         graphics.fillRect(0, 0, getWidth(), getHeight());

         // Draw each of the sprite objects at different positions on the
         // screen.

         // Draw the first sprite at position 25, 25.
         mineSprite1.draw(graphics, 25, 25);
         // Draw the first sprite at position 50, 50.
         mineSprite2.draw(graphics, 50, 50);
         // Draw the first sprite at position 75, 75.
         mineSprite3.draw(graphics, 75, 75);
      }
   }

   /**
    * Constructor for our demo that loads up the mine graphics and uses these
    * to create an image set and then three sprites. It then creates a canvas as
    * well as a quit command.
    */
   public AdvancedSpriteTest()
   {
      try
      {
         // Load up the mine.png image and then use extract frames to rip out
         // the frames of the image. In this code we start at position 0, 0 in
         // the mine.png (which means our mine frames are in the top left of
         // the file) and then load 3 frames across and 2 down -- a total of
         // 6 frames.
         Image[] frames = ImageSet.extractFrames(Image.createImage("/mine.png"),
                                                     0, 0, 3, 2,
                                                     MINE_FRAME_WIDTH,
                                                     MINE_FRAME_HEIGHT);
         // Create an ImageSet for the mine frames.
         ImageSet set = new ImageSet(1); 
         // Set the animation speed to be 500 (so we'll animate through all 6
         // frames in half a second (500 ms).
         set.addState(frames, 500);

         // Create the first Sprite with a starting state of 0 and starting
         // frame of 0.
         mineSprite1 = new Sprite(set, 0, 0);
         // Create another sprite (using the same mine graphics - ImageSet) but
         // this time set the starting frame to 3 in order to offset the
         // animation a little. Changing the starting frame like this stops
         // all the mines animating with exactly the same frames.
         mineSprite2 = new Sprite(set, 0, 3);
         // For the last one we start at frame 5.
         mineSprite3 = new Sprite(set, 0, 5);
      }
      catch (IOException ioe)
      {
         System.out.println("unable to load image");
      }

      // Construct the canvas.
      myCanvas = new MyCanvas();

      // And a way to quit.
      quit = new Command("Quit", Command.EXIT, 2);
      myCanvas.addCommand(quit);

      myCanvas.setCommandListener(this);

      // Create a thread to do the animation and then redraw the Canvas.
      running = true;
      Thread t = new Thread(this);
      t.start();
   }

   /**
    * Executed when we start the Thread for this class. Calls cycle on the three
    * Sprite objects as well as asking for a repaint on the Canvas.
    */
   public void run()
   {
      while (running) 
      {
         myCanvas.repaint();

         // Call the cycle method in order to have it advance the animation. To
         // save on code here I've just used a simple value of 100 to represent
         // the amount of time that has passed. This isn't accurate but it will
         // do for a demo of sprites. You should replace this with proper code
         // to track and determine the time difference between each cycle call.
         mineSprite1.cycle(100);
         mineSprite2.cycle(100);
         mineSprite3.cycle(100);

         try
         {
            Thread.sleep(100);
         }
         catch (InterruptedException e)
         {
         }

      }
   }

   /**
    * Handles Application Manager notification the MIDlet is starting (or
    * resuming from a pause). In this case we set the canvas as the current
    * display screen.
    * @throws MIDletStateChangeException
    */
   protected void startApp() throws MIDletStateChangeException
   {
      Display.getDisplay(this).setCurrent(myCanvas);
   }

   /**
    * Handles Application Manager notification the MIDlet is about to be paused.
    * We don't bother doing anything for this case.
    */
   protected void pauseApp()
   {
   }

   /** 
    * Handles Application Manager notification the MIDlet is about to be
    * destroyed. We don't bother doing anything for this case.
    */
   protected void destroyApp(boolean unconditional)
            throws MIDletStateChangeException
   {
   }

   /**
    * The CommandListener interface method called when the user executes
    * a Command, in this case it can only be the quit command we created in the
    * constructor and added to the Canvas.
    * @param command The command that was executed.
    * @param displayable The displayable that command was embedded within.
    */
   public void commandAction(Command command, Displayable displayable)
   {
      try
      {
         if (command == quit)
         {
            running = false;
            destroyApp(true);
            notifyDestroyed();
         }
      }

      catch (MIDletStateChangeException me)
      {
         System.out.println(me + " caught.");
      }
   }

}

And Beyond

That's about all you need for your sprite system in Star Assault. While it's not overly complicated, it has everything you need for your game. Being a programmer, though, I'm sure you already have 50 ideas for expanding the functionality I've presented. The problem is, you've already used about 8 KB of JAR space just creating your three classes (ImageSet, Tools, and Sprite). Because you already have pretty much everything you need, you can't afford to waste precious JAR space on functionality not directly used by the current project.

Being forced to include only what you absolutely need makes engine development extremely difficult. You'll blow 64 KB faster than you can say, "Sorry sir, I confused you with a giant pretzel." Try to keep this in mind when you look to add more capabilities to your classesit'll save you from removing code later.

Having said that, there are certainly plenty of development situations in which you will need extra functionality, so here are a few ideas for how to expand the capabilities of your sprite system. It's up to you when and where you use these ideasjust be ready to sacrifice something else in exchange for them.

  • Reverse animation. Add the option to have animations play either forward or backward (or maybe even sideways!).

  • Transitioning. Support the concept of states that can transition to other states. When adding a state, you could specify which state should follow, either automatically (after a set number of cycles) or manually, by calling a nextState method.

  • Triggers. Commonly, you'll want to know when an animation or state transition occurs. To help with this, you could have the Sprite class call you back when these events occur. Any class wanting to listen to these events could implement a SpriteEventListener interface. Although this might sound expensive, consider how much code you waste polling to see whether these events have occurred anyway.

  • Image set manager. As the name implies, you can add a manager for all of your game's image sets. You can use this to centralize image loading and unloading (or nulling), as well as to keep all graphics access code in one place. Centralizing management will also help you keep track of the graphics that are currently in memory at any point in time, which can prove very useful when you're running low on RAM.

  • Resource data. Instead of putting your graphics meta-data (frame size and layout, animation speed, and state setup) in code, consider storing it in a separate resource data file. Artists can then modify the file without needing to bother the all-important programmer. You could even write some simple MIDlets for artists to load and test sprites independent of the game.

    Table of ContentsSprite BasicsConclusion