Sunday, January 25, 2015

Anansi: Upgrades


Thinking of it, we could pick up any kinds of collectibles. We could create a different class for each bonus, but that would mean we'd have to use the instanceof operator in order to distinguish them. I think it's better to give the collectibles an internal type. This way we compare simply an enum instead of using instanceof.

We introduce the following collectibles:

  • Stroyent: increase score
  • Nuke: all enemies explode
  • Shield: gives the player temporary invicibility
  • Ammo: upgrades the bullet level
  • Health: increases the player's health

So let's change the Stroyent class to a Bonus class which can have various types.

public class Bonus extends SpriteBase {

 public enum Type {
  STROYENT,
  NUKE,
  HEALTH,
  AMMO,
  SHIELD
 }
 
 private Type type;
 
 public Bonus(Type type, 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);
  this.type = type;
 }
 
 public Type getType() {
  return type;
 }

 @Override
 public void checkRemovability() {

  if( Double.compare( getY(), Settings.SCENE_HEIGHT) > 0) {
   setRemovable(true);
  }

  
 }
}

We load the images for the various bonuses.
Image bonusStroyentImage;
Image bonusNukeImage;
Image bonusHealthImage;
Image bonusShieldImage;
Image bonusAmmoImage;

// bonuses
bonusStroyentImage= new Image( getClass().getResource( "assets/bonus/stroyent.png").toExternalForm());
bonusNukeImage= new Image( getClass().getResource( "assets/bonus/nuke.png").toExternalForm());
bonusHealthImage= new Image( getClass().getResource( "assets/bonus/health.png").toExternalForm());
bonusShieldImage= new Image( getClass().getResource( "assets/bonus/shield.png").toExternalForm());
bonusAmmoImage= new Image( getClass().getResource( "assets/bonus/ammo.png").toExternalForm());

We spawn the various bonuses depending on a randomness level in our Settings class.

public static int BONUS_RANDOMNESS = 50;

We consider the randomness in the spawnBonus class. We could vary the frequency of the different bonus types by changing the rndBonus check from single integer to an interval. For now the bonus types except stroyent show up equally distributed.
private void spawnBonus( SpriteBase sprite) {

 Bonus.Type type;
 Image image;
 
 // set the bonus type randomly
 int rndBonus = rnd.nextInt( Settings.BONUS_RANDOMNESS);
 
 // nuke
 if( rndBonus == 0) {
  type = Bonus.Type.NUKE;
  image = bonusNukeImage;
 }
 // health
 else if( rndBonus == 1) {
  type = Bonus.Type.HEALTH;
  image = bonusHealthImage;
 }
 // shield
 else if( rndBonus == 2) {
  type = Bonus.Type.SHIELD;
  image = bonusShieldImage;
 }
 // ammo
 else if( rndBonus == 3) {
  type = Bonus.Type.AMMO;
  image = bonusAmmoImage;
 }
 // default: stroyent
 else {
  type = Bonus.Type.STROYENT;
  image = bonusStroyentImage;
 }

 
 // random speed
 double speed = rnd.nextDouble() * 1.0 + 1.0;

 // random rotation delta
 double dr = rnd.nextDouble() * 1.0 + 1.0;
 
 // random left/right rotation
 if( rnd.nextInt(2) == 0)
  dr = -dr;

 double x = sprite.getCenterX();
 double y = sprite.getCenterY();
 
 
 // create a sprite
 Bonus stroyent = new Bonus( type, playfieldLayer, image, x, y, 0, 0, speed, dr, 1, 0);
 
 // becomes visible once the ship (which is on top of the stroyent sprite) is gone
 stroyent.getView().toBack();
 
 // manage sprite
 stroyentList.add( stroyent);
 
}


With this change the collectibles look like this on the screen:



We need to deal with the various bonus items. We modify the collision check and add a method which processes the items.

/**
 * Let player ship pick up bonus items. 
 * @param bonusList
 */
private void checkBonusCollisions( List bonusList) {
 
 for( Player player: playerList) {
  for( Bonus bonus: bonusList) {
   
   // consider only alive sprites
   if( !bonus.isAlive())
    continue;

   if( player.collidesWith( bonus)) {
    
    // stop movement of sprite
    bonus.stopMovement();
    
    // collect bonus
    collectBonus( player, bonus);

    // destroy sprite, set health to 0
    bonus.kill();
    
    // remove the sprite from screen by flagging it as removable
    bonus.remove();
    
   }
  }
 }
}

private void collectBonus( Player player, Bonus bonus) {
 
 switch( bonus.getType()) {
 case STROYENT:
  // show collection score
  spawnScoreTransition( bonus, Settings.SCORE_BONUS_STROYENT);
  break;
 case NUKE:
  System.err.println( "TODO: add nuke code");
  break;
 case SHIELD:
  System.err.println( "TODO: add shield code");
  break;
 case HEALTH:
  System.err.println( "TODO: add health code");
  break;
 case AMMO:
  System.err.println( "TODO: add ammo code");
  break;
 default:
  System.err.println("Unsupported bonus type: " + bonus.getType());
 }
 
}

Now the code is similar to what it was before when we had a dedicated Stroyent bonus class, except that we can add various bonus handlers.

A note about the code which we still need to implement: In this special case I prefer to simply output an error message for the missing code. Throwing an UnsupportedOperationException or IllegalArgumentException here would be overkill. It's not really necessary and we don't want to break the game with unnecessary exceptions.

What I'm curious though is the nuke. We already have the code to let an enemy ship explode when a bullet/missile hit's it. So the nuke is just a loop over all enemies and letting them explode. Let's add this now.

The difficulty we are facing is that we can't simply run through the enemy and bonus loops again while we're already running through them. We'd get ConcurrentModificationExceptions. The solution to this is that we do the same what we already did with the bullets and missiles: We only set a flag that the nuke got fired and we fire it in the game loop. Let's get coding.

We extend the Player class:
boolean nukeCharged = false;

public void fireNuke() {
 this.nukeCharged = true;
}

public boolean isFireNuke() {
 return this.nukeCharged;
}

public void unchargeNuke() {
 this.nukeCharged = false;
}

We modify the bonus collection and explosion mechanism:
/**
 * Check if a projectile (eg player bullet/missile) hits an enemy.
 * If a collision is detected, the enemy is damaged. If enemy is killed, an explosion is spawned.
 * @param projectileList
 */
private void checkProjectileCollisions( List projectileList) {
 
 for( SpriteBase projectile: projectileList) {
  for( Enemy enemy: enemyList) {
   
   // consider only alive sprites
   if( !enemy.isAlive())
    continue;

   if( projectile.collidesWith(enemy)) {
    
    // inflict damage, reduce enemy's health
    enemy.getDamagedBy(projectile);

    // explosion animation
    if( !enemy.isAlive()) {
     
     explode( enemy);
     
    }

    // destroy sprite, set health to 0
    projectile.kill();
    
    // remove the sprite from screen by flagging it as removable
    projectile.remove();
    
   }
  }
 }
}

private void collectBonus( Player player, Bonus bonus) {
 
 switch( bonus.getType()) {
 case STROYENT:
  // show collection score
  spawnScoreTransition( bonus, Settings.SCORE_BONUS_STROYENT);
  break;
 case NUKE:
  // kill all enemies
  player.fireNuke();
  break;
 case SHIELD:
  System.err.println( "TODO: add shield code");
  break;
 case HEALTH:
  System.err.println( "TODO: add health code");
  break;
 case AMMO:
  System.err.println( "TODO: add ammo code");
  break;
 default:
  System.err.println("Unsupported bonus type: " + bonus.getType());
 }
 
}

private void nuke() {

 for (Enemy enemy : enemyList) {

  // consider only alive sprites
  if (!enemy.isAlive())
   continue;

  // set health to 0
  enemy.kill();

  // let enemy explode, spawn bonus
  explode( enemy);

 }
}

private void explode( Enemy enemy) {
 
 // stop movement of sprite
 enemy.stopMovement();
 
 // let enemy explode
 spawnExplosion( enemy);
 
 // show score
 spawnScoreTransition( enemy, Settings.SCORE_ENEMY);

 // show bonus
 spawnBonus( enemy);

}

private void checkNukeFiring() {
 for( Player player: playerList) {
  if( player.isFireNuke()) {
   
   // nuke can only be fired once
   player.unchargeNuke();
   
   // nuke all enemies
   nuke();
  }
 }
}


And we add the nuke mechanism to the game loop:
// check if player fires nuke and if so, kill all enemies
checkNukeFiring();


Looks great, works perfectly without problems when we pick up a nuke:


We'll consider the other upgrades in another blog entry.

No comments:

Post a Comment