Friday, January 23, 2015

Anansi: Heat Seeking Missiles


The heat seeking missiles have the following characteristics:

  • spawn upon user request
  • similar to the bullets we already use
  • find the closest target
  • follow the target once it has locked on to it
  • have a limited amount of fuel

The code is very similar to the one we already used with the player's bullets. The main difference is the algorithm to find a target and to move towards the target using rotation.

Let's get coding.

We already have the secondary weapon control considered in the input class. All that's left is to use it. Then there's the question about where we want the missiles to spawn. In the middle would be boring, that's already occupied by the cannon. Spawning it at the sides would be the solution. But we don't want it to be one-sided, so we have to spawn them left and right. I like the idea of having them spwan alternatively, i. e. left, then right, then left and so on.

We extend the player class with these code fragments:
double missileChargeTime = 10.0;
double missileChargeCounterDelta = 1.0;
double missileChargeCounter = missileChargeTime;
double missileSpeed = 4.0;
int missileSlot = 0; 
...
public void chargeSecondaryWeapon() {
 
   // limit fire
   // ---------------------------
   // charge weapon: increase a counter by some delta. once it reaches a limit, the weapon is considered charged
   missileChargeCounter += missileChargeCounterDelta;
   if( missileChargeCounter > missileChargeTime) {
    missileChargeCounter = missileChargeTime;
   }
   
}

public double getSecondaryWeaponX() {
 
 if( missileSlot == 0) {
  return x + 10; // just a value that looked right in relation to the sprite image
 } else {
  return x + 34;  // just a value that looked right in relation to the sprite image
 }
}
public double getSecondaryWeaponY() {
 return y + 10; // just a value that looked right in relation to the sprite image
}


public double getSecondaryWeaponMissileSpeed() {
 return missileSpeed;
}

public void unchargeSecondaryWeapon() {
 
 // player bullet uncharged
 missileChargeCounter = 0;
 
 // next slot
 missileSlot++;
 missileSlot = missileSlot % 2;

}


As I said: Very similar to the cannon code and as easy to use. Now we create the Missile class itself:
public class Missile extends SpriteBase {


SpriteBase target; 

double turnRate = 0.6;
double missileSpeed = 4.0;

double missileHealth = 100; 

public Missile(Pane layer, Image image, double x, double y) {
 super(layer, image, x, y, 0, 0, 0, 0, 1, 1);
 init();
}

public Missile(Pane layer, Image image, double x, double y, double r, double dx, double dy, double dr, double health, double damage) {
 super(layer, image, x, y, r, dx, dy, dr, health, damage);
 init();
}

private void init() {

 // initially move upwards => dy = -speed
 setDy( -missileSpeed);
 
 // limit missile alive time (consider it as fuel)
 setHealth( missileHealth);
 
}

public SpriteBase getTarget() {
 return target;
}

public void setTarget(SpriteBase target) {
 this.target = target;
}

/**
 * Find closest target
 * @param targetList
 */
public void findTarget( List targetList) {


 // we already have a target
 if( getTarget() != null) {
  return;
 }

 SpriteBase closestTarget = null;
 double closestDistance = 0.0;

 for (SpriteBase target: targetList) {

  if (!target.isAlive())
   continue;

  //get distance between follower and target
  double distanceX = target.getCenterX() - getCenterX();
  double distanceY = target.getCenterY() - getCenterY();

  //get total distance as one number
  double distanceTotal = Math.sqrt(distanceX * distanceX + distanceY * distanceY);   


  if (closestTarget == null) {
   
   closestTarget = target;
   closestDistance = distanceTotal;
   
  } else if (Double.compare(distanceTotal, closestDistance) < 0) {
   
   closestTarget = target;
   closestDistance = distanceTotal;
   
  }
 }

 setTarget(closestTarget);

}

@Override
public void move() {
 
 SpriteBase follower = this;
 
 if( target != null)
 {
  //get distance between follower and target
  double distanceX = target.getCenterX() - follower.getCenterX();
  double distanceY = target.getCenterY() - follower.getCenterY();

  //get total distance as one number
  double distanceTotal = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
  
  //calculate how much to move
  double moveDistanceX = this.turnRate * distanceX / distanceTotal;
  double moveDistanceY = this.turnRate * distanceY / distanceTotal;

  //increase current speed
  follower.dx += moveDistanceX;
  follower.dy += moveDistanceY;

  //get total move distance
  double totalmove = Math.sqrt(follower.dx * follower.dx + follower.dy * follower.dy);
  
  //apply easing
  follower.dx = missileSpeed * follower.dx/totalmove;
  follower.dy = missileSpeed * follower.dy/totalmove;
  
 } 
 
 //move follower
 follower.x += follower.dx;
 follower.y += follower.dy;

 //rotate follower toward target
 double angle = Math.atan2(follower.dy, follower.dx);
 double degrees = Math.toDegrees(angle) + 90;
 
 follower.r = degrees;

}

@Override
public void checkRemovability() {

 health--; // TODO: let it explode on health 0
 
 if( Double.compare( health, 0) < 0) {
  setTarget(null);
  setRemovable(true);
 }
 
}
   
}


The parts you'll be most interested are the findTarget() method and the move() method. It's just basic math to calculate the distance and the angle.

Now let's integrate the missiles into our game, similar to the bullets.
Image playerMissileImage;
List playerMissileList = new ArrayList<>();

private void loadResources() {

... 
 // missiles
 playerMissileImage = new Image( getClass().getResource( "assets/bullets/missile.png").toExternalForm());
... 
 
}

private void createGameLoop() {
 
    gameLoop = new AnimationTimer() {
     
        @Override
        public void handle(long l) {

          ...         

          // non-player AI
         // --------------------------
        // for every missile find an enemy target
        for (Missile missile : playerMissileList) {
         missile.findTarget( enemyList);
        }
         
         // spawn bullets of players
         // ---------------------------
         for( Player player: playerList) {
          spawnSecondaryWeaponObjects( player);
         }
         
         // move sprites internally
         // ---------------------------
         ...
         moveSprites( playerMissileList);
         
         // move sprites on screen
         // ---------------------------
         ...
         updateSpritesUI(playerMissileList);

         // check if sprite can be removed
         // ------------------------------
         ...
         checkRemovability( playerMissileList);
         
         // remove removables from list, layer, etc
         // ------------------------------
         ...
         removeSprites( playerMissileList);
         
        }

    };
      
}

private void spawnSecondaryWeaponObjects( Player player) {
 
 player.chargeSecondaryWeapon();

   if( player.isFireSecondaryWeapon()) {
    
    Image image = playerMissileImage;

    double x = player.getSecondaryWeaponX() - image.getWidth() / 2.0; 
    double y = player.getSecondaryWeaponY();
    
    Missile missile = new Missile( bulletLayer, image, x, y);
    
    playerMissileList.add( missile);
    
    player.unchargeSecondaryWeapon();
   }
 
} 

Now we have nice heat seeking missiles which turn as they follow the enemy. Awesome! The game looks like this:

No comments:

Post a Comment