Table of ContentsGame StateDealing with Damage

The Primary Actor

Star Assault involves the player piloting his ship around an alien maze of doom, wreaking as much havoc as possible. His ship, a Hurricane-class deep-space assault fighter, is just another type of actor. However, because it's the player, there's a clear relationship between the ship and the game screen (the controller of game play).

The first thing you need to do is create the player's ship and ensure that you keep a solid reference to it. You can do all this inside the GameScreen constructor.

private Ship playerShip;
public GameScreen()
{
    // create the world and playerShip
    world = new World(screenWidth, screenHeight);

Note here I'm using a new version of the Ship constructor that takes only the World argument. The init method now takes care of setting the type and position. You can see a complete example in the final Star Assault source code on the CD.

    playerShip = new Ship(world);
    playerShip.init(Ship.PLAYER_SHIP, 0, 0);

    world.setPlayerShip(playerShip);

    ...
}

You need to add a setPlayerShip method (and corresponding field) to the World class for this to work.

/**
 * @param a The Actor object that represents the player in the game.
 */
public final void setPlayerShip(Ship a)
{
    playerShip = a;
}

The World class now differentiates between regular actors and the primary, so you need to modify the rendering, cycling, and collision detection to accommodate this. For example, you'll need to specifically render the player actor last in the World render method.

Notice the PLAYER_SHIP constant I'm using in the init method called from the revised Game Screen constructor above? This is so you can differentiate between the different types of ships you want. Next you'll see how to create player (and enemy) ship types.

Ship Types

In Star Assault you have a number of different types of ship class actors flying around the game. The simplest is the drone; it's something like a floating mine. The challenge for the player is to simply avoid hitting it. This is a good type of enemy for introducing the player to the game because it doesn't fight back. The next enemy type, the turret, is a stationary gun that is able to turn 360 degrees and fire at the player. The turret can detect the player's presence and turn toward him. Finally, the fighter is just like a turret except it can fly around as well. The player's ship is basically the same as the fighter, but with slightly modified specs to make it a little bit faster and more maneuverable.

If you were developing this in a normal environment, you would use quite a few classes to represent all these capabilities. For example, I would create a TargetingActor class to represent the functionality of an actor being able to detect and target the player's ship. I would then extend this class to create TurretActor and FighterActor classes, and then I would add all the specific code for these ship types into overridden methods (drawing, special physics, custom AI). Finally, I'd create a HurricaneActor class to represent the player's ship. Wouldn't it be lovely? Unfortunately, I just wasted 5 to 10 KB of JAR space just on classes.

Classes are very expensive in terms of resources; you really have to think twice about creating one, which unfortunately means you have to ditch some of the tried and true practices of object-oriented development (such as extensive inheritance trees). For Star Assault you already have more classes than you'd think (GameScreen, Actor, ActorPool, Sprite, ImageSet, Ship, Bullet, Tools and the World), and you haven't yet added menus, splash screens, or resource handlers. Without even blinking, you can have 20 classes, minimum. Creating five classes just to represent different ship types isn't practical.

To get around this, you can go back to the tried and true method of using a type indicator to change the type of class being instantiated. This typically results in a bunch of nested if and switch blocks spread throughout the code, which, as you can imagine, can quickly turn into a mess. If things start to get too unruly, consider adding another class or two if you really think it's worth it.

Adding types to your actors is pretty easy. You simply put a type field and methods to access it in the Actor class.

abstract public class Actor
{

    ...
    public static final int SHIP_START = 0;

If you later want to move the ship indices for some reason (such as making room for other actor types), you can do so simply by adjusting this SHIP_START value.

public static final int PLAYER_SHIP = SHIP_START + 1;

The following order of ship types is important. For example, to detect if a ship is an enemy, you test if its type value is between the ENEMY_SHIP_START and ENEMY_SHIP_END values.

public static final int ENEMY_SHIP_START = SHIP_START + 2;
public static final int ENEMY_AI_START = SHIP_START + 3;

public static final int ENEMY_FIGHTER = SHIP_START + 4;
public static final int SHIP_END = SHIP_START + 5;

Notice I've also added a new range type ENEMY_AI_START.This is used later in the cycle code to only carry out AI operations for enemy ships. If you later add different enemy types (such as those in the next section), you can change whether they use the AI code, or remain dumb.

    public static final int BULLET_START = 1000;
    public static final int PLASMA_CANNON = BULLET_START + 1;
    public static final int BULLET_END = BULLET_START + 2;

    private int type;    // type of actor (instead of subclassing)

    public final int getType()
    {
        return type;
    }

    public final void setType(int type)
    {
        this.type = type;
    }

    ...

}

Are you wondering why I'm doing this in the Actor class and not the Ship class? This is so you can differentiate between all Actor class types without having to use the unbelievable slow instanceof operator. There are plenty of cases in which you'll want to know the type of a particular actor (most importantly, whether it's a Bullet or Ship class). Using the instanceof operator for this is a waste of precious CPU power. By moving the type into the Actor class, you can differentiate any class of Actor, as well as its subtypes. Add some methods to the Actor class to help with this.

public final boolean isEnemyShip()
{
    return (type > ENEMY_SHIP_START && type < SHIP_END);
}

public final boolean isBullet()

{
    return (type > BULLET_START && type < BULLET_END);
}

You can now determine whether an Actor is a Bullet or Ship simply by calling these methods, rather than using instanceof. In the section "The Shield Effect" later in this chapter, you'll see how you use this method to detect the type of collision that occurs.

Now that you have your type set up, you need to adjust the Ship class to take this into account. The following init method adds support for the four distinct types of ships in the game.

public class Ship extends Actor
{
    ...

    /**
     * Initialize an instance of a ship. May be called at any time to reuse this
     * instance of the object as another type.
     */
    public final void init(int typeArg, int x, int y)
    {

This method starts by setting up some default values used by all the ship types.

int firingDelay = 500;
int alignedDiv = 16;
int thrust = 0;
int speed = 0;
int maxVel = 0;
int dir = 0;
int bounceVel = Actor.FP_MINUS_05;
int maxSpinRate = 23;

Next the actor type is set, then based on this type you make changes to the default values. The enemy fighter for example has a slower speed, maximum velocity, firing rate, and thrust than the player's ship.

        setType(typeArg);

        switch (getType())
        {
            case PLAYER_SHIP:
                shipSprite = new Sprite(yellowShipImageSet, 0, 0);
                speed = 2;
                maxVel = 2;
                thrust = 2;
                break;
            case ENEMY_FIGHTER:
                shipSprite = new Sprite(redShipImageSet, 0, 0);
                speed = 1;
                maxVel = 1;
                thrust = 1;
                firing = true;
                firingDelay = 2000;
                break;
        }
        super.init(null, x, y, thrust, speed, maxVel, dir, alignedDiv,
                    bounceVel, maxSpinRate);
    }

    ...

}

Expanding the Enemy

Now that you have different ship types you can add some extra flavor to the game by introducing the Drone (a dumb floating mine) and the Turret (a sort-of fixed position fighter that will turn and shoot at the player). To support this you'll need to add extra types to the Actor class.

public static final int ENEMY_SHIP_START = SHIP_START + 2;
public static final int ENEMY_DRONE = SHIP_START + 3;
public static final int ENEMY_AI_START = SHIP_START + 4;
public static final int ENEMY_TURRET = SHIP_START + 5;
public static final int ENEMY_FIGHTER = SHIP_START + 6;
public static final int SHIP_END = SHIP_START + 7;

Notice that I've left the ENEMY_DRONE outside of the ENEMY_AI_START range. This will ensure the drones don't come chasing after you!

The next change you need is to load up graphics to represent the new enemy. For the mine, I've used a six-frame spinning globe animation and a 16-direction turret gun looking thing (that's the technical name) for the turret.

Since all the code to load up and prepare the images is getting a little large, it's best to move it out into a separate Ship class setup method.

public final static void setup()
{
    Image shipImage = ImageSet.loadClippedImage("/ship.png", 0, 0);

Here is the new code to load up the drone images from the separate file mine.png. The turret frames are already within the ship.png file.

Image droneImage = ImageSet.loadClippedImage("/mine.png", 0, 0);

yellowShipImageSet = new ImageSet(16);
redShipImageSet = new ImageSet(16);
turretImageSet = new ImageSet(16);
droneSet = new ImageSet(1);

Image[] yellowDirImages = null;
Image[] redDirImages = null;
Image[] turretImages = null;

The drones and turret images are then extracted from the file images. The turret uses 16 states to represent the 16 directions it can face. The mine, however, only faces one direction, but has six frames of animation. The following code extracts these six frames (arranged in two rows of three tiles) and then sets the ImageSet to animation a complete cycle over an 800 millisecond period.

Image[] droneImages = ImageSet.extractFrames(droneImage,
                            0, 0, 3, 2, 16, 16);
droneSet.addState(droneImages, 800);

yellowDirImages = ImageSet.extractFrames(shipImage, 0, 0, 4, 4, 16, 16);
redDirImages = ImageSet.extractFrames(shipImage, 4 * 16, 0, 4, 4, 16, 16);
turretImages = ImageSet.extractFrames(shipImage, 8 * 16, 0, 4, 4, 16, 16);

for (int i = 0; i < 16; i++)
{
    Image[] yellowFrame = {yellowDirImages[i]};
    yellowShipImageSet.addState(yellowFrame, 0);

    Image[] redFrame = {redDirImages[i]};
    redShipImageSet.addState(redFrame, 0);

This is where the turret images are initialized in the same way the ship images are.

        Image[] turretFrame = {turretImages[i]};
        turretImageSet.addState(turretFrame, 0);
    }
}

Next you need to add initialization code to the Ship class's init method to support the two new ship types.

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

    switch (getType())
    {
        ...

        case ENEMY_DRONE:
            shipSprite = new Sprite(droneSet, 0, 0);
            alignedDiv = 0;
            dir = Tools.getRand(0, 359);
            speed = maxVel = 1;
            thrustFP = 0;
            bounceVelFP = Actor.FP_MINUS_1;
            break;
        case ENEMY_TURRET:
            rechargeRate = 0;
            speed = 0;
            maxSpinRate = 5;
            firingDelay = 2000;
            shipSprite = new Sprite(turretImageSet, 0, 0);
            firing = true;
            break;
   }

   ...

   super.init(null, x, y, thrustFP, MathFP.toFP(speed),
               MathFP.toFP(maxVel), dir, alignedDiv,
               bounceVelFP, maxSpinRate);
}

To draw the new enemy types, you'll also need to update the Ship class's render method to detect the type and use the correct graphics.

public final void render(Graphics g, int offsetX, int offsetY)
{

Here's where you handle the special case enemy drone image; in all other cases you use the current ship sprite and set the direction to correspond to the correct frame. For the enemy drone, the sprite animation code will take care of animating the image as long as you call the sprite's cycle method.

    if (getType() != ENEMY_DRONE)
    {
        int s = MathFP.toInt(MathFP.div(MathFP.toFP(getDirection()),
                                  Actor.FP_225));
        if (s != shipSprite.getCurrentState())
            shipSprite.setState(s, false);
    }

    shipSprite.draw(g, getX() - offsetX, getY() - offsetY);
}

The drone relies on you calling the sprite object's cycle method so you'll need to add that call to the Ship classes cycle method.

public final void cycle(long elapsedTime)
{
    super.cycle(elapsedTime);

    if (getType() == ENEMY_DRONE)
        shipSprite.cycle(elapsedTime);
    ...
}

For a complete example of the new enemy types check the Star Assault project on the CD.

    Table of ContentsGame StateDealing with Damage