Specifying a Sprite with a Statechart


Coding a Sprite

The Sprite class is simple, storing little more than the sprite's current position, its speed specified as step increments in the x- and y- directions, with imaging managed by ImagesLoader and ImagesPlayer objects. The ImagesPlayer class allows the sprite to show a sequence of images repeatedly since this is how the ant moves its legs.

The Sprite subclasses, BatSprite and BallSprite in BugRunner, manage user interactions, environment concerns (e.g., collision detection and response), and audio effects. These elements are too application specific to be placed in Sprite.

The Sprite Constructor

A sprite is initialized with its position, the size of the enclosing panel, an ImagesLoader object, and the name of an image:

     // default step sizes (how far to move in each update)     private static final int XSTEP = 5;     private static final int YSTEP = 5;     private ImagesLoader imsLoader;     private int pWidth, pHeight;   // panel dimensions     // protected vars     protected int locx, locy;      // location of sprite     protected int dx, dy;          // amount to move for each update     public Sprite(int x, int y, int w, int h, ImagesLoader imsLd, String name)     { locx = x; locy = y;       pWidth = w; pHeight = h;       dx = XSTEP; dy = YSTEP;       imsLoader = imsLd;       setImage(name);    // the sprite's default image is 'name'     }

The sprite's coordinate (locx, locy) and its step values (dx, dy) are stored as integers. This simplifies certain tests and calculations but restricts positional and speed precision. For instance, a ball can't move 0.5 pixels at a time.

The alternative is to use floats or doubles to hold the coordinates and velocities. However, this adds complexity and is unnecessary in this example. floats would be useful when the calculations require greater accuracy, for example, for rotations using matrix multiplication.


locx, locy, dx, and dy are protected rather than private due to their widespread use in Sprite subclasses. They have getter and setter methods, so they can be accessed and changed by objects outside of the Sprite hierarchy.

Sprite only stores (x, y) coordinates: there's no z-coordinate or z-level; such functionality is unnecessary in BugRunner. Simple z-level functionality can be achieved by ordering the calls to drawSprite( ) in gameRender( ). Currently, the code is simply:

     ball.drawSprite(dbg);     bat.drawSprite(dbg);

The ball is drawn before the bat, so will appear behind it if they happen to overlap on-screen. In an application where you had 5, 10, or more sprites, this won't work, especially if the objects move in a way that changes their z-level.

A Sprite's Image

setImage( ) assigns the named image to the sprite:

     // default dimensions when there is no image     private static final int SIZE = 12;     // image-related globals     private ImagesLoader imsLoader;     private String imageName;     private BufferedImage image;     private int width, height;     // image dimensions     private ImagesPlayer player;  // for playing a loop of images     private boolean isLooping;     public void setImage(String name)     {       imageName = name;       image = imsLoader.getImage(imageName);       if (image == null) {    // no image of that name was found         System.out.println("No sprite image for " + imageName);         width = SIZE;         height = SIZE;       }       else {         width = image.getWidth( );         height = image.getHeight( );       }       // no image loop playing       player = null;       isLooping = false;     }

setImage( ) is a public method, permitting the sprite's image to be altered at runtime.

An ImagesPlayer object, player, is available to the sprite for looping through a sequence of images. Looping is switched on with the loopImage( ) method:

     public void loopImage(int animPeriod, double seqDuration)     {       if (imsLoader.numImages(imageName) > 1) {         player = null;   // for garbage collection of previous player         player = new ImagesPlayer(imageName, animPeriod, seqDuration,                                          true, imsLoader);         isLooping = true;       }       else         System.out.println(imageName + " is not a sequence of images");     }

The total time for the loop is seqDuration seconds. The update interval (supplied by the enclosing animation panel) is animPeriod milliseconds.

Looping is switched off with stopLooping( ):

     public void stopLooping( )     { if (isLooping) {         player.stop( );         isLooping = false;       }     }

A Sprite's Bounding Box

Collision detection and collision response is left to subclasses. However, the bounding box for the sprite is available through the getMyRectangle( ) method:

     public Rectangle getMyRectangle( )     {  return  new Rectangle(locx, locy, width, height);  }

Sprite uses the simplest form of bounding box, but it wouldn't be difficult to introduce a reduction factor. The reduced bounded box would have a smaller width and height and would need to be positioned so its center coincided with the center of the image's full-size bounding rectangle.


Updating a Sprite

A sprite is updated by adding its step values (dx, dy) to its current location (locx, locy):

     // global     private boolean isActive = true;     // a sprite is updated and drawn only when active     public void updateSprite( )     {       if (isActive( )) {         locx += dx;         locy += dy;         if (isLooping)           player.updateTick( );  // update the player       }     }

The isActive boolean allows a sprite to be (temporarily) removed from the game since the sprite won't be updated or drawn when isActive is false. There are public isActive( ) and setActive( ) methods for manipulating the boolean.

No attempt is made in updateSprite( ) to test for collisions with other sprites, obstacles, or the edges of the gaming pane. These must be added by the subclasses when they override updateSprite( ).


Sprites are embedded in an animation framework that works hard to maintain a fixed frame rate. run( ) calls updateSprite( ) in all the sprites at a frequency as close to the specified frame rate as possible. For example, if the frame rate is 40 FPS (as it is in BugRunner), then updateSprite( ) will be called 40 times per second in each sprite.

This allows me to make assumptions about a sprite's update timing. For instance, if the x-axis step value (dx) is 10, then the sprite will be moved 10 pixels in each update. This corresponds to a speed of 10 x 40 = 400 pixels per second along that axis. This calculation is possible because the frame rate is tightly constrained to 40 FPS.

An alternative approach is to call updateSprite( ) with an argument holding the elapsed time since the previous call. This time value can be multiplied to a velocity value to get the step amount for this particular update. This technique is preferably in animation frameworks, where the frame rate can vary during execution.

Drawing a Sprite

The animation loop will call updateSprite( ) in a sprite, followed by drawSprite( ) to draw it:

     public void drawSprite(Graphics g)     {       if (isActive( )) {         if (image == null) {   // the sprite has no image           g.setColor(Color.yellow);   // draw a yellow circle instead           g.fillOval(locx, locy, SIZE, SIZE);           g.setColor(Color.black);         }         else {           if (isLooping)             image = player.getCurrentImage( );           g.drawImage(image, locx, locy, null);         }       }     }

If the image is null, then the sprite's default appearance is a small yellow circle. The current image in the looping series is obtained by calling ImagesPlayer's getCurrentImage( ) method.



Killer Game Programming in Java
Killer Game Programming in Java
ISBN: 0596007302
EAN: 2147483647
Year: 2006
Pages: 340

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net