The Jumping SpriteA JumperSprite object can appear to move left or right, jump, and stand still. The sprite doesn't move horizontally at all, but the left and right movement requests will affect its internal state. It maintains its current world coordinates in (xWorld, yWorld). When a sprite starts moving left or right, it will keep traveling in that direction until stopped by a brick. If the sprite runs off a raised platform, it will fall to the ground below and continue moving forward. When the sprite jumps, it continues upward for a certain distance and falls back to the ground. The upward trajectory is stopped if the sprite hits a brick. Statechart SpecificationThe JumperSprite statechart is given in Figure 12-23. The statechart models JumperSprite as three concurrent activities: its horizontal movement in the top section, its vertical movement in the middle section, and the update/draw cycle in the bottom section.
The horizontal movement section shows that a new updateSprite( ) event doesn't change the current state, be it moving right, moving left, or stationary. Movement stops when the user sends a stop event or when the sprite hits a brick. The vertical movement section utilizes three states: not jumping, rising, and falling. Rising is controlled by an upCount counter, which limits how long an upward move can last. Rising may be stopped by the sprite hitting a brick. Falling is triggered when Figure 12-23. The JumperSprite statechartrising finishes and when no brick is underneath the sprite. This latter condition becomes true when the sprite moves horizontally off a raised platform. The falling state can lead to termination if the sprite drops below the bottom of the panel (yWorld > pHeight). In fact, this transition led to a redesign of BricksManager to reject a bricks map with a gap in its floor. Consequently, dropping off the panel cannot occur in JumpingJack. Though the statechart is clear, I want to avoid the complexity of multiple threads in JumperSprite. Instead, the concurrent activities are interleaved together in my code, making it somewhat harder to understand but easier to write. Representing the StatesThe moving right, moving left, and stationary states are represented indirectly as two BooleansisFacingRight and isStillwhich combine to define the current horizontal state. For instance, when isStill is false and isFacingRight is true, then the sprite is moving right. The not jumping, rising, and falling states are encoded as constants, assigned to a vertMoveMode variable: private static final int NOT_JUMPING = 0; private static final int RISING = 1; private static final int FALLING = 2; private int vertMoveMode; /* can be NOT_JUMPING, RISING, or FALLING */ private boolean isFacingRight, isStill;
InitializationThe initialize state is coded in JumperSprite's constructor: // some globals private int vertStep; // distance to move vertically in one step private int upCount; private int moveSize; // obtained from BricksManager private int xWorld, yWorld; /* the current position of the sprite in 'world' coordinates. The x-values may be negative. The y-values will be between 0 and pHeight. */ public JumperSprite(int w, int h, int brickMvSz, BricksManager bm, ImagesLoader imsLd, int p) { super(w/2, h/2, w, h, imsLd, "runningRight"); // standing center screen, facing right moveSize = brickMvSz; // the move size is the same as the bricks ribbon brickMan = bm; period = p; setStep(0,0); // no movement isFacingRight = true; isStill = true; /* Adjust the sprite's y- position so it is standing on the brick at its mid x- position. */ locy = brickMan.findFloor(locx+getWidth( )/2)-getHeight( ); xWorld = locx; yWorld = locy; // store current position vertMoveMode = NOT_JUMPING; vertStep = brickMan.getBrickHeight( )/2; // the jump step is half a brick's height upCount = 0; } // end of JumperSprite( ) The (xWorld, yWorld) coordinates and the sprite's position and speed are set. The state variables isFacingRight, isStill, and vertMoveMode define a stationary, nonjumping sprite, facing to the right. BricksManager's findFloor( ) method is used to get a y location for the sprite that lets it stand on top of a brick. The method's input argument is the sprite's midpoint along the x-axis, which is its leftmost x-coordinate plus half its width (locx+getWidth( )/2). Key Event ProcessingThe events move left, move right, stop, and jump in the statechart are caught as key presses by the key listener in JackPanel, TRiggering calls to the JumperSprite methods moveLeft( ), moveRight( ), stayStill( ), and jump( ). moveLeft( ), moveRight( ), and stayStill( ) affect the horizontal state by adjusting the isFacingRight and isStill variables. The animated image associated with the sprite changes: public void moveLeft( ) { setImage("runningLeft"); loopImage(period, DURATION); // cycle through the images isFacingRight = false; isStill = false; } public void moveRight( ) { setImage("runningRight"); loopImage(period, DURATION); // cycle through the images isFacingRight = true; isStill = false; } public void stayStill( ) { stopLooping( ); isStill = true; } The jump( ) method represents the transition from the not jumping to the rising state in the statechart. This is coded by changing the value stored in vertMoveMode. The sprite's image is modified: public void jump( ) { if (vertMoveMode == NOT_JUMPING) { vertMoveMode = RISING; upCount = 0; if (isStill) { // only change image if the sprite is 'still' if (isFacingRight) setImage("jumpRight"); else setImage("jumpLeft"); } } } JackPanel Collision TestingThe [will hit brick on the right] and [will it brick on the left] conditional transitions in the statechart are implemented as a public willHitBrick( ) method called from JackPanel's gameUpdate( ) method: private void gameUpdate( ) { if (!isPaused && !gameOver) { if (jack.willHitBrick( )) { // collision checking first jack.stayStill( ); // stop everything moving bricksMan.stayStill( ); ribsMan.stayStill( ); } ribsMan.update( ); // update background and sprites bricksMan.update( ); jack.updateSprite( ); fireball.updateSprite( ); if (showExplosion) explosionPlayer.updateTick( ); // update the animation } } The reason for placing the test in JackPanel's hands is so it can coordinate the other game entities when a collision occurs. The JumperSprite and the background layers in the game are halted: public boolean willHitBrick( ) { if (isStill) return false; // can't hit anything if not moving int xTest; // for testing the new x- position if (isFacingRight) // moving right xTest = xWorld + moveSize; else // moving left xTest = xWorld - moveSize; // test a point near the base of the sprite int xMid = xTest + getWidth( )/2; int yMid = yWorld + (int)(getHeight( )*0.8); // use y posn return brickMan.insideBrick(xMid,yMid); } // end of willHitBrick( ) willHitBrick( ) represents two conditional transitions, so the isFacingRight flag is used to distinguish how xTest should be modified. The proposed new coordinate is generated and passed to BricksManager's insideBrick( ) for evaluation. The vertical collision testing in the middle section of the statechart, [will hit brick below] and [will hit brick above], is carried out by JumperSprite, not JackPanel, since a collision affects only the sprite. Updating the SpriteThe statechart distributes the actions of the updateState( ) event around the statechart: actions are associated with the moving right,moving left, rising, and falling states. These actions are implemented in the updateState( ) method, and the functions it calls: public void updateSprite( ) { if (!isStill) { // moving if (isFacingRight) // moving right xWorld += moveSize; else // moving left xWorld -= moveSize; if (vertMoveMode == NOT_JUMPING) // if not jumping checkIfFalling( ); // may have moved out into empty space } // vertical movement has two components: RISING and FALLING if (vertMoveMode == RISING) updateRising( ); else if (vertMoveMode == FALLING) updateFalling( ); super.updateSprite( ); } // end of updateSprite( ) The method updates its horizontal position (xWorld) first, distinguishing between moving right or left by examining isStill and isFacingRight. After the move, checkIfFalling( ) decides whether the [no brick below] transition from not jumping to falling should be applied. The third stage of the method is to update the vertical states. Lastly, the call to Sprite's updateSprite( ) method modifies the sprite's position and image. updateSprite( ) illustrates the coding issues that arise when concurrent activities (in this case, horizontal and vertical movement) are sequentialized. The statechart places no restraints on the ordering of the two types of movement, but an ordering must be imposed when it's programmed as a sequence. In updateSprite( ), the horizontal actions are carried out before the vertical ones. Falling?checkIfFalling( ) determines whether the not jumping state should be changed to falling: private void checkIfFalling( ) { // could the sprite move downwards if it wanted to? // test its center x-coord, base y-coord int yTrans = brickMan.checkBrickTop( xWorld+(getWidth( )/2), yWorld+getHeight( )+vertStep, vertStep); if (yTrans != 0) // yes it could vertMoveMode = FALLING; // set it to be in falling mode } The test is carried out by passing the coordinates of the sprite's feet, plus a vertical offset downward, to checkBrickTop( ) in BricksManager. Vertical MovementupdateRising( ) deals with the updateSprite( ) event associated with the rising state, and tests for the two conditional transitions that leave the state: rising can stop either when upCount = MAX or when [will hit brick above] becomes true. Rising will continue until the maximum number of vertical steps is reached or the sprite hits the base of a brick. The sprite then switches to falling mode. checkBrickBase( ) in BricksManager carries out the collision detection: private void updateRising( ) { if (upCount == MAX_UP_STEPS) { vertMoveMode = FALLING; // at top, now start falling upCount = 0; } else { int yTrans = brickMan.checkBrickBase(xWorld+(getWidth( )/2), yWorld-vertStep, vertStep); if (yTrans == 0) { // hit the base of a brick vertMoveMode = FALLING; // start falling upCount = 0; } else { // can move upwards another step translate(0, -yTrans); yWorld -= yTrans; // update position upCount++; } } } // end of updateRising( ) updateFalling( ) processes the updateSprite( ) event associated with the falling state, and deals with the [will hit brick below] transition going to the not jumping state. checkBrickTop( ) in BricksManager carries out the collision detection. The other conditional leading to termination is not implemented since the bricks map cannot contain any holes for the sprite to fall through: private void updateFalling( ) { int yTrans = brickMan.checkBrickTop(xWorld+(getWidth( )/2), yWorld+getHeight( )+vertStep, vertStep); if (yTrans == 0) // hit the top of a brick finishJumping( ); else { // can move downwards another step translate(0, yTrans); yWorld += yTrans; // update position } } private void finishJumping( ) { vertMoveMode = NOT_JUMPING; upCount = 0; if (isStill) { // change to running image, but not looping yet if (isFacingRight) setImage("runningRight"); else // facing left setImage("runningLeft"); } } |