Table of ContentsActorsConclusion

The Enemy

One of the things that makes action games fun is battling enemies. Whether they are German U-boats, backpack-nuke-wielding terrorists, or Horde space fighters, there's a certain satisfaction in blowing them away.

However, you can't just throw some red fighters in Star Assault. The enemy has to be able to detect the presence of the player and then react in a semi-intelligent way. Take the case in which your intrepid player flies by an enemy turret. What should the turret do in reaction to the player? To begin with, it needs to detect the player's presence within a certain range. If the player is within that range, the turret should figure out which direction it should turn to best aim at the player, and then fire if the angle is close enough. The following sections will show you how to code all of this.

The end result will be a cool little demo where an enemy ship will follow the player's ship around the screen. You can check out the result in the Chapter 10 source code directory under "Enemy Test".

Computing Distance

The first thing your AI needs to be able to do is figure out how far away the player is. If the distance is beyond a certain limit, the turret doesn't need to do anything because there's nothing to react to. You need to figure the distance between the two points. You can do this by using the Pythagorean Theorem, which states that the square of the hypotenuse is the sum of the squares of the other two sides. That is:

c[2] = a[2] + b[2]

Now, if you recall that any two points (x1, y1) and (x2, y2) form a right triangle, you can then calculate the length of the a side (the x-axis side) as x2x1 and the length of the b side (the y-axis side) as y2y1. You can see this illustrated in Figure 10.19.

Figure 10.19. You can compute the distance between two points using the Pythagorean Theorem.

graphic/10fig19.gif


To calculate the length of c, you just take the square root of a[2] + b[2]. Here's the code to do that:

public final static int distance(int x1, int y1, int x2, int y2)
{
   int dx = (x2 - x1) * (x2 - x1);
   int dy = (y2 - y1) * (y2 - y1);
   if (dx == 0 || dy == 0) return 0;

   try
   {
      return MathFP.toInt(MathFP.sqrt( MathFP.toFP( dx + dy ) ));
   }

   catch(ArithmeticException ae)
   {
      return 0;
   }
}

You should add this method to the Actor class.

Targeting an Angle

Now that you know the distance between your turret and the player, you know when to wake up and take some notice. The next problem is getting your turret to turn and aim at the ship, so you need to figure out the direction of the location of the ship relative to the turret. Take a look at this problem in Figure 10.20.

Figure 10.20. An enemy turret currently facing 60 degrees needs to turn towards the player's ship by changing direction to face at 300 degrees.

graphic/10fig20.gif


Notice that your turret is currently facing about 60 degrees, whereas the ship lies to the southeast at around 300 degrees (relative to the turret's position). You need to figure out the direction of the ship relative to the turret based on their positions. You can calculate this angle by taking the tangent of the b side over the a side. This will return the angle. However, the angle will only be within a range of 0 to 90 degrees, so you need to do a further adjustment to figure out the quadrant the other object is in and adjust the angle to match. Here's the code to do this. (Again, this should go in your Actor class.)

First, you need a convenient way to go from degrees to radians.

public static final int FP_PI2 = MathFP.mul(MathFP.PI, MathFP.toFP(2));
public static final int FP_DEGREES_PER_RAD = MathFP.div(MathFP.toFP(360),
                                                             FP_PI2);
public final static int getAngleFromRadians(int radiansFP)
{
   return MathFP.toInt(MathFP.mul(radiansFP, FP_DEGREES_PER_RAD)); 
}

public final static int getRadiansFromAngle(int angle)
{
   return MathFP.div(MathFP.toFP(angle), FP_DEGREES_PER_RAD);
}

Next we calculate the facing angle.

public final static int getFacingAngle(int x1, int y1, int x2, int y2)
{
   // figure the two sides of our right angle triangle
   int a = MathFP.toFP(Math.abs(x2 - x1));
   int b = MathFP.toFP(Math.abs(y2 - y1));

   if (a == 0) a = FP_ONE;
   if (b == 0) b = FP_ONE;

   int bovera = MathFP.div(b, a);

   int angleInRadians = MathFP.atan(bovera); 
   int angle = getAngleFromRadians(angleInRadians);

   // now adjust for which quadrant we're really in
   if (x2 < x1)
   {
      // left side
      if (y2 < y1)
         return angle + 180;
      return angle + 90;
   }
   else
   {
      // right side
      if (y2 < y1)
         return angle + 180;
      return angle;
   }
}

Using this code, you can calculate which way to face, but there's still something else you need to know about this angle. Which way should the turret turn in order to face your ship as quickly as possible? It's going to look a bit silly if the turret is facing five degrees away from the ship and it decides to turn completely around the other way. You need to know whether turning clockwise or counterclockwise is the fastest route to a particular direction. Here's a static method for the Actor class that figures this out for you:

public final static boolean isClockwise(int angleA, int angleB)
{
   if (angleA > angleB)
      return (Math.abs(angleA - angleB)) < (angleB + (360 - angleA));
   else
      return (angleA + (360 - angleB)) < (Math.abs(angleB - angleA));
}

Turning through Spin

Now that you know the direction your turret needs to face and which way to turn, you need to actually make the turret turn that way and then stop when it reaches the correct direction. To do this, you need to add controlled spin to the Actor class.

Spinning is simply turning at a constant rate expressed in degrees per tick (hundredths of a second). This rate represents how fast any actor can turn, and it can vary from actor to actor. For example, the player can turn his ship at a rate of 22.5 degrees per tick, thus completing a full 360-degree turn in approximately 1.6 seconds, whereas the turret turns at a slower rate of only 10 degrees per tick. This makes it easier for the player to outmaneuver the turret because the turret will have to slowly track the player. Higher-level turrets can then increase this rate to make them more accurate and thus more dangerous.

Spin is also used when the player holds down the left or right arrow key. All you do is set the player's ship to have a spin rate of either 22.5 or 22.5, depending on which direction the player hits. When the player releases the key, you just set the spin rate back to 0. For example you could just add the following code to the GameScreen class (we'll get to the setSpin method in just a second):

protected void keyPressed(int keyCode)
{
   int action = getGameAction(keyCode);
   if (action == RIGHT)
      playerShip.setSpin(MathFP.toFP("22.5"));
   if (action == LEFT)
      playerShip.setSpin(MathFP.toFP("-22.5"));
}

protected void keyReleased(int keyCode)
{
   int action = getGameAction(keyCode);

Notice below I don't use the MathFP.toFP. This is because zero is always zero, so there's no need to convert it.

   if (action == RIGHT)
      playerShip.setSpin(0);
   if (action == LEFT)
      playerShip.setSpin(0);
}

The code to actually spin the object is even easier. You need to add a member to track both the rate at which the object can spin (you'll use this for the enemy) and the current spin rate.

private int currentSpinFP;             // current spin rate (can be negative)
private int spinRateFP;                // spin ship capability (can be negative)

To support these you need to add some get and set methods for the spin value. I've also added a way to set the direction (safely).

   /**
    * Gets the spin rate for this Actor in degrees per second.
    * @return The current spin (turning).
    */ 
   public int getSpin()
   {
      return MathFP.toInt(spinFP);
   }

   /**
    * Set the spin rate for this Actor in degrees per second.
    * @param newSpin The spin rate (as a MathFP value).
    */
   public void setSpin(int newSpinFP)
   {
      spinFP = newSpinFP;
   }
   /**
    * Change the Actor's current direction.
    * @param newDirection The new direction to face (in degrees).
    */
   public void setDirection(int newDirection)
   {
      direction = newDirection;
      if (direction < 0) direction = (359 + (direction));   // add the neg value
      if (direction > 359) direction = (newDirection - 360);
   }

To spin the ship, you simply modify the cycle method of the Actor to change the direction based on the current spin rate. As you can see, all you're doing is adding (or taking away, in the case of a negative value) the spin from the current direction.

// spin based on degrees per tick
if (currentSpinFP != 0)
   setDirection(direction + MathFP.toInt(MathFP.mul(ticksFP, currentSpinFP)));

All right, you can now turn the player's Ship based on a key hit by setting the spin value. But you need to do a little more to have your turrets turn toward the player automatically. Next you can add a target direction which the Actor cycle code will automatically turn towards (using the shortest route).

To support this we first add a targetDirection to the Actor class as well as a boolean to indicate whether this is active or not. The targetDirection is initialized with the value of negative one so that we know a target direction has not been set. You don't want to use zero for this because it's a valid angle.

private boolean autoSpinning;
private int targetAngle=-1; 
public final void setTargetDirection(int angle)
{
   // set an angle this actor wants to face; the actor will start spinning
   // at its default spin rate towards the target angle - see cycle for
   // the actual spin code
   targetAngle = angle;
   autoSpinning = false;
}

Next you need to modify the cycle code to turn towards the target angle if it's enabled. This code checks whether the Actor has a value in the targetAngle field not equal to a negative one. If it does, the Actor automatically starts spinning in the correct direction. Once it's at the right angle, the Actor then stops spinning.

// move towards our target direction, if we have one
if (targetAngle != -1)
{
   if (!autoSpinning)
   {
      // start spin in the dir of the target angle
      setSpin(isClockwise(getDirection(), targetAngle) ? -maxSpinRate : maxSpinRate);
   }

   // and check if we've made it to the target direction
   if (targetAngle == direction)
   {
      // cancel turning
      setSpin(0);
      setTargetDirection(-1);
      autoSpinning = false;
   }
}

Aligning Directions

Introducing precise spin to the Actors shows you another problem very quicklysprite alignment. In Figure 10.21, you can see all the frames for your enemy turret. Since we're no longer moving in nice even increments of 22.5 degrees the turret can face an angle that cannot be represented by these frames.

Figure 10.21. The 16 frames of the turret graphic

graphic/10fig21.gif


The problem is that you're now turning the ship to directions you can't render properly on the screen. This will produce weird results, such as the Ship flying in a slightly different direction than the way it's facing on the screen and the turret bullets coming out in slightly wrong directions.

One way to fix this is to only allow an actor to change to a direction it can accurately represent. But this is messy, and you lose the accuracy of actual direction changes. What you really need is two directionsone that represents the direction in which the actor is really facing (the real direction) and another that represents a direction aligned to a particular degree increment (the aligned direction). All your code to manipulate your direction will then continue to work perfectly based on the real direction member, and you can use the aligned direction for drawing, moving, and firing.

To do this, you need to make a few adjustments to your Actor class. First, add the different direction members (real and aligned) as well as the degrees to which you want to align.

private int realDir;               // actual direction in degrees
private int alignedDir;            // aligned direction
private int alignedDivDegreesFP;   // degrees per facing division

You need to initialize the member to an appropriate value in the Actor constructor. For example, in the following code, the alignedDivArg is what's passed into the constructor:

wantAlignment = false;
if (alignedDivArg > 0)

{
   alignedDivDegreesFP = MathFP.div(360, alignedDivArg);
   wantAlignment = true;
}

You then need to change the direction methods in the Actor class. The major change is to the setDirection method. This now does all the work of updating the aligned direction whenever the real direction changes.

   /**
    * Change the Actor's current direction.
    * @param newDirection The new direction to face (in degrees).
    */
   public void setDirection(int newDirection)
   {
      realDir = newDirection;
      if (realDir < 0) realDir = (359 + (realDir));   // add the neg value
      if (realDir > 359) realDir = (newDirection - 360);

      // set the facing direction to be the closest alignment 
      if (wantAlignment)
         alignedDir = getAlignedDirection(realDir);
      else
         alignedDir = realDir;
   }
/**
 * Gets the closest aligned direction to the passed in direction (in
 * degrees).
 * @param dir The direction to align (in degrees).
 * @return A direction which aligns to the number of divisions the Actor
 * supports.
 */
public final int getAlignedDirection(int dir)
{
   int divisions = MathFP.toInt(MathFP.div(MathFP.toFP(dir),
                                               alignedDivDegreesFP));
   int roundedDivisions = MathFP.toInt(MathFP.mul(MathFP.toFP(divisions),
                                                      alignedDivDegreesFP));
   if (roundedDivisions < 0) roundedDivisions = 0;
   if (roundedDivisions > 359) roundedDivisions = 0;
   return roundedDivisions;
}

/**
 * @return The current aligned direction.
 */
public final int getDirection()
{
   return alignedDir;
}

/**
 * @return The current real direction (not aligned).
 */
public final int getRealDirection()
{
   return realDir;
}

You'll notice I've updated the getDirection method to now return the aligned direction, not the real one. Anything using getDirection will now use a value properly aligned to the capabilities of the actor.

Creating an Enemy

In the next example you'll see how to add some control logic to enemy ships so that they'll chase after you. Before you can do that though you need to add support in the Ship class for the enemy graphics and properties.

Firstly, you need a boolean to indicate whether this is an enemy or player Ship as well as support for the two distinct sets of images used to draw them.

public class Ship extends Actor
{
   private boolean isEnemy;
   private static ImageSet playerShipImageSet;
   private static ImageSet enemyShipImageSet;

In the Ship constructor you'll need to load up two sets of ship images: the yellow ones for the player and the red ones for the enemy. All these images are within the one image file.

   public Ship(boolean isEnemyArg, int startX, int startY)
   {
      super(startX, startY, 0, 16, MathFP.toFP("0.2"), MathFP.toFP("0.0"),
            MathFP.toFP("2.0"), MathFP.toFP("-1.0"), 23);

      if (playerShipImageSet == null)
      {
         try
         {
            Image shipGraphic = Image.createImage("/ship.png");

Here we extract two sets of images now instead of just one. The red fighter frames (the enemy) start four frames across in the file (4 * 16).

         // Extract out the image frames for the player ship.
         Image[] playerShipFrames = ImageSet.extractFrames(shipGraphic,
             0, 0, 4, 4, SHIP_FRAME_WIDTH, SHIP_FRAME_HEIGHT);
         playerShipImageSet = new ImageSet(1);
         playerShipImageSet.addState(playerShipFrames, 0);

         // Extract out the image frames for the enemy ship.
         Image[] enemyShipFrames = ImageSet.extractFrames(shipGraphic,
             4*16, 0, 4, 4, SHIP_FRAME_WIDTH, SHIP_FRAME_HEIGHT);
         enemyShipImageSet = new ImageSet(1);
         enemyShipImageSet.addState(enemyShipFrames, 0);
      }
      catch (IOException ioe)
      { 
         System.out.println("unable to load image");
      }
   }

Based on the isEnemyArg value we set the isEnemy flag and then initialize the ship sprite with the appropriate images.

      // Set the ship sprite based on the type. If it's an enemy we use the
      // red ship frames.
      isEnemy = isEnemyArg;

      if (isEnemy)
         shipSprite = new Sprite(enemyShipImageSet, 0, 0);
      else
         shipSprite = new Sprite(playerShipImageSet, 0, 0);
   }

That's it for creating an enemy type; next I'll show you how to give it a brain.

Creating a Brain

Finally, our AI is ready to come together. You have all the tools you need to have your enemies act and react within the game. To make all this come alive, you need to give your actors a brain. Don't worryit's nothing like the HAL9000. All you need is control logic to check for conditions and then react appropriately. For your enemy ships and turrets, you check whether the player is within a sensible range and then turn toward him. For example here's a revised Ship cycle method that adds basic logic to the enemy:

   /**
    * Cycling for the Ship calls Actor.cycle to handle movement. It then checks
    * to see if this is an enemy type ship and updates the direction based on
    * the relative angle of the player's ship from the enemy one.
    * @param deltaMS The amount of time that has passed since the last cycle
    * (in milliseconds).
    */
   public void cycle(long deltaMS)
   {
      super.cycle(deltaMS);

      if (isEnemy)
      {
         // If insufficient time has passed to do an AI update we just add the
         // deltaMS time to the counter. If enough time has passed it executes
         // the Enemy AI code (this is only done periodically since it's
         // typically expensive stuff you don't want to do every frame). 
         if (msSinceLastAIUpdate < msPerAIUpdate)
            msSinceLastAIUpdate += deltaMS;
         else
         {
            msSinceLastAIUpdate -= msPerAIUpdate; // take off one update's worth

            // Calculate the distance to the player so we ignore cases where
            // the player is too far away to bother with.
            Ship playerShip = GameScreen.getGameScreen().getPlayerShip();
            int distanceToPlayer = distanceTo(playerShip);
            if (distanceToPlayer < ENEMY_IGNORE_DISTANCE)
            {
               // Figure out the angle we need to face to fly directly towards
               // the player's ship.
               int facingAngle = getFacingAngle(getX(), getY(), playerShip.getX(),
                                                   playerShip.getY());
               // Set this to be our target direction. The Actor.cycle method
               // will take care of turning this ship until it faces the target
               // angle we set here.
               setTargetDirection(facingAngle);
            }
         }
      }
   }

You'll notice I've wrapped the code to check the distance and then adjust the turn within a time check. There's no need to do an AI update every cycle; in fact, it's a complete waste of time. Having an update rate of about one second is usually more than enough to make your enemies seem responsive. This also gives you more freedom to make this code more complex if you need to.

    Table of ContentsActorsConclusion