Table of ContentsCollision DetectionThe Enemy

Actors

Back when you developed RoadRun, one of the primary classes was Actor, a relatively simple object used to represent anything that could move around in the game. It contained the object's position (x, y), dimensions (width, height), and methods for drawing and cycling the object.

NOTE

Tip

The complete code and a working JAD/JAR for the following code are on the CD under the Chapter 10 source code directory "EnemyTest".

For a game such as Star Assault, you'll need to expand the original Actor object to handle your new physics, collision detection, and AI systems. This new Actor class will serve as the base class for the game's ships, enemy mines, turrets, fighters, and even the bullets these will fire.

The New Actor Class

The revised Actor class is simply an abstraction of much of the functionality in the Ship class; however, I've modified the movement code to use proper timing. Here are the changes needed to turn the Ship into a useful Actor base class for future development.

The first change is to the class name and type. I've renamed it to Actor (obviously) and declared it abstractwe'll see the abstract methods soon. I've also added in a fluff variable for use later in the movement code and a new constructor.

abstract public class Actor
{

   ...

   private long fluff = 0;

   /**
    * Constructs an Actor at position x, y facing direction d and setting
    * a maximum velocity and starting speed and thrust. (Note the
    * maxVelFPArg, speedFPArg and thrustFPArg must be MathFP values.)
    */
   public Actor(int x, int y, int d, int thrustFPArg, int speedFPArg,
                 int maxVelFPArg)
   {
      worldWidth = GameScreen.getGameScreen().getWidth();
      worldHeight = GameScreen.getGameScreen().getHeight(); 
      xFP = MathFP.toFP(x);
      yFP = MathFP.toFP(y);
      direction = d;

      maxVelFP = maxVelFPArg;
      thrustFP = thrustFPArg;
      // Set the Actor's current speed.
      setVel(speedFPArg);
   }
   /**
    * Changes the current speed of the actor by setting the velocity based on
    * the current direction.
    * @param speedFPArg The speed to travel at (as a MathFP value).
    */
   public final void setVel(int speedFPArg)
   {
      xVelFP = MathFP.mul(speedFPArg,
                            MathFP.cos(getRadiansFromAngle(direction)));
      yVelFP = MathFP.mul(speedFPArg,
                            -MathFP.sin(getRadiansFromAngle(direction)));

      // Cap the velocity to a reasonable level.
      if (xVelFP > maxVelFP)
         xVelFP = maxVelFP;
      else if (xVelFP < -maxVelFP) xVelFP = -maxVelFP;
      if (yVelFP > maxVelFP)
         yVelFP = maxVelFP;
      else if (yVelFP < -maxVelFP) yVelFP = -maxVelFP;
   }

Next we need to add in the abstract methods. You will need to implement these in any class that extends Actor.

   /**
    * Abstract method to render the Actor. You must implement this method
    * in order to use this class.
    */
   abstract public void render(Graphics graphics);
   /**
    * Abstract method to return the width of the Actor.
    */
   abstract public int getWidth(); 
   /**
    * Abstract method to return the height of the Actor.
    */
   abstract public int getHeight();

Next is the cycle method. For this Actor class we do the calculations for movement and then check collisions by calling back the GameScreen.

   /**
    * A cycle method that moves the Actor a distance relative to its current
    * speed (the value of the speed int) and the amount of time that has passed
    * since the last call to cycle (deltaMS). This code uses a fluff value in
    * order to remember values too small to handle (below the tick level).
    * @param deltaMS The number of milliseconds that have passed since the last
    * call to cycle.
    */   public void cycle(long deltaMS)
   {
      int ticks = (int) (deltaMS + fluff) / 100;

      // remember the bit we missed
      fluff += (deltaMS - (ticks * 100));

      if (ticks > 0)
      {
         int ticksFP = MathFP.toFP(ticks);

         // move the ship according to its current direction (in radians)
         int dirRadians = MathFP.div(MathFP.toFP(direction), FP_DEGREES_PER_RAD);

         int xAccFP = MathFP.mul(thrustFP, MathFP.cos(dirRadians));
         int yAccFP = MathFP.mul(thrustFP, -MathFP.sin(dirRadians));

         xVelFP = MathFP.add(xVelFP, xAccFP);
         yVelFP = MathFP.add(yVelFP, yAccFP);

         // cap our velocity to a controllable level
         if (xVelFP > maxVelFP) xVelFP = maxVelFP;
         else if (xVelFP < -maxVelFP) xVelFP = -maxVelFP;
         if (yVelFP > maxVelFP) yVelFP = maxVelFP;
         else if (yVelFP < -maxVelFP) yVelFP = -maxVelFP;

         int lastXFP = xFP;
         xFP = MathFP.add(xFP, MathFP.mul(xVelFP, ticksFP)); 
         // test for collisions, after x movement
         if (GameScreen.getGameScreen().checkCollision(this))
         {
            xVelFP = MathFP.mul(xVelFP, MathFP.toFP("-1"));
            xFP = MathFP.add(lastXFP, xVelFP);
         }

         int lastYFP = yFP;
         yFP = MathFP.add(yFP, MathFP.mul(yVelFP, ticksFP));

         // test for collisions, after y movement
         if (GameScreen.getGameScreen().checkCollision(this))
         {
            yVelFP = MathFP.mul(yVelFP, MathFP.toFP("-1"));
            yFP = MathFP.add(lastYFP, yVelFP);
         }

         // check our position and wrap around if we have to
         if (MathFP.toInt(xFP) < 0) xFP = MathFP.toFP(worldWidth-1);
         if (MathFP.toInt(xFP) > worldWidth) xFP = MathFP.toFP(0);
         if (MathFP.toInt(yFP) < 0) yFP = MathFP.toFP(worldHeight-1);
         if (MathFP.toInt(yFP) > worldHeight) yFP = MathFP.toFP(0);
      }
   }

You may notice that this class doesn't require a reference to the GameScreen when it's constructed. In previous code, I've used this reference to carry out collision detection. If you look at the revised cycle code you'll see a call to:

GameScreen.getGameScreen().checkCollision(this))

Revised Ship Class

Using the new Actor class as a base, you can now revise the Ship class. As you can see in the following code, things are much simpler now (and more organized). The main code that has been removed is the cycle method.

import net.jscience.math.kvm.MathFP;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.io.IOException;

public class Ship extends Actor 
{

The first thing you can see about this new Ship class is that we no longer need any of the position and movement code. That's all now handled by the Actor base class.

   private static int SHIP_FRAME_WIDTH = 16;
   private static int SHIP_FRAME_HEIGHT = 16;

   public static final int FP_22P5 = MathFP.toFP("22.5");

   private static ImageSet shipImageSet;  // the one and only imageset

   private Sprite shipSprite;

   public Ship(int startX, int startY)
   {

To properly construct the Ship you have to now call the Actor constructor first.

      super(startX, startY, 0, MathFP.toFP("0.2"), MathFP.toFP("0.0"),
            MathFP.toFP("2.0"));
      if (shipImageSet == null)
      {
         try
         {
            Image[] frames = ImageSet.extractFrames(Image.createImage("/ship.png"),
                                                        0, 0, 4, 4,
                                                        SHIP_FRAME_WIDTH,
                                                        SHIP_FRAME_HEIGHT);
            shipImageSet = new ImageSet(1);
            shipImageSet.addState(frames, 0);
         }
         catch (IOException ioe)
         {
            System.out.println("unable to load image");
         }
      }

      shipSprite = new Sprite(shipImageSet, 0, 0);
   }

   public void render(Graphics graphics)
   { 
      int frame = MathFP.toInt(MathFP.div(MathFP.toFP(getDirection()), FP_22P5));
      shipSprite.setFrame(frame);
      shipSprite.draw(graphics, getX(), getY());
   }

   public int getHeight()
   {
      return SHIP_FRAME_HEIGHT;
   }

   public int getWidth()
   {
      return SHIP_FRAME_WIDTH;
   }
}

Revised Game Screen

In the previous version of the Ship class, I passed a reference to the GameScreen object to know the size of the world (in order to detect when you hit the edge of the screen) and to check whether the Ship had collided with anything. This was getting a little cumbersome, so you'll notice in the revised Actor and Ship classes that I've switched to using a simpler system. Because there is only ever one instance of the GameScreen object, I simply have the object store a static reference to itself, and then use a static method to gain access to it from anywhere.

GameScreen has also been updated to use a Vector to contain all the actors in the game. This will serve you later when you start dynamically adding and removing objects from the game.

Here's the revised version of the class:


import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Graphics;
import java.util.Vector;

public class GameScreen extends Canvas
{

Note this code uses a Vector, not an array to store Actors. This is so we can dynamically add and remove Actors later on.

   private Vector actors;

This is a static reference to the one and only instance of the GameScreen (known as a sin gleton). This is initialized in the constructor and then accessed using the getGameScreen method.

   private static GameScreen theGameScreen;   // the one and only

   public GameScreen()
   {
      theGameScreen = this;

      // This code now uses a Vector instead of an array.
      actors = new Vector();

      // create 5 ships
      for (int i = 0; i < 5; i++)
         actors.addElement( new Ship() );
   }

A new static method to return the one and only instance of the GameScreen object.

   public final static GameScreen getGameScreen()
   {
      return theGameScreen;
   }

The main difference in the revised paint and cycle methods is to use the Actor vector, rather than an array.

   protected void paint(Graphics graphics)
   {
      graphics.setColor(0);
      graphics.fillRect(0, 0, getWidth(), getHeight());

      for (int i = 0; i < actors.size(); i++)
         ((Actor)actors.elementAt(i)).render(graphics);
   }

   public void cycle()
   {
      for (int i = 0; i < actors.size(); i++)
         ((Actor) actors.elementAt(i)).cycle(100);
   }

   public boolean checkCollision(Actor s)
   { 
      // did this ship hit another?
      for (int j = 0; j < actors.size(); j++)
      {
         Actor another = (Actor)actors.elementAt(j);
         if (another != s &&
             Tools.isIntersectingRect(s.getX(), s.getY(), s.getWidth(), s.getHeight(),
                                        another.getX(), another.getY(),
another.getWidth(), another.getHeight()))
             return true;
      }
      return false;
   }

}

Adding Weapon Fire

Flying your ship around is fun, but to get things really moving, you can add some weapon fire! With your revised Actor class, this is now surprisingly easy. To start, you need to create a type of actor for the missiles fired by your shipsthe Bullet class. When a ship fires, you'll construct a new Bullet object with a starting direction and speed to make it look like the bullet is flying out from the front of the ship.

NOTE

Tip

Constructing objects under J2ME is extremely slow. In Chapter 11, "The World," you'll see how to use object pooling to avoid this delay.

The first thing you need to do is figure out where the Bullet object should start in the world. At first you might think this is just the position of the ship; however, it will look a bit strange if the bullet starts inside the body of the ship. You also need to start bullets at different positions if you want to implement something like dual forward firing weapons.

To figure out where your bullet should start, you need to be able to pick a starting point and then project it along a particular angle path for a certain distance. As you can see in Figure 10.18, if you want to fire your weapon you project from the center of the ship along its current direction so your bullet can start slightly in front.

Figure 10.18. To find the point where a Bullet originates, you project outwards from the center of the ship in the direction it's facing.

graphic/10fig18.gif


The code to do this is basically exactly the same as your previous motion code. You just start at a certain position and then apply your movement code to the distance you need to project. Here's the new method for the Actor class (it can be static since it doesn't rely on any instance data in the Actor class):

   /**
    * Projects a point starting at x, y outwards at an angle for the specified
    * distance. The result is an array of two integer with the x and y point
    * of the projected point.
    * @param startX The starting x position.
    * @param startY The starting y position.
    * @param angleToProjectAt The angle to project along.
    * @param distanceToGo The distance to go.
    * @return An array of 2 integers with the x and y position of the projected
    * point.
    */
   public final static int[] getProjectedPos(int startX, int startY,
                                                 int angleToProjectAt,
                                                 int distanceToGo)
   {
      int angleInRadians = MathFP.div(MathFP.toFP(angleToProjectAt),
                                         FP_DEGREES_PER_RAD);

      int dx = MathFP.cos(angleInRadians);
      int dy = -MathFP.sin(angleInRadians);

      int xFP = MathFP.toFP(startX);
      int yFP = MathFP.toFP(startY);
      int distanceFP = MathFP.toFP(distanceToGo);

      xFP = MathFP.add(xFP, MathFP.mul(dx, distanceFP));
      yFP = MathFP.add(yFP, MathFP.mul(dy, distanceFP));

      int[] result = {MathFP.toInt(xFP), MathFP.toInt(yFP)};
      return result;
   }

Now that you know how to find the origin point of a new bullet, you can create the bullet itself. This class is very similar to the Ship actor; I've just added specific drawing code, changed the size returned by the getWidth and getHeight methods, and added some code to limit each Bullet's lifetime.

import net.jscience.math.kvm.MathFP;
import javax.microedition.midlet.*; 
import javax.microedition.lcdui.*;
import java.io.IOException;

/**
 * A Bullet Actor is a simple flying object fired from a Ship. It has a limited
 * lifetime of 3 seconds using code in the cycle method after which the object
 * is removed from the GameScreen.
 * @author Martin J. Wells
 */
public class Bullet extends Actor
{

The Bullet class uses its own graphics through an ImageSet and Sprite class.

   private static ImageSet bulletImageSet;  // the one and only imageset
   private Sprite bulletSprite;

The constructor is basically the same as the Ship except it has different velocity and thrust values. This code then loads up the graphics from the general.png image file and breaks out the four frames each three pixels square.

   /**
    * Creates a new Bullet at the specified starting position and facing
    * direction. This is called by the Ship class when the player hits the fire
    * key.
    * @param startX The starting x position.
    * @param startY The starting y position.
    * @param direction The starting direction.
    */
   public Bullet(int x, int y, int direction)
   {

Note in the following call to the Actor constructor the Bullet does not have any thrust, only velocity. Because it has no thrust the direction value has no real effect (since there's no "push" behind that direction). When the bullet then strikes something, it will apply the bounce change to the velocity which will cause it to go the other way. If you were to set a thrust value, the bullet would hit the wall, bounce away, then fly right back at the wall again, and again, and again (the same as the ship does).

      super(x, y, direction, MathFP.toFP("0.0"), MathFP.toFP("2.0"),
            MathFP.toFP("2.0"));

      if (bulletImageSet == null)
      {
         try
         { 
            Image[] frames = ImageSet.extractFrames(Image.createImage("/general.png"),
                                                        0, 0, 4, 1, 3, 3);
            bulletImageSet = new ImageSet(1);
            bulletImageSet.addState(frames, 0);
         }
         catch (IOException ioe)
         {
            System.out.println("unable to load image");
         }
      }

      bulletSprite = new Sprite(bulletImageSet, 0, 0);
   }

The render method is basically the same as the Ship except that the bullet graphic doesn't face a particular direction so you don't need to worry about setting that.

   /**
    * The render method is basically the same as the Ship except that the bullet
    * graphic doesn't face a particular direction so you don't need to worry
    * about setting that.
    * @param graphics The graphics context upon which to draw the bullet.
    */
   public void render(Graphics graphics)
   {
      bulletSprite.draw(graphics, getX(), getY());
   }

The cycle method calls the base class cycle (Actor) which handles the movement, then we cycle the Sprite object so it will animate (progress to the next frame) before checking the amount of time the object has been around and destroy it by calling the GameScreen.removeActor method (we'll see the removeActor method next).

   /**
    * The cycle method calls the base class cycle (Actor) which handles the
    * movement, then we cycle the Sprite object so it will animate (progress to
    * the next frame) before checking the amount of time the object has been
    * around and destroy it by calling the GameScreen.removeActor method.
    * @param deltaMS The amount of time that has passed since the last call to
    * cycle (in milliseconds).
    */
   public void cycle(long deltaMS)
   {
      super.cycle(deltaMS); 
      bulletSprite.cycle(deltaMS);

      //Tterminate this bullet if it's older than 3 seconds
      // See below for the GameScreen method this code calls.
      if (bulletSprite.getTimeInCurrentState() > 3000)
         GameScreen.getGameScreen().removeActor(this);
   }

A Bullet is a tiny object in the game so we give it a size of one pixel square.

   public int getHeight() { return 1; }
   public int getWidth() { return 1; }
}

This is about as simple as an Actor class object can get. One interesting addition, though, is the use of the Sprite getTimeInCurrentState method to give your Bullet objects a lifetime. If this time passes three seconds (3000 milliseconds), you use the GameScreen method to remove the bullet from the world.

The next step to getting firing to work is to register the fire key press and then have the Ship object respond by firing a bullet. A simple way to do this would be to just construct a Bullet object each time the key is pressed, however this means players will be able to fire every time they hit a key (with no limits on their rate of fire). The same would apply to enemy ships which would just rapidly blast away at the player without limits.

To implement a fire rate limiter we first need to add some member to the Ship class to track whether the ship is firing (the firing boolean), when the last shot took place (timeLastFired), and how long this ship has to wait between shots (firingDelay).

public class Ship extends Actor
{
   // Weapons fire.
   private boolean firing;            // Is the ship currently firing?
   private int firingDelay=500;       // The delay between shots (in ms)
   private long timeLastFired;        // Used to track when I can fire again

The next step is to add code to the cycle method to detect when the firing flag is true, check the time to see if enough has elapsed before the next shot can be made, and then to construct the actual Bullet object.

   public void cycle(long deltaMS)
   {
      ...
      if (firing)
      { 
         // Calculate the amount of time that has passed. If it's greater than
         // the firing delay then the Ship is clear to fire again.
         long timeSinceLastFire = (System.currentTimeMillis() - timeLastFired);
         if (timeSinceLastFire > firingDelay)
         {
            int[] nosePos = Actor.getProjectedPos(getX()+ (SHIP_FRAME_WIDTH/2),
                                                      getY()+ (SHIP_FRAME_HEIGHT/2),
                                                      getDirection(), 12);

            // Add the new bullet actor to our world.
            GameScreen.getGameScreen().addActor(
               new Bullet(nosePos[0], nosePos[1], getDirection()) );

            // Update the time the last fire took place (now).
            timeLastFired = System.currentTimeMillis();
         }
      }
   }

Since you'll also need a way of triggering the firing process a method needs to be added to the Ship class.

   public final void setFiring(boolean b)
   {
      firing = b;
   }

The final step is to trigger firing when the player hits the up arrow by setting the firing boolean to true, and then turn off firing when the key is released. This means the ship will continue to fire while the key is down.

public class GameScreen extends Canvas
{
   ...

   protected void keyPressed(int keyCode)
   {
      ...

      if (action == UP)
         playerShip.setFiring(true);
   }

   protected void keyReleased(int keyCode)
   { 
      ...

      if (action == UP)
         playerShip.setFiring(false);
   }

    Table of ContentsCollision DetectionThe Enemy