Table of ContentsThe Primary ActorSaving and Loading

Dealing with Damage

I've got a personal thing about games in which you die due to a hit by a lone stray bullet. It's just not fair, damn it! (Smacks the machine.)

In Star Assault, you'll give the ships an extra chance by adding shielding. When the ship takes damage from collisions with enemy ships or bullets, you reduce the shield level by varying amounts. It's only when the energy level of the shield falls below zero that things get fatal. To make it fair, you'll give shields to the enemy as well (although they won't be as strong, of course).

Adding Energy

The first step to adding shielding is to add a current energy level to the Ship class.

private int energy;                    // current reactor core energy level
private int energyMax;                 // max level
private int rechargeRate;              // recharge rate (energy per sec)
private int msPerRecharge;             // how many MS to equal one recharge
private long msSinceLastRecharge;      // multicycle counter for recharge

A little overly complicated just for an energy level, isn't it? That's because you're going to make things a little more interesting by recharging the shields slowly over time. This lets the player take damage and, if required, withdraw from the fight for a moment (letting the shields power back up) before returning for more action.

The energy field represents the current level of shield power. The maximum level you'll let this reach is the current energyMax.The rechargeRate represents how fast you'll re-energize the shields. You use the other two fields to manage the recharge process.

The first thing you need to do to make this work is initialize the shield level. If you look at the code in the previous section, you'll notice I snuck it in already. The init method sets up the energy and energyMax fields based on the type of ship. You also set the recharge rate, along with the msPerRecharge field.

To recharge the shields, you modify the cycle code for the Ship to increment the energy level based on the recharge rate over time.

public final void cycle(long deltaMS)
{
    ...

    if (rechargeRate > 0 && energy < energyMax)
    {
        if (msSinceLastRecharge < msPerRecharge)
            msSinceLastRecharge += deltaMS;
        else
        {
            energy++;
            msSinceLastRecharge -= msPerRecharge;
            if (energy > energyMax) energy = energyMax;
        }
    }
}

Note that this code will give a shield to all ship types, including turrets and drones. The strength of that shield is set in the Ship class's init method based on the type. For example:

public final void init(int typeArg, int x, int y)
{
    ...

    energyMax = 1000;
    energy = energyMax;
    rechargeRate = 50;

    switch (getType())
    {
           ...

The ships all use the default values, except the enemy turret, which has a shield but no recharge.

        case ENEMY_TURRET:
            ...
            rechargeRate = 0;
            energyMax = 200;
            break;
    }

    if (rechargeRate > 0)
        msPerRecharge = 1000 / rechargeRate;

    ...
}

For convenience later, you should also add some access methods for the maximum and current energy levels.

public final int getEnergy()
{
    return energy;
}

public final int getEnergyMax()
{
    return energyMax;
}

The Shield Effect

Because the ships no longer explode when they're hit by weapons fire (or other ships), you need to give the player some other indication of the impact. To do this, you flash a circular shield that slowly fades away around the ship. The image frames for the shield effect are within the ship PNG (shown in Figure 12.1).

Figure 12.1. The ship.png also contains the four frames for the shield effect in the lower left-hand corner.

graphic/12fig01.gif


To add the shield effect to the Ship class, you first need to load the images and create a sprite.

public class Ship extends Actor
{
    ...

    private static ImageSet shieldImageSet;
    private Sprite shieldSprite;

    ...
    public Ship(World worldArg)
    {
        super(worldArg);

        // load up the images (static) if we haven't done so already
        if (shieldImageSet == null)
            setup();
    }

    public final static void setup()
    {
        Image shipImage = ImageSet.loadClippedImage("/ship.png", 0, 0);
        Image[] shieldImages = null;
        shieldImages = ImageSet.extractFrames(shipImage, 0, 4 * 16, 4, 1, 20, 20);
        shieldImageSet.addState(shieldImages, 200);

        // init other images for ship
        ...
    }

    ...

}

Now you have the images ready, so it's time to get your shield effect going. The trigger you use is whenever the ship takes damage. You set a flag to indicate that you want to display the shields, and then use the sprite state to indicate when you should turn them off. For example, here's a new method for the Ship class:

public final void takeDamage(int damageLevel)
{
    energy -= damageLevel;
    showShields = true;
}

This method is also decrementing the energy level based on the damage done by an impact. Typically, you call this method from the collision notifier (recall that the collision notifier is called from the Actor class whenever a collision takes placecheck the Star Assault Actor class cycle method for an example).

public final void onCollision(Actor actorItHit)
{
    if (actorItHit == null) return;

First you need to test that the owner (the actor that created this object) is not the same. This stops you from hitting your own bullets. The code then tests if the actor that this actor hit is a bullet and reacts (takes damage).

if (actorItHit.getOwner() != this)
{
    if (actorItHit.isBullet())
    {
        Bullet bulletItHit = (Bullet) actorItHit;
        takeDamage(bulletItHit.getDamage());
        actorItHit.setCollidable(false);
    }
}

In a case where any of the ships collide, this ship takes a big damage hit. For most enemy types this will be fatal.

    // if we hit an enemy ship, take a big hit
    if (!a.isBullet())
        takeDamage(250);

}

Now you need to react to the state of the showShields Boolean set in the takeDamage method. The first thing to do is draw the shield sprite when you display the ship in the Ship class's render method.

public final void render(Graphics g, int offsetX, int offsetY)
{
    // draw the 20 x 20 shield sprite underneath the 16 x 16 ship
    // sprite (the edges will stick out)

    if (showShields)
        shieldSprite.draw(g, getX() - offsetX, getY() - offsetY);

    // draw the ship
    ...

}

Next you need to turn the shields off when they have gone through the cycle once (the flash and then fade sequence). To do this, you can use the Sprite class's getTotalCycles method. As soon as this reaches 1, you know you're finished. Again, this all belongs in the Ship class.

public final void cycle(long deltaMS)
{
    ...

    if (showShields)
    {
        shieldSprite.cycle(deltaMS);

        if (shieldSprite.getTotalCycles() > 0)
        {
            shieldSprite.reset();
            showShields = false;
        }
    }
}

Energy Bar

Players now have to manage the level of shield energy they have left. Letting it get below zero will result in destruction, so you should add an energy bar to give the player a visual indicator as to the current level of shielding. An example of this is illustrated in Figure 12.2. The small green bar in the bottom-left corner of the screen is the current shield level.

Figure 12.2. An energy bar indicates the player ship's current shield level.

graphic/12fig02.gif


To add an energy bar, you modify GameScreen's rendering code to draw a few filled rectangles. Because drawing rectangles is not particularly fast, you need to use a small image to cache the results of the drawing. It's much faster to just draw the image (instead of the rectangles). If the bar value changes, you redraw the cached image. Here's the GameScreen code to do this (note that I've reorganized the GameScreen class by moving initialization code into a new initResources method):

private void initResources()
{
    // create the cache image for the energy bar (width is kept relative
    // to the size of the screen)

    barWidth = screenWidth / 5;
    barHeight = 6;
    energyBarImage = Image.createImage(barWidth, barHeight);

    ...

}

/**
 * update the energy bar image
 */
private final void updateEnergyBar(int barFill, boolean showRed)
{
    Graphics g = energyBarImage.getGraphics();

    // clear the background
    g.setColor(0);
    g.fillRect(0, 0, energyBarImage.getWidth(), energyBarImage.getHeight());

    // draw a white(ish) border
    g.setColor(0xaaaaaa);
    g.drawRect(0, 0, barWidth, 6);
    if (showRed)
        g.setColor(0xcc2222);
    else
        g.setColor(0x22cc22);

    g.fillRect(1, 1, barFill, 5);
}
private final void renderWorld(Graphics graphics)
{
    ...
    // draw the playerShip energy bar
    int pixelsPerEnergyPoint = playerShip.getEnergyMax() / barWidth;
    int barFill = playerShip.getEnergy() / pixelsPerEnergyPoint;

If the bar hasn't changed value (in terms of the number of pixels to draw), then we don't need to redraw the image.

if (lastDrawnBarValue != barFill)
{

The updateEnergyBar has a parameter to draw the bar in red. This is set to true in the method call below if the player's shields are below 50 percent. I've used a cheap method to do this without fractions by dividing the max energy level by the current energy. If the result (rounded down) is greater than one then they have less than half of their energy left.

        updateEnergyBar(barFill, playerShip.getEnergy() > 0 &&
                                   playerShip.getEnergyMax() /
                                   playerShip.getEnergy() > 1);
        lastDrawnBarValue = barFill;
    }
    graphics.drawImage(energyBarImage, 6, screenHeight - 12,
                         Tools.GRAPHICS_TOP_LEFT);

    ...
}

Dealing with Death

There's a slight issue with the current game play in Star Assault.Although your shielding system now absorbs damage, there are no consequences for that energy level falling below zero.All the ships just keep flying. What you need to do is properly handle the death of both the player and enemy ships.

Like shielding, the first step in this process is to give some visual feedback that the ship is going down for the count. To do this, you draw an explosion sprite in place of the ship when it reaches this point. After the explosion has faded away, you can deal with the results.

Figure 12.3 shows the explosion sprite you'll use; it's a simple sequence of frames showing a fireball that fades away slowly.

Figure 12.3. The frames of the explosion sprite.

graphic/12fig03.gif


To use the explosion sprite, you need to add an explosion image set and initialize the images in the Ship class's static setup method.

public class Ship extends Actor
{
    private static ImageSet explosionSet;

    public final static void setup()
    {
        Image explosionImage = ImageSet.loadClippedImage("/explode.png", 0, 0);
        Image[] explosionImages = ImageSet.extractFrames(explosionImage,
                                                         0, 0, 1, 4, 25, 25);
        explosionSet.addState(explosionImages, 500);
        ...

Just like you did with shields, add a Boolean to indicate when the ship is in the exploding state and set it in the Ship class's takeDamage method.

public final void takeDamage(int damageLevel)
{
    energy -= damageLevel;
    showShields = true;

    if (energy < 0)
    {
        exploding = true;
        showShields = false;
    }
}

Once you have the Boolean set, you can use it when rendering the ship as a special case in the Ship class's render method.

public final void render(Graphics g, int offsetX, int offsetY)
{
    if (exploding)
        explodingSprite.draw(g, getX() - offsetX - 2, getY() - offsetY - 2);
    else
    {
        // normal ship drawing code
        ...

Hopefully this is all getting familiar now. Next you can use the cycle method to trigger the consequences of the ship's death. For the non-player ships, you just ask the world to remove the ship. You then notify the game screen that a ship has died, and it will handle the adjustment to the scoring or cases in which the player's ship has died. Here's an update Ship cycle method.

public final void cycle(long deltaMS)
{
    ...

    if (exploding)
    {
        explodingSprite.cycle(deltaMS);

        if (explodingSprite.getTotalCycles() > 0)
        {
            explodingSprite.reset();

            if (getType() != PLAYER_SHIP)
                // if this is not the player ship then just have the
                // world release the ship back into the pool
                world.releaseShip(this);

            else
            {
                // big vibration if player died
                Tools.vibrate(10, 800);

                // tell the game screen about it
                GameScreen.getGameScreen().notifyShipDied(this);
            }
        }
    }

    ...
}

Your ships will now explode with a rather gratifying animation. Next take a look at how the game screen should handle the death of these ships properly.

To make things a little more challenging for the player, you can implement a limited number of lives. When the player has gotten himself destroyed more than the allotted number of times, you'll transition to a "game over" state.

To implement this, you just add a lives counter field and the notifyShipDied handler to the GameScreen class.

private int lives=3;

public void notifyShipDied(Ship ship)

{

    if (ship == playerShip)

    {

        lives--;

        if (lives == 0)

        {

            setState(GAME_OVER);

        }

        else

        {

            world.restart();

            setState(DYING);

        }

    }

}

The World class method restart is required to reset the level back to a starting state. For example:

/**
 * Restart the level
 */
public void restart()
{
    // reset all the ships (used only of course)
    Ship s = (Ship) enemyShipPool.getFirstUsed();
    while (s != null)
    {
       Actor next = s.getNextLinked();

       // Final check used to remove any inactive or exploding actors.
        // This can happen sometimes if actors were not given enough time
        // to complete their death sequence before this restart method was
        // called. For example, if the player collides with an enemy ship
        // before dying it won't have time to finish its exploding state and
        // suicide before we get this call in here. Without this check we
        // could end up with floating, half-dead phantom objects.
        if (!s.isVisible() || s.isExploding())
            releaseShip(s);
        else
            s.reset();

        s = (Ship) next;
    }
    playerShip.reset();

    // release all the bullets
    Actor a = bulletPool.getFirstUsed();
    while (a != null)
    {
        Actor next = a.getNextLinked();
        releaseBullet((Bullet) a);
        a = next;
    }
}

I've introduced two new game states hereDYING,which you use to delay restarting play a little, and GAME_OVER, as a result of the player running out of lives. Handling these two new states is relatively simple. First you draw the game over message on the screen, if required. Here's an example of the code you would add to the GameScreen's renderWorld method.

private final void renderWorld(Graphics graphics)
{
    ...

    if (state == GAME_OVER)
    {
        graphics.setColor(0x00ffcc66);
        graphics.setFont(defaultFont);
        graphics.drawString("GAME OVER", getWidth() / 2, getHeight() / 2,
                              Tools.GRAPHICS_TOP_LEFT);
    }
}

Next you add the revised cycle code for GameScreen.Here's where you really deal with these state changes. Notice that after you display GAME OVER for a little while, you call an activateMenu method; I'll cover this in the next chapter. (I think you can imagine what it does, though.)

public void run()
{
    try
    {
        while (running)
        {

            ...

            if (state == PLAYING)
                world.cycle();

            // if we're in the starting up state, wait a moment then
            // progress to playing
            if (state == STARTING_UP)
            {
                long timeSinceStateChange = System.currentTimeMillis() 
                                              timeStateChanged;
                if (timeSinceStateChange > 3000)
                    setState(PLAYING);
            }

            if (state == GAME_OVER)
            {
                long timeSinceStateChange = System.currentTimeMillis() 
                                              timeStateChanged;
                if (timeSinceStateChange > 3000)
                {
                    setState(GAME_DONE);
                    theMidlet.activateMenu();
                }
            }

            // if they died we wait 1 second before restarting play
            if (state == DYING)
            {
        long timeSinceStateChange = System.currentTimeMillis() 
                                      timeStateChanged;
        if (timeSinceStateChange > 1000)
            setState(STARTING_UP);
    }

    ...

That's about it. If you were to play through this game now, you'd find it feels much more like a typical action game. You now have the challenge of staying alive for as long as possible.

    Table of ContentsThe Primary ActorSaving and Loading