Coding a SpriteThe 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 ConstructorA 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.
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 ImagesetImage( ) 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 BoxCollision 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); }
Updating a SpriteA 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.
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 SpriteThe 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. |