Sunday, February 1, 2015

Anansi: Statistics Overlay


Now that we can hit enemies and collect items, we very much would like to see these data on the screen.

Our statistics overlay could include several items per player:

  • Score
  • Health bar
  • Number of collected items
  • Number of ships left
  • Number of retries (usually coins) left after a game over
  • Number of special bombs left
  • Rank
  • ...

For now we settle with a score, a health bar and the number of collected items. Adding the other data is easy once the framework is set up.

We create a dedicated statistics overlay component. For now we have a single player game, but we should think ahead and plan on having a multiplayer game. So we create statistics for every player. The main question is: Who keeps track of the statistics and the update? The player or a dedicated Statistics component? We'll settle for a dedicated component.

What we need to do is to add another layer on top of all the other layers.
Pane statisticsLayer;
...
root.getChildren().add( backgroundLayer);
root.getChildren().add( lowerCloudLayer);
root.getChildren().add( bulletLayer);
root.getChildren().add( playfieldLayer);
root.getChildren().add( upperCloudLayer);
root.getChildren().add( statisticsLayer);
root.getChildren().add( debugLayer);
Then we add various statistics attributes to the player class. We add a score holder, a bonus item holder and a health calculation mechanism. We'll use a health bar for the health display, so the fill state of it will be between 0 and 100% or more exactly it'll range from 0.0 to 1.0.
private double healthMax = Settings.PLAYER_SHIP_HEALTH;
private long score = 0;
private int bonusCollected = 0;

/**
 * Health as a value from 0 to 1.
 * @return
 */
public double getRelativeHealth() {
 return getHealth() / healthMax;
}

public void resetScore() {
 this.score = 0;
}

public void addScore( long points) {
 this.score += points;
}

public long getScore() {
 return this.score;
}

public void incBonusCollected() {
 addBonusCollected(1);
}

public void addBonusCollected( int count) {
 this.bonusCollected += count;
}

public int getBonusCollected() {
 return this.bonusCollected;
}

We create a health bar that consists of 2 rectangles: An outer and an inner rectangle. The outer shows the total health, the inner the current health.
public class HealthBar extends Pane {
 
 Rectangle outerHealthRect;
 Rectangle innerHealthRect;
 
 public HealthBar() {
  
  double height = 10;
  
  double outerWidth = 60;
  double innerWidth = 40;
  
  double x=0.0;
  double y=0.0;
  
  outerHealthRect = new Rectangle( x, y, outerWidth, height);
  outerHealthRect.setStroke(Color.BLACK);
  outerHealthRect.setStrokeWidth(2);
  outerHealthRect.setStrokeType( StrokeType.OUTSIDE);
  outerHealthRect.setFill(Color.WHITE);

  innerHealthRect = new Rectangle( x, y, innerWidth, height);
  innerHealthRect.setStrokeType( StrokeType.OUTSIDE);
  innerHealthRect.setFill(Color.LIMEGREEN);

  getChildren().addAll( outerHealthRect, innerHealthRect);

 }
 
 public void setValue( double value) {
  innerHealthRect.setWidth( outerHealthRect.getWidth() * value);
 }
 
}

The tricky part in this is to keep the health bar smooth. JavaFX calculates in doubles and if we'd simply create 2 rectangles, then some antialiasing will be applied and the bar wouldn't be crisp. You'd get some shades of colors, it would look blurry. To overcome this problem there are several solutions. One would be to snap to grid by casting the location value to int and then applying 0.5 to it. Another would be to create an entire pane that does these adjustments for us. And then there's simply to not use strokes of types CENTERED. I really would like to avoid calculations with 0.5 because where do we start with messing around and where does it end? So the solution we use is a different stroke type.

The Statistics component is a simple StackPane in which we layout our score, health bar, an icon with the bonus items and a number next to it showing the number of collected bonus items.
public class Statistics extends StackPane {

 Player player;
 
 HealthBar healthBar;
 Text scoreText;
 Text bonusText;
 
 DecimalFormat bonusFormatter = new DecimalFormat( "x0000");
 
 // TODO: use image manager for bonus image
 public Statistics( Player player, Image bonusImage) { 
  
  this.player = player;
  
  setPadding( new Insets(2));
  
  // health bar
  healthBar = new HealthBar();
  StackPane.setAlignment( healthBar, Pos.TOP_LEFT);
  StackPane.setMargin( healthBar, new Insets( 6,0,0,6));
  
  getChildren().add( healthBar);

  
  // score
  scoreText = new Text();
  scoreText.setFont(Font.font("Arial", FontWeight.BOLD, 26)); // TODO: css
  scoreText.setStroke(Color.BLACK);
  scoreText.setFill(Color.WHITE);
  
  StackPane.setAlignment( scoreText, Pos.TOP_CENTER);
  
  getChildren().add( scoreText);

  // bonus collection icon
  ImageView bonusImageView = new ImageView( bonusImage);
  
  StackPane.setAlignment( bonusImageView, Pos.TOP_LEFT);
  StackPane.setMargin( bonusImageView, new Insets( 20,0,0,6));
  
  getChildren().add( bonusImageView);

  // bonus collection score
  bonusText = new Text("x0000");
  bonusText.setFont(Font.font("Arial", FontWeight.BOLD, 18)); // TODO: css
  bonusText.setStroke(Color.BLACK);
  bonusText.setFill(Color.RED);
  
  StackPane.setAlignment( bonusText, Pos.TOP_LEFT);
  StackPane.setMargin( bonusText, new Insets( 20,0,00,18));
  
  getChildren().add( bonusText);

  
 }
 
 public void updateUI() {
  
  // update health bar
  healthBar.setValue( player.getRelativeHealth());
  
  // update score
  scoreText.setText(NumberFormat.getInstance().format( player.getScore()));
  
  // number of collected bonus items
  bonusText.setText( bonusFormatter.format( player.getBonusCollected()));
  
 }
 
}

Of course we'd use CSS for formatting the text, we'll schedule that for later. Another tricky thing is to get the bonus item image from the main class to this component, since it should be loaded in the general game loading mechanism. For a quick solution we hand the image over from the main class, but in the end we'll use an image manager for it.

What's left is to use these code parts in our main game code.
Image statisticsBonusImage;

private void loadResources() {
  ...
 // statistics icons
 statisticsBonusImage = new Image( getClass().getResource( "assets/statistics/stroyent.png").toExternalForm());
 ...
}
 
    
private void createLevel( Scene scene) {
 
 // load game assets
 loadResources();
 
 // create level structure
 createBackground();
 
 // create player, including input controls
 Player player = createPlayer( scene);

 // create player statistics overlay
 createStatistics( player);
}

private void createStatistics( Player player) {

 // create player statistics overlay
 Statistics statistics = new Statistics(player, statisticsBonusImage);
 
 // stretch statistics overlay width to full scene width
 statistics.prefWidthProperty().bind( primaryStage.getScene().widthProperty());
 
 // add to UI
 statisticsLayer.getChildren().add( statistics);

 // add to managed list
 statisticsList.add( statistics);
}    


private void createGameLoop() {
 
      gameLoop = new AnimationTimer() {
       
          @Override
          public void handle(long l) {
            ...
           // update score, bonus, etc
           // ---------------------------
           updateStatisticsOverlay();
           ...
          }

      };
      
}

That was rather easy and quickly done. Here's a screenshot.



With this mechanism we can always exchange the statistics component against another component which shows the statistics of multiple players. Or we simply relocate the display elements of the Statistics component depending on some layout parameter, e. g. player 1 statistics are on top left, player 2 statistics on top right.


Anansi: Background Scrolling - Revisited


Our background scrolling consists currently of an ImageView that's being relocated in the game loop. The whole ImageView. The relevant code for this:

public class Background extends SpriteBase {

 public Background(Pane layer, Image image, double x, double y, double speed) {
  super(layer, image, x, y, 0, 0, speed, 0, Double.MAX_VALUE, 0);
 }

 public void move() {
  
  super.move();
  
  checkBounds();
  
 }

 private void checkBounds() {
  
     // check bounds. we scroll upwards, so the y position is negative. once it's > 0 we have reached the end of the map and stop scrolling
     if( Double.compare( y, 0) >= 0) {
      y = 0;
     }
     
 }
 
 @Override
 public void checkRemovability() {
  // nothing to do
 }


}

When we take a look at the ImageView class, there's a method setViewPort. We could as well use that one. We only have to modify a few lines of code:

public class Background extends SpriteBase {

 public Background(Pane layer, Image image, double x, double y, double speed) {
  super(layer, image, x, y, 0, 0, speed, 0, Double.MAX_VALUE, 0);
  
  // we relocate to origin, the scrolling is done via setting the viewport
  getView().relocate(0, 0);
 }

 public void move() {
  
  super.move();
  
  checkBounds();
  
 }

 private void checkBounds() {
  
     // check bounds. we scroll upwards, so the y position is negative. once it's > 0 we have reached the end of the map and stop scrolling
     if( Double.compare( y, 0) >= 0) {
      y = 0;
     }
     
 }
 
 @Override
 public void checkRemovability() {
  // nothing to do
 }


 public void updateUI() {
  
  imageView.setViewport( new Rectangle2D( 0, -y, Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT));

 }
}


In other words: Instead of relocating the ImageView in the updateUI method of the super class, we simply change the view port.

This gives us exactly the same visual result. It'll be interesting though how the internals of JavaFX work and what differences there are regarding performance in numbers. I couldn't see any performance gain or drop by using either of the solutions.

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.

Anansi: Collecting Items


We collect the bonus items with the player ship. Everyone likes collectibles and scoring. The more the better. We can easily add let's say another 5 score points to the player's score when the ship picks up an item.

What do we have to do with our engine? We have to

  • check if the player ship collides with a bonus item
  • add points to the player's score
  • remove the sprite
  • add a ScoreTransition to the screen

Should be easy to do and done quickly, since it's similar to code we already have. Let's get coding.

We define the score value for the picking up of bonus items in the Settings class.
public static int SCORE_BONUS_STROYENT = 5;

We check if the player sprite collides with the bonus sprites
/**
 * Let player ship pick up bonus items. 
 * @param stroyentList
 */
private void checkBonusCollisions( List stroyentList) {
 
 for( Player player: playerList) {
  for( SpriteBase target: stroyentList) {
   
   // consider only alive sprites
   if( !target.isAlive())
    continue;

   if( player.collidesWith( target)) {
    
    // stop movement of sprite
    target.stopMovement();
    
    // show score
    spawnScoreTransition( target, Settings.SCORE_BONUS_STROYENT);

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


And we invoke the check method in the game loop
// bonus items
checkBonusCollisions( stroyentList);

That's it. You can see how it looks like in this screenshot, check out the fading text objects with "5" around the player ship:

Anansi: Bonus


Everyone likes bonuses. In games you often get to collect bonuses (coins, etc) after you achieved something. Let's add that when we shoot down an enemy.

The game Enemy Territory: Quake Wars used something called Stroyent as collectible. So we could take a screenshot of that, create a sprite of it and let the enemy ship drop it when it gets shot down.

We have the engine to do that, so let's get coding. The code is similar to the code which we use for the enemy: Create a sprite and let it move down the screen.

The stroyent sprite floats from top to bottom:
public class Stroyent extends SpriteBase {

 public Stroyent(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);
 }

 @Override
 public void checkRemovability() {

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

  
 }
}

We create a reference to the image and the sprite and we load the image as usual.
Image stroyentImage;
...
List stroyentList = new ArrayList<>();
...
// bonus: stroyent
stroyentImage= new Image( getClass().getResource( "assets/collectibles/bonus/stroyent.png").toExternalForm());

We extend the game loop as usual.
private void createGameLoop() {
 
 // game loop
  gameLoop = new AnimationTimer() {
   
      @Override
      public void handle(long l) {
        ...
       moveSprites( stroyentList);
        ...
       updateSpritesUI(stroyentList);
        ...
       checkRemovability( stroyentList);
        ...
       removeSprites( stroyentList);
        ...
      }
  };
}

And we create a method to spawn the bonus from the exploding enemy's position.

Let's add something new: We already have a rotation mechanism, so in order to not make the floating stroyent look static we simply add a little rotation to the sprite.

We spawn the bonus item when the ship gets hit. Of course it would overlap the ship. We need to position the sprite behind the ship. We can do that with the toBack() method. This way the stroyent sprite becomes automatically visible when both the enemy ship and the explosion disappear.
private void spawnStroyent( SpriteBase sprite) {
 
 // image
 Image image = stroyentImage;
 
 // 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
 Stroyent stroyent = new Stroyent( 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);
 
}

All that's left is to spawn the sprite. Here's the collision check method as a whole, including the new line for the stroyent spawning:
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()) {
     
     // stop movement of sprite
     enemy.stopMovement();
     
     // let enemy explode
     spawnExplosion( enemy);
     
     // show score
     spawnScoreTransition( enemy, Settings.SCORE_ENEMY);

     // show bonus
     spawnStroyent( enemy);
     
    }

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

Here's how it looks like with the bonus items.



What's left is to pick up the items.

Anansi: Scoring


I guess the FX in JavaFX stands for Effects. So let's see which ones it provides and use them.

When we hit an enemy ship, it's an achievement. We score some points. We could simply increment the high score. That would usually do it, but we'd like to pimp up our game.

JavaFX has a FadeTransition class, we should use it. Let's show the score of every enemy we shoot down on screen. We overlay the explosion with the points we score and let that text fade out slowly.

The Oracle website has a nice article about Using Text and Text Effects in JavaFX.

Looks like it can be done very easy. Let's get coding.

public class ScoreTransition {
 
 Text text;

 FadeTransition transition;
 
 public ScoreTransition( Pane layer, SpriteBase sprite, double durationMs, int score) {
  
  // create view
  text = new Text( NumberFormat.getInstance().format( score));
  
  // set visual properties
  // TODO: uses css, eg text.setStyle("-fx-font: 120px Tahoma; -fx-text-fill: white; -fx-stroke: black; -fx-stroke-width: 2;");
        text.setStroke(Color.BLACK);
        text.setStrokeWidth(0.2);
        text.setFill(Color.WHITE);
        
        // item gets visible at the center of the given sprite
  relocateToCenterOf( sprite);
  
        // make item visible on screen
  layer.getChildren().add( getView());
  
  // create fade transition
        transition = new FadeTransition( Duration.millis( durationMs));
        
        transition.setNode( getView());
        transition.setFromValue(1.0);
        transition.setToValue(0.0);
        transition.setCycleCount(1);
        transition.setAutoReverse(false);
        
        // remove object from screen
        transition.setOnFinished(new EventHandler() {

   @Override
   public void handle(ActionEvent event) {
    
    // remove from screen
    layer.getChildren().remove( getView());
    
   }
         
        });

        
 }
 
 public void play() {
  transition.play();
 }
 
 private Text getView() {
  return text;
 }
 
 /**
  * Move the view to the center of the given sprite.
  * @param sprite
  */
 private void relocateToCenterOf( SpriteBase sprite) {
  getView().relocate( sprite.getCenterX() - getView().getBoundsInParent().getWidth() / 2.0, sprite.getCenterY() - getView().getBoundsInParent().getHeight() / 2.0);
 }

}

It turned out that we can't subclass from FadeTransition. So we implement a play() method in analogy to the sprite animation class.

Each sprite we hit will give us a different score, so let's define them in the Settings class.
public static int SCORE_ENEMY = 100;

We show the score in the game with these few lines of code.
private void spawnScoreTransition( SpriteBase sprite, int score) {
 
 ScoreTransition transition = new ScoreTransition(playfieldLayer, sprite, 1000.0, score);
 transition.play();
 
}

private void checkProjectileCollisions( List projectileList) {
  ...
  if( projectile.collidesWith(enemy)) {
   ...
   // show score
   spawnScoreTransition( enemy, Settings.SCORE_ENEMY);
   ...
  }
  ...
}   
Nice and easy. You can see how it looks like in the following screenshot:


Now we have all the information we need to create a high score for the game. But let's first toy around a bit more with sprites.

Anansi: Collisions - Revisited


We already have the option to detect collsions. But there are a few things we have to consider in addition to the existing mechanism:

  • a sprite should only explode when its health is depleted
  • we only want 1 explosion per sprite
  • the exploding sprite should stop its movement

So we have to extend our checkBulletCollisions and checkMissileCollisions classes. Thinking of it, the classes are very similar - for now. I split them because maybe we'll add a mini-explosion to a detonating missile, but let's combine them until we actually add the mini-explosion.

/**
 * 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()) {
     
     // stop movement of sprite
     enemy.stopMovement();
     
     // let enemy explode
     spawnExplosion( enemy);
    }

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

Blogspot again modifies the code. This would be the correct method signature: checkProjectileCollisions( List<? extends SpriteBase> projectileList)

The modified invocation method.
// check collisions
// ---------------------------
checkProjectileCollisions( playerBulletList);
checkProjectileCollisions( playerMissileList);

We modify the SpriteBase class with the stopMovement code
boolean canMove = true;

...

/**
 * Set flag that the sprite can't move anymore.
 */
public void stopMovement() {
 this.canMove = false;
}

...

public void move() {

 if( !canMove)
  return;
 
 x += dx;
 y += dy;
 r += dr;

} 

Now we can hit something and get animated explosions:



That looks very nice and smooth. Okay, now we're curious about the performance. Let's change the enemy spawning value and our own bullet fire rate to see how it performans with lots of explosions:


It performs quite well. This means we can wreak havoc on screen. Awesome!