Monday, January 25, 2010

The Animation Framework









The Animation Framework


JackPanel is a subclass of JPanel and implements the animation framework described in Chapters 2 and 3; JackPanel resembles the BugPanel class of Chapter 11.


The JackPanel( ) constructor in Example 12-1 creates the game entities: the RibbonsManager, BricksManager, JumperSprite, and FireBallSprite objects. It prepares the explosion animation and the title/help screen.



Example 12-1. The JackPanel constructor


// some of the globals
private JumpingJack jackTop;
private JumperSprite jack; // the sprites
private FireBallSprite fireball;
private RibbonsManager ribsMan; // the ribbons manager
private BricksManager bricksMan; // the bricks manager

// to display the title/help screen
private boolean showHelp;
private BufferedImage helpIm;

// explosion-related
private ImagesPlayer explosionPlayer = null;
private boolean showExplosion = false;
private int explWidth, explHeight; // image dimensions
private int xExpl, yExpl; // coords where image is drawn


public JackPanel(JumpingJack jj, long period)
{
jackTop = jj;
this.period = period;

setDoubleBuffered(false);
setBackground(Color.white);
setPreferredSize( new Dimension(PWIDTH, PHEIGHT));

setFocusable(true);
requestFocus( ); // so receives key events

addKeyListener( new KeyAdapter( ) {
public void keyPressed(KeyEvent e)
{ processKey(e); }
});

// initialise the loaders
ImagesLoader imsLoader = new ImagesLoader(IMS_INFO);
clipsLoader = new ClipsLoader(SNDS_FILE);


// initialise the game entities

bricksMan = new BricksManager(PWIDTH, PHEIGHT, BRICKS_INFO, imsLoader);
int brickMoveSize = bricksMan.getMoveSize( );

ribsMan = new RibbonsManager(PWIDTH, PHEIGHT, brickMoveSize, imsLoader);

jack = new JumperSprite(PWIDTH, PHEIGHT, brickMoveSize,
bricksMan, imsLoader, (int)(period/1000000L) ); // in ms


fireball = new FireBallSprite(PWIDTH, PHEIGHT,
imsLoader, this, jack);


// prepare the explosion animation

explosionPlayer = new ImagesPlayer("explosion",
(int)(period/1000000L), 0.5, false, imsLoader);
BufferedImage explosionIm = imsLoader.getImage("explosion");
explWidth = explosionIm.getWidth( );
explHeight = explosionIm.getHeight( );
explosionPlayer.setWatcher(this) // report anim end back here


// prepare title/help screen

helpIm = imsLoader.getImage("title");
showHelp = true; // show at start-up
isPaused = true;

// set up message font
msgsFont = new Font("SansSerif", Font.BOLD, 24);
metrics = this.getFontMetrics(msgsFont);
} // end of JackPanel( )




The BricksManager object is created first, so a brickMoveSize variable can be initialized. This will contain the number of pixels that the bricks map is shifted when the sprite appears to move. brickMoveSize is used as the basis for the move increments employed by the Ribbon objects managed in RibbonsManager and is used by the JumperSprite. However, the fireball travels at its own rate, independent of the background, so it doesn't require the move size.


JackPanel is in charge of a fireball's animated explosion and its associated audio, rather than FireBallSprite. The explosion animation in explosion.gif is loaded into an ImagesPlayer (see Figure 12-5 for its contents), and the dimensions of its first image are recorded. When the sequence is finished, ImagesPlayer will call sequenceEnded( ) back in JackPanel.



Figure 12-5. The images strip in explosion.gif



The title/help image (in title.gif; see Figure 12-6) is loaded into the global helpIm, and the values of the Booleans showHelp and isPaused are set. isPaused causes the game's execution to pause and was introduced in the basic game animation framework; showHelp is a new Boolean, examined by gameRender( ) to decide whether to draw the image.



Figure 12-6. title.gif: the title/help screen in JumpingJack



gameRender( ) displays the image centered in the JPanel, so the image should not be too large or its borders may be beyond the edges of the panel. If the image is the same size as the JPanel, it will totally obscure the game window and look more like a screen rather than an image drawn on the game surface.


Clever use can be made of transparency to make the image an interesting shape though it's still a rectangle as far as drawImage( ) is concerned.



Switching on isPaused while the help image is visible requires a small change to the resumeGame( ) method:



public void resumeGame( )
{ if (!showHelp) // CHANGED
isPaused = false;
}



This method is called from the enclosing JumpingJack JFrame when the frame is activated (deiconified). Previously, resumeGame( ) is always set isPaused to false, but now this occurs only when the help screen isn't being displayed.


If the game design requires distinct title and help screens, then two images and two Booleans will be needed. For example, you would need showHelp for the help image and showTitle for the titles, which would be examined in gameRender( ). Initially, showTitle would be set to true and showHelp assigned a false value. When the titles or the help is on-screen, isPaused would be set to true.



Dealing with Input


Only keyboard input is supported in JumpingJack. A key press triggers a call to processKey( ), which handles three kinds of input: termination keys, help controls, and game-play keys:



private void processKey(KeyEvent e)
{
int keyCode = e.getKeyCode( );



// termination keys

// listen for esc, q, end, ctrl-c on the canvas to
// allow a convenient exit from the full screen configuration
if ((keyCode==KeyEvent.VK_ESCAPE) || (keyCode==KeyEvent.VK_Q) ||
(keyCode == KeyEvent.VK_END) ||
((keyCode == KeyEvent.VK_C) && e.isControlDown( )) )
running = false;


// help controls

if (keyCode == KeyEvent.VK_H) {
if (showHelp) { // help being shown
showHelp = false; // switch off
isPaused = false;
}
else { // help not being shown
showHelp = true; // show it
isPaused = true;
}
}


// game-play keys

if (!isPaused && !gameOver) {
// move the sprite and ribbons based on the arrow key pressed
if (keyCode == KeyEvent.VK_LEFT) {
jack.moveLeft( );
bricksMan.moveRight( ); // bricks and ribbons move other way
ribsMan.moveRight( );
}
else if (keyCode == KeyEvent.VK_RIGHT) {
jack.moveRight( );
bricksMan.moveLeft( );
ribsMan.moveLeft( );
}
else if (keyCode == KeyEvent.VK_UP)
jack.jump( ); // jumping has no effect on bricks/ribbons
else if (keyCode == KeyEvent.VK_DOWN) {
jack.stayStill( );
bricksMan.stayStill( );
ribsMan.stayStill( );
}
}
} // end of processKey( )



The termination keys are utilized in the same way as in earlier examples. The help key (h) toggles the showHelp and isPaused Booleans on and off. The arrow keys are assigned to be the game play keys. When the left or right arrow keys are pressed, the scenery (the bricks and ribbons) is moved in the opposite direction from Jack. You'll see that the calls to moveLeft( ) and moveRight( ) in Jack don't cause the sprite to move at all.




Multiple Key Presses/Actions


A common requirement in many games is to process multiple key presses together. For example, it should be possible for Jack to jump and move left/right at the same time. There are two parts to this feature: implementing key capture code to handle simultaneous key presses and implementing simultaneous behaviors in the sprite.


JumpingJack has the ability to jump and move left/right simultaneously: it was wired into the JumperSprite class at the design stage, as you'll see. If Jack is currently moving left or right, then an up arrow press will make him jump. A related trick is to start Jack jumping from a stationary position, causing him to rise and fall over 1 to 2 seconds. During that interval, the left or right arrow keys can be pressed to get him moving horizontally through the air or to change his direction in mid-flight!


Though Jack can jump and move simultaneously, this behavior is triggered by distinct key presses. First, the left/right arrow key is pressed to start him moving, and then the up arrow key makes him jump. Alternatively, the up arrow key can be pressed first, followed by the left or right arrow keys. If you want to capture multiple key presses at the same time, then modifications are needed to the key listener code.


The main change would be to use keyPressed( ) and keyReleased( ) and to introduce new Booleans to indicate when keys are being pressed. The basic coding strategy is shown here:




// global Booleans, true when a key is being pressed

private boolean leftKeyPressed = false;
private boolean rightKeyPressed = false;
private boolean upKeyPressed = false;


public JackPanel(JumpingJack jj, long period)
{
... // other code
addKeyListener( new KeyAdapter( ) {
public void keyPressed(KeyEvent e)
{ processKeyPress(e); }
public void keyReleased(KeyEvent e)
{ processKeyRelease(e); }
});
... // other code
}


private void processKeyPress(KeyEvent e)
{
int keyCode = e.getKeyCode( );


// record the key press in a Boolean

if (keyCode == KeyEvent.VK_LEFT)
leftKeyPressed = true;
else if (keyCode == KeyEvent.VK_RIGHT)

rightKeyPressed = true;
else if (keyCode == KeyEvent.VK_UP)
upKeyPressed = true;


// use the combined key presses

if (leftKeyPressed && upKeyPressed)
// do a combined left and up action
else if (rightKeyPressed && upKeyPressed)
// do a combined right and up action

... // other key processing code
} // end of processKeyPress( )


private void processKeyRelease(KeyEvent e)
{
int keyCode = e.getKeyCode( );


// record the key release in a Boolean

if (keyCode == KeyEvent.VK_LEFT)
leftKeyPressed = false;
else if (keyCode == KeyEvent.VK_RIGHT)
rightKeyPressed = false;
else if (keyCode == KeyEvent.VK_UP)
upKeyPressed = false;
} // end of processKeyRelease( )



Key presses cause the relevant Booleans to be set, and they remain set until the user releases the keys at some future time. The combination of key presses can be detected by testing the Booleans in processKeyPress( ).


This coding effort is only needed for combinations of "normal" keys (e.g., the letters, the numbers, and arrow keys). Key combinations involving a standard key and the shift, control, or meta keys can be detected more directly by using the KeyEvent methods isShiftDown( ), isControlDown( ), and isMetaDown( ). This coding style can be seen in the termination keys code in processKey( ):



if (...||((keyCode==KeyEvent.VK_C) && e.isControlDown( ))) //ctrl-c
running = false;





The Animation Loop


The animation loop is located in run( ) and is unchanged from earlier examples. For example, it's the same run( ) method seen in BugRunner in Chapter 11. Essentially, it is:



public void run( )
{ // initialization code
while (running) {

gameUpdate( );


gameRender( );

paintScreen( );

// timing correction code
}
System.exit(0);
}



gameUpdate( ) updates the various game elements (the sprites, the brick layers, and Ribbon objects):



private void gameUpdate( )
{
if (!isPaused && !gameOver) {
if (jack.willHitBrick( )) { // collision checking first
jack.stayStill( ); // stop jack and scenery
bricksMan.stayStill( );
ribsMan.stayStill( );
}
ribsMan.update( ); // update background and sprites
bricksMan.update( );
jack.updateSprite( );
fireball.updateSprite( );

if (showExplosion)
explosionPlayer.updateTick( ); // update the animation
}
}



The new element here is dealing with potential collisions: if Jack is to hit a brick when the current update is carried out, then the update should be cancelled. This requires a testing phase before the update is committed, embodied in willHitBrick( ) in JumperSprite. If Jack is to hit a brick with his next update, it will be due to him moving (there are no animated tiles in this game), so the collision can be avoided by stopping Jack (and the backgrounds) from moving.


The fireball sprite is unaffected by Jack's impending collision: it travels left regardless of what the JumperSprite is doing.


The showExplosion Boolean is set to true when the explosion animation is being played by the ImagesPlayer (explosionPlayer), so updateTick( ) must be called during each game update.



Rendering order

gameRender( ) draws the multiple layers making up the game. Their ordering is important because rendering must start with the image farthest back in the scene and work forward. This ordering is illustrated in Figure 12-2 for JumpingJack:



private void gameRender( )
{
if (dbImage == null){
dbImage = createImage(PWIDTH, PHEIGHT);
if (dbImage == null) {
System.out.println("dbImage is null");
return;

}
else
dbg = dbImage.getGraphics( );
}

// draw a white background
dbg.setColor(Color.white);
dbg.fillRect(0, 0, PWIDTH, PHEIGHT);

// draw the game elements: order is important
ribsMan.display(dbg); // the background ribbons
bricksMan.display(dbg); // the bricks
jack.drawSprite(dbg); // the sprites

fireball.drawSprite(dbg);


if (showExplosion) // draw the explosion (in front of jack)
dbg.drawImage(explosionPlayer.getCurrentImage( ),
xExpl, yExpl, null);
reportStats(dbg);
if (gameOver)
gameOverMessage(dbg);

if (showHelp) // draw help at the very front (if switched on)
dbg.drawImage(helpIm, (PWIDTH-helpIm.getWidth( ))/2,
(PHEIGHT-helpIm.getHeight( ))/2, null);
} // end of gameRender( )



gameRender( ) relies on the RibbonsManager and BricksManager objects to draw the multiple Ribbon objects and the individual bricks. The code order means that Jack will be drawn behind the fireball if they are at the same spot, i.e., when the fireball hits him. An explosion is drawn in front of the fireball, and the game statistics, the Game Over message, and the help screen is layered on top.





Handling an Explosion


The fireball sprite passes the responsibility of showing the explosion animation and its audio clip to JackPanel, by calling showExplosion( ):



// names of the explosion clips
private static final String[] exploNames =
{"explo1", "explo2", "explo3"};

public void showExplosion(int x, int y)
// called by FireBallSprite
{
if (!showExplosion) { // only allow a single explosion at a time
showExplosion = true;
xExpl = x - explWidth/2; // (x,y) is center of explosion
yExpl = y - explHeight/2;

/* Play an explosion clip, but cycle through them.
This adds variety, and gets around not being able to

play multiple instances of a clip at the same time. */
clipsLoader.play( exploNames[ numHits%exploNames.length ],
false);
numHits++;
}
} // end of showExplosion( )



The (x, y) coordinate passed to showExplosion( ) is assumed to be where the center of the explosion should occur, so the top-left corner of the explosion image is calculated and placed in the globals (xExpl, yExpl). These are used to position the explosion in gameRender( ).


The use of a single Boolean (showExplosion) to determine if an explosion appears on-screen is adequate only if a single explosion animation is shown at a time. This means that if a fireball hits Jack while an explosion sequence is playing (as a result of a previous fireball that hit him), a second animation will not be rendered. This restriction allows me to use a single ImagesPlayer object instead of a set containing one ImagesPlayer for each of the current explosions.


play( ) in ClipsLoader eventually calls start( ) for the Clip object. A design feature of start( ) is that when a clip is playing, further calls to start( ) will be ignored. This makes it impossible to play multiple instances of the same Clip object at the same time and means that while the explosion clip is playing (for 1 to 2 seconds), another explosion can't be heard. This absence is quite noticeable (more so than the lack of multiple explosion animations, for some reason). Also, the game just seems more fun if there's a crescendo of explosions as Jack gets pummeled.


Therefore, I've gone for a set of explosion clips, stored in exploNames[], and the code cycles through them. A set of three seems enough to deal with even the highest rate of fireball hits to Jack. Since these names represent separate Clips stored in the ClipsLoader, they can be played simultaneously.


The clips are different from each other, so there's a pleasing interplay of noises as multiple explosions go off. The order the sounds are played isn't relevant, at least in this game.


I found the clips by searching for sound filenames containing the word "explosion," "bomb," and similar, using the FindSounds site (http://www.findsounds.com/). I looked for small clips, lasting 1-2 seconds, to roughly match the duration of the explosion animation.



Once an explosion animation has finished playing, its ImagesPlayer object calls sequenceEnded( ) in JackPanel:



public void sequenceEnded(String imageName)
// called by ImagesPlayer when the expl. animation finishes
{
showExplosion = false;
explosionPlayer.restartAt(0); // reset animation for next time


if (numHits >= MAX_HITS) {
gameOver = true;
score = (int) ((J3DTimer.getValue( ) -
gameStartTime)/1000000000L);
clipsLoader.play("applause", false);
}
}



sequenceEnded( ) resets the animation, so it's ready to be played next time, and checks the game over condition. If the number of fireball hits equals or exceeds MAX_HITS, then the game over flag is set, causing the game to terminate.


The main question about sequenceEnded( ) is why it is being used at all. The answer is to make the game terminate at a natural time, just after an explosion has finished. For instance, if the game over condition was tested at the end of showExplosion( ), the game might have been terminated while ImagesPlayer was in the middle of displaying the explosion animation. This might seem a bit odd to a player, especially one who likes to see explosions run their course.










    No comments: