Showing posts with label shoot em up. Show all posts
Showing posts with label shoot em up. Show all posts

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!

Saturday, January 24, 2015

Anansi: Sprite Animation


I was wondering about the possible techniques for sprite animation in JavaFX. So I searched the web and found a very good approach. Mike's Blog has an entry about Creating a Sprite Animation with JavaFX. Looks fairly easy to do.

We have to

  • subclass from Transition
  • implement the Transition class's interpolate method which is called in every frame
  • provide a sequence of Image objects
  • use ImageView to display each Image at a given interval

The code in the blog picks the images from a single image. We have separte images for each frame in the animation, so we have to modify the code a little.

Since we'll probably need more animations, we create a base class. Just like we did with the sprites.
public class AnimationBase extends Transition {

 private final ImageView imageView;
 private final int count;

 private int lastIndex;

 private Image[] sequence;

 public AnimationBase( Image[] sequence, double durationMs) {
  
  this.imageView = new ImageView( sequence[0]);
  this.sequence = sequence;
  this.count = sequence.length;

        setCycleCount( 1);
  setCycleDuration(Duration.millis( durationMs));
  setInterpolator(Interpolator.LINEAR);
  
 }

 protected void interpolate(double k) {
  
  final int index = Math.min((int) Math.floor(k * count), count - 1);
  if (index != lastIndex) {
   imageView.setImage(sequence[index]);
   lastIndex = index;
  }
  
 }
 
 public ImageView getView() {
  return imageView;
 }

}

With this we have the base for an animation. Now to the explosion itself. We may need multiple variations of explosions, so we create a dedicated explosion class. What do we need? The explosion happens at the sprite's location, so it's convenient if we hand over the sprite to calculate the location of the explosion. Then we need to put the view of the explosion on a layer. And we'd like to remove the exploding sprite after the explosion finishes. Let's get coding.
public class Explosion extends AnimationBase {

 public Explosion(Pane layer, SpriteBase explodingSprite, Image[] sequence, double durationMs) {
  
  super(sequence, durationMs);
  
  // explosions happen at the center of the object
  relocateToCenterOf(explodingSprite);
  
  // make explosion visible on screen
  layer.getChildren().add( getView());
  
  // remove objects when animation finishes
  setOnFinished(new EventHandler() {

   @Override
   public void handle(ActionEvent event) {
    
    // flag sprite to be removable
    explodingSprite.setRemovable(true);

    // stop animation
    stop();
    
    // remove explosion from screen
    layer.getChildren().remove( getView());
    
    
   }
  });
 }

 /**
  * Move the animation to the center of the given sprite, eg an explosion is on top of the enemy ship.
  * @param sprite
  */
 public void relocateToCenterOf( SpriteBase sprite) {
  getView().relocate( sprite.getCenterX() - getView().getImage().getWidth() / 2.0, sprite.getCenterY() - getView().getImage().getHeight() / 2.0);
 }
}

This gives us options to spawn an explosion at any sprite's location with as little code as:
private void spawnExplosion( SpriteBase sprite) {
 
  Explosion animation = new Explosion( playfieldLayer, sprite, explosionSequence, 400);
  animation.play();
 
}


We load the images for the explosion sequence the usual way in our resource loading method:
Image[] explosionSequence;
...
explosionSequence = new Image[ 10];
explosionSequence[0] = new Image( getClass().getResource("assets/effects/explosion/explosion_01/explosion_w100_01.png").toExternalForm());
explosionSequence[1] = new Image( getClass().getResource("assets/effects/explosion/explosion_01/explosion_w100_02.png").toExternalForm());
explosionSequence[2] = new Image( getClass().getResource("assets/effects/explosion/explosion_01/explosion_w100_03.png").toExternalForm());
explosionSequence[3] = new Image( getClass().getResource("assets/effects/explosion/explosion_01/explosion_w100_04.png").toExternalForm());
explosionSequence[4] = new Image( getClass().getResource("assets/effects/explosion/explosion_01/explosion_w100_05.png").toExternalForm());
explosionSequence[5] = new Image( getClass().getResource("assets/effects/explosion/explosion_01/explosion_w100_06.png").toExternalForm());
explosionSequence[6] = new Image( getClass().getResource("assets/effects/explosion/explosion_01/explosion_w100_07.png").toExternalForm());
explosionSequence[7] = new Image( getClass().getResource("assets/effects/explosion/explosion_01/explosion_w100_08.png").toExternalForm());
explosionSequence[8] = new Image( getClass().getResource("assets/effects/explosion/explosion_01/explosion_w100_09.png").toExternalForm());
explosionSequence[9] = new Image( getClass().getResource("assets/effects/explosion/explosion_01/explosion_w100_10.png").toExternalForm());

Looking at the resource loading code a resource handler would come in handy. One in which we specify a folder or a file name pattern and then the array gets created automatically depending on the files. Sooner or later we'll implement that. But let's get coding, we want to see explosions.

Anansi: Collisions


Now it's time to hit something with hour bullets and missiles. There are 2 approaches for a collision detection:

  • hit zone
  • per pixel

The per pixel collision requires performance, so we choose the hit zone. It's plain and simple: We consider each sprite a rectangle and check if its bounds overlap with the bounds of another.

Checking the bounds of a rectangle is a very fast hit detection mechanism.It has a disadvantage though. We don't have a rectangle, we have an image with transparent areas. A bullet shouldn't hit the transparent area, it should hit the object. We can overcome this by not using the entire sprite as hit zone, but some areas inside of it. Examples: The missile hits only with the missile head. A Tormentor sprite is only hit at the ship's hull, but not at the legs of the ship. These aren't hard constraints that change the way the gameplay feels. In fact this is used throughout many Shoot'em'ups. Especially in bullet hell shooters where the player's ship often has only a single pixel hit zone.

Let's get coding. What we need is just another loop and a mechanism to check the overlapping of rectangles. For now it's sufficient if we use the entire sprite's rectangular area has hit zone. We'll introduce custom hit zones later.

We put this method into the SpriteBase class:
public boolean collidesWith( SpriteBase otherSprite) {
 
 return ( otherSprite.x + otherSprite.w >= x && otherSprite.y + otherSprite.h >= y && otherSprite.x <= x + w && otherSprite.y <= y + h);
 
}

Now we can easily find out if a bullet hits an enemy ship. We simply check each bullet with each enemy and perform an action if they collide. We extend the SpriteBase class with some more meaningful methods:
/**
 * Reduce health by the amount of damage that the given sprite can inflict
 * @param sprite
 */
public void getDamagedBy( SpriteBase sprite) {
 health -= sprite.getDamage();
}

/**
 * Set health to 0
 */
public void kill() {
 setHealth( 0);
}

/**
 * Set flag that the sprite can be removed from the UI.
 */
public void remove() {
 setRemovable(true);
}

And check for the collision of bullets:
private void checkBulletCollisions() {
 
 for( PlayerBullet bullet: playerBulletList) {
  for( Enemy enemy: enemyList) {
   if( bullet.collidesWith(enemy)) {
    
    // inflict damage, reduce enemy's health
    enemy.getDamagedBy(bullet);
    
    // destroy bullet, set health to 0
    bullet.kill();
    
    // remove the bullet from screen by flagging it as removable, 
    bullet.remove();
    
   }
  }
 }
}

The check for the missiles is very similar:
private void checkMissileCollisions() {
 
 for( Missile missile: playerMissileList) {
  for( Enemy enemy: enemyList) {
   if( missile.collidesWith(enemy)) {
    
    // inflict damage, reduce enemy's health
    enemy.getDamagedBy(missile);
    
    // destroy bullet, set health to 0
    missile.kill();
    
    // remove the bullet from screen by flagging it as removable, 
    missile.remove();
    
   }
  }
 }
}

Then we use the collision check methods in the game loop:
// check collisions
// ---------------------------
checkBulletCollisions();
checkMissileCollisions();

That's it. Quite easy to do. Here's a screenshot which shows that the bullets hit something and get removed. I increased the bullet counter a bit to show the effect better.


Anansi: Chapter 3: Gameplay Elements


We have created a perfectly capable gameplay engine. I'm still impressed about how easy it was so far with JavaFX.

It is time now to add gameplay elements. The stuff that makes the game fun.

The question is which features we'd like to provide for the user. Usually you'll have to decide that kind of stuff before you start coding. But since we code for fun and have limited spare time for that we define gameplay as we code along.

Most important thing is: Make it fun! Don't decide whether it makes sense or not, but rather decide whether it's fun or not. Who cares if we can fire an unlimited amount of huge missiles out of our ship as long as we can shoot things up! :-)

So let's get coding.

Friday, January 23, 2015

Anansi: Multiplayer


As promised, it's easy to add another player to the game. All you need to do is to add this line for every player

createPlayer( scene);

and give the new players dedicated keyboard controls
input.setUpKey(KeyCode.W);
input.setDownKey(KeyCode.S);
input.setLeftKey(KeyCode.A);
input.setRightKey(KeyCode.D);
input.setPrimaryWeaponKey(KeyCode.J);
input.setSecondaryWeaponKey(KeyCode.K);

You can as well choose a different color, different weapons, whatever you prefer. The feature is available and it's as easy to add even more players.

Anansi: Global Keyboard Handler for Screenshots


Like in the prototype we want to integrate a screenshot creation mechanism. Taking screenshots isn't part of the game, so the keyboard handler got excluded from the input class. Instead we add a global event listener.

While we're at it, it's easy to add a button for pause/resume and to toggle the debug layer.

Let's get coding:

primaryStage.addEventFilter(KeyEvent.KEY_RELEASED, globalKeyEventHandler);
...
private EventHandler globalKeyEventHandler = new EventHandler() {
 
  @Override
  public void handle(KeyEvent event) {

   // toggle pause
          if( event.getCode() == KeyCode.P) { 

           if( gamePaused) {
            resumeGame();
           } else {
            pauseGame();
           }
           
          }
          // toggle debug overlay
          else if( event.getCode() == KeyCode.F10) { 

           debugLayer.setVisible( !debugLayer.isVisible());
           
          }
          // take screenshot, open save dialog and save it
          else if( event.getCode() == KeyCode.F12) {

           takeScreenshot();

          }
 }
};

And the previously mentioned utility method to take a screenshot from a scene:
public class Utils {

 /**
  * Take a screenshot of the scene in the given stage, open file save dialog and save it.
  * @param stage
  */
 public static void screenshot( Stage stage) {
  
  // take screenshot
     WritableImage image = stage.getScene().snapshot( null);

     // create file save dialog
     FileChooser fileChooser = new FileChooser();
     
     // title
        fileChooser.setTitle("Save Image");
        
        // initial directory
        fileChooser.setInitialDirectory(
                new File(System.getProperty("user.home"))
            );              
        
        // extension filter
        fileChooser.getExtensionFilters().addAll(
            // new FileChooser.ExtensionFilter("All Images", "*.*"),
            // new FileChooser.ExtensionFilter("JPG", "*.jpg"),
            new FileChooser.ExtensionFilter("PNG", "*.png")
        );
        
        // show dialog
        File file = fileChooser.showSaveDialog( stage);
        if (file != null) {
         
            try {
             
                // save file
                ImageIO.write(SwingFXUtils.fromFXImage(image, null), "png", file);
                
            } catch (IOException ex) {
             
                System.err.println(ex.getMessage());
                
            }
        }
 }
 
}

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:

Anansi: Putting it all together


We have all the pieces for the game, now we have to put them all together.

In the game loop we have to update sprite objects of various types. We keep references to them in dedicated lists.


List backgroundList = new ArrayList<>();
List cloudList = new ArrayList<>();
List enemyList = new ArrayList<>();
List playerBulletList = new ArrayList<>();
List playerList = new ArrayList<>();

ps: please excuse the html tags that blogspot adds for an unknown reason

We create the game and start it ...
// create game level (background, player, etc)
createLevel( scene);

// add nodes which display debug information
createDebugOverlay();

// create game loop
createGameLoop();

// start the game
startGame();

... using these methods ...
private void createLevel( Scene scene) {
 
 // load game assets
 loadResources();
 
 // create level structure
 createBackground();
 
 // create player, including input controls
 createPlayer( scene);
 
}

private void createBackground() {
 
 Image image = backgroundImage;
 
 double x = 0;
 double y = -image.getHeight() + Settings.SCENE_HEIGHT;
   
 Background background = new Background( backgroundLayer, backgroundImage, x, y, Settings.BACKGROUND_SPEED);
 
 backgroundList.add( background);

}

private void createPlayer( Scene scene) {
 
 // player input
 Input input = new Input( scene);
 
 // register input listeners
 input.addListeners();

 Image image = playerShipImage;
 
 // center horizontally, position at 70% vertically
 double x = (Settings.SCENE_WIDTH - image.getWidth()) / 2.0;
 double y = Settings.SCENE_HEIGHT * 0.7;
 
 // create player
 Player player = new Player(playfieldLayer, playerShipImage, x, y, 0, 0, 0, 0, Settings.PLAYER_SHIP_HEALTH, 0, Settings.PLAYER_SHIP_SPEED, input);
 
 // register player
 playerList.add( player);
 
}


We add more attributes to the settings class
public class Settings {

 public static double SCENE_WIDTH = 400;
 public static double SCENE_HEIGHT = 800;
 
 public static double BACKGROUND_SPEED = 0.6;
 
 public static double PLAYER_SHIP_SPEED = 4.0;
 public static double PLAYER_SHIP_HEALTH = 100.0;
 
 public static int ENEMY_SPAWN_RANDOMNESS = 50;
 public static int CLOUD_SPAWN_RANDOMNESS = 100;
}

We spawn the clouds at random intervals ...
private void spawnClouds( boolean random) {
 
 if( random && rnd.nextInt(Settings.CLOUD_SPAWN_RANDOMNESS) != 0) {
  return;
 }
 
 Pane layer;
 Image image;
 double speed;
 double opacity;
 double x;
 double y;

 // determine random layer
 // clouds in the upper layer are less opaque than the ones in the lower layer so that the player ship is always visible
 // and the upper layer clouds are faster
 if( rnd.nextInt(2) == 0) {
  layer = lowerCloudLayer;
  opacity = 1.0;
    speed = rnd.nextDouble() * 1.0 + 1.0;
 } else {
  layer = upperCloudLayer;
  opacity = 0.5;
    speed = rnd.nextDouble() * 1.0 + 2.0;
 }

 // determine random image
 image = cloudImages[ rnd.nextInt( cloudImages.length)]; 
 
 // set position horizontally so that half of it may be outside of the view, vertically so that it enters the view with the next movement
 x = rnd.nextDouble() * Settings.SCENE_WIDTH - image.getWidth() / 2.0;
 y = -image.getHeight();
 
 // create a sprite 
 Cloud cloud = new Cloud( layer, image, x, y, speed, opacity);
 
 // manage sprite
 cloudList.add( cloud);
  
}

... and the enemies ...
private void spawnEnemies( boolean random) {
 
 if( random && rnd.nextInt(Settings.ENEMY_SPAWN_RANDOMNESS) != 0) {
  return;
 }
 
 // image
 Image image = enemyImage;
 
 // random speed
 double speed = rnd.nextDouble() * 1.0 + 1.0;
 
 // x position range: enemy is always fully inside the screen, no part of it is outside
 // y position: right on top of the view, so that it becomes visible with the next game iteration
 double x = rnd.nextDouble() * (Settings.SCENE_WIDTH - image.getWidth());
 double y = -image.getHeight();
 
 // create a sprite
 Enemy enemy = new Enemy( playfieldLayer, image, x, y, 0, 0, speed, 0, 1,1);
 
 // manage sprite
 enemyList.add( enemy);
   
}

... and the bullets whenever the player demands it.
private void spawnPrimaryWeaponObjects( Player player) {
 
 player.chargePrimaryWeapon();

   if( player.isFirePrimaryWeapon()) {
    
    PlayerBullet playerBullet;

    Image image = playerBulletImage;
    
    // primary weapon gives horizontal center of the cannon, but we also need to consider the center of the bullet
    double x = player.getPrimaryWeaponX() - image.getWidth() / 2.0; 
    double y = player.getPrimaryWeaponY();

    double spread = player.getPrimaryWeaponBulletSpread();
    double count = player.getPrimaryWeaponBulletCount();
    double speed = player.getPrimaryWeaponBulletSpeed();
    
    // create sprite
    playerBullet = new PlayerBullet( bulletLayer, image, x, y, 0, -speed);
    playerBulletList.add( playerBullet);
    
    // left/right: vary x-axis position
    for( int i=0; i < count / 2.0; i++) {

        // left
        playerBullet = new PlayerBullet( bulletLayer, image, x, y, -spread * i, -speed);
        playerBulletList.add( playerBullet);

        // right
        playerBullet = new PlayerBullet( bulletLayer, image, x, y, spread * i, -speed);
        playerBulletList.add( playerBullet);

    }
    
    player.unchargePrimaryWeapon();
   }
 
}

We then have to process various methods like move() on every sprite. We create helper methods which do that for us. There's an alternative to the for loop by using forEach in Java 8, but we keep the for loop for now. That way the game remains playable with Java 7. The main advantage of Java 8 would be how easy it is to parallelize code by using streams. Maybe we'll come back to that later. There's no performance problem, so let's not make things too complicated for now.
private void moveSprites( List spriteList) {

 for( SpriteBase item: spriteList) {
  item.move();
 }
 
}

private void updateSpritesUI( List spriteList) {

 for( SpriteBase item: spriteList) {
  item.updateUI();
 }
 
}

private void checkRemovability( List spriteList) {

 for( SpriteBase item: spriteList) {
  item.checkRemovability();
 }
 
}

In case you are curious, instead of a method with the for loop, you could as well write in Java 8
spriteList.forEach( player -> player.move());

Now we invoke these methods in the game loop
private void createGameLoop() {
 
    gameLoop = new AnimationTimer() {
     
        @Override
        public void handle(long l) {
         
         // player AI (player input)
         // ---------------------------
         for( Player player: playerList) {
          player.processInput();
         }
         
         // add random sprites
         // --------------------------
         spawnClouds( true);
         spawnEnemies( true);

         // spawn bullets of players
         // ---------------------------
         for( Player player: playerList) {
          spawnPrimaryWeaponObjects( player);
         }
         
         // move sprites internally
         // ---------------------------
         moveSprites( playerList);
         moveSprites( backgroundList);
         
         moveSprites( playerBulletList);
         moveSprites( cloudList);
         moveSprites( enemyList);

         // move sprites on screen
         // ---------------------------
         updateSpritesUI(playerList);
         updateSpritesUI(backgroundList);
         
         updateSpritesUI(playerBulletList);
         updateSpritesUI(cloudList);
         updateSpritesUI(enemyList);

         // check if sprite can be removed
         // ------------------------------
         checkRemovability( playerBulletList);
         checkRemovability( cloudList);
         checkRemovability( enemyList);
         
         // remove removables from list, layer, etc
         // ------------------------------
         removeSprites( playerBulletList);
         removeSprites( cloudList);
         removeSprites( enemyList);
         

         // calculate fps
         // ---------------------------
         updateFps();
         
         // show debug information (fps etc)
         // --------------------------------
         updateDebugOverlay();
        }

    };
    
}

And that was about it. We now have a game engine with which we can proceed.

Finally here's a screenshot about what the game looks like.


Maybe not much of a difference to you compared to the prototype, but you'll see how easy it will be to add other kinds of stuff. Next time we spawn heat seeking missiles and make things go booom.


Anansi: The Player Ship


We now have a playfield, a sprite base class and a keyboard handler. It's time we add the player ship.

The class for the playership needs some information from us:

  • the user input for ship navigation
  • the boundary of the playfield so that the ship can't be navigated outside
  • the ship's speed

The ship is equipped with weapons. We'll add a cannon to it. The cannon can be fired, but needs to recharge itself, so that the firing won't happen too quickly.

There are several options to fire a bullet. One of them is that the player's ship spawns the bullets. But for that it would need to know about the bullet layer. So I'd rather prefer we only indicate that a bullet is fired and let the sprite manager, i. e. for now a method call in the game loop do the bullet spawning for us.

The player class:


public class Player extends SpriteBase {

 double playerShipMinX;
 double playerShipMaxX;
 double playerShipMinY;
 double playerShipMaxY;

 Input input;
 
 double speed;
 
 double cannonChargeTime = 6; // the cannon can fire every n frames 
 double cannonChargeCounter = cannonChargeTime; // initially the cannon is charged
 double cannonChargeCounterDelta = 1; // counter is increased by this value each frame 
 
 double cannonBullets = 5; // number of bullets which the cannon can fire in 1 shot (center, left, right)
 double cannonBulletSpread = 0.6; // dx of left and right bullets
 double cannonBulletSpeed = 8.0; // speed of each bullet

 public Player(Pane layer, Image image, double x, double y, double r, double dx, double dy, double dr, double health, double damage, double speed, Input input) {

  super(layer, image, x, y, r, dx, dy, dr, health, damage);

  this.speed = speed;
  this.input = input;
  
  init();
 }

 
 private void init() {
  
  // calculate movement bounds of the player ship
  // allow half of the ship to be outside of the screen 
  playerShipMinX = 0 - image.getWidth() / 2.0;
  playerShipMaxX = Settings.SCENE_WIDTH - image.getWidth() / 2.0;
  playerShipMinY = 0 - image.getHeight() / 2.0;
  playerShipMaxY = Settings.SCENE_HEIGHT -image.getHeight() / 2.0;
  
 }

 public void processInput() {
  
  // ------------------------------------
  // movement
  // ------------------------------------
  
     // vertical direction
     if( input.isMoveUp()) {
      dy = -speed;
     } else if( input.isMoveDown()) {
      dy = speed;
     } else {
      dy = 0d;
     }
     
     // horizontal direction
     if( input.isMoveLeft()) {
      dx = -speed;
     } else if( input.isMoveRight()) {
      dx = speed;
     } else {
      dx = 0d;
     }

 }
 
 @Override
 public void move() {
  
  super.move();
  
  // ensure the ship can't move outside of the screen
  checkBounds();
  
     
 }
 
 private void checkBounds() {

     // vertical
     if( Double.compare( y, playerShipMinY) < 0) {
      y = playerShipMinY;
     } else if( Double.compare(y, playerShipMaxY) > 0) {
      y = playerShipMaxY;
     }

     // horizontal
     if( Double.compare( x, playerShipMinX) < 0) {
      x = playerShipMinX;
     } else if( Double.compare(x, playerShipMaxX) > 0) {
      x = playerShipMaxX;
     }

 }
 
 @Override
 public void checkRemovability() {
  // TODO will be added later when the player ship explodes
 }

 public boolean isFirePrimaryWeapon() {
  
     boolean isCannonCharged = cannonChargeCounter >= cannonChargeTime;
     
     return input.isFirePrimaryWeapon() && isCannonCharged;
     
 }

 public void chargePrimaryWeapon() {
  
     // limit weapon fire
     // ---------------------------
     // charge weapon: increase a counter by some delta. once it reaches a limit, the weapon is considered charged
     cannonChargeCounter += cannonChargeCounterDelta;
     if( cannonChargeCounter > cannonChargeTime) {
      cannonChargeCounter = cannonChargeTime;
     }
     
 }

 public void unchargePrimaryWeapon() {
  
  // player bullet uncharged
  cannonChargeCounter = 0;

 }
 
 public double getPrimaryWeaponX() {
  return x + image.getWidth() / 2.0; // center of the ship
 }
 public double getPrimaryWeaponY() {
  return y;
 }
 
 public double getPrimaryWeaponBulletSpread() {
  return cannonBulletSpread;
 }
 public double getPrimaryWeaponBulletCount() {
  return cannonBullets;
 }
 public double getPrimaryWeaponBulletSpeed() {
  return cannonBulletSpeed;
 }
 
}


Now we have the playfield, scrolling environment, enemies, bullets, player and the input. What's left to do is to put it all together.

Anansi: Input


For the input we use the mechanism from the prototype, it's perfectly fine. As mentioned earlier we use a bitset to know if the user keeps a key being pressed.

I modified the code a bit so that we can add and remove the listener. The listener will have to be removed once the game is over.

A dedicated class for the input handler allows us to create another instance of it. This way it should be easy to assign another instance to a second player which gives us the freedom of creating a multiplayer game with only a few additional lines of code.

I'll leave the key codes hardcoded for now. In the end we'll use custom configurations per player.

Here's the keyboard handler:

public class Input {

 /**
  * Bitset which registers if any {@link KeyCode} keeps being pressed or if it is released.
  * 
  * Note: ordinal may be not appropriate, but we don't have a method in {@link KeyCode} to get the int code, so it'll have to do 
  */
 private BitSet keyboardBitSet = new BitSet();

 // -------------------------------------------------
 // default key codes
 // we will vary them later when we let the user customize the key codes or when we add support for a 2nd player
 // -------------------------------------------------
 
 private KeyCode upKey = KeyCode.UP;
 private KeyCode downKey = KeyCode.DOWN;
 private KeyCode leftKey = KeyCode.LEFT;
 private KeyCode rightKey = KeyCode.RIGHT;
 private KeyCode primaryWeaponKey = KeyCode.SPACE;
 private KeyCode secondaryWeaponKey = KeyCode.CONTROL;
 
 Scene scene;
 
 public Input( Scene scene) {
  this.scene = scene;
 }
 
 public void addListeners() {
  
  scene.addEventFilter(KeyEvent.KEY_PRESSED, keyPressedEventHandler);
  scene.addEventFilter(KeyEvent.KEY_RELEASED, keyReleasedEventHandler);
  
 }

 public void removeListeners() {
  
  scene.removeEventFilter(KeyEvent.KEY_PRESSED, keyPressedEventHandler);
  scene.removeEventFilter(KeyEvent.KEY_RELEASED, keyReleasedEventHandler);
  
 }

 /**
  * "Key Pressed" handler for all input events: register pressed key in the bitset
  */
 private EventHandler keyPressedEventHandler = new EventHandler() {
  @Override
  public void handle(KeyEvent event) {
   
   // register key down
   keyboardBitSet.set(event.getCode().ordinal(), true);
   
  }
 };
 
 /**
  * "Key Released" handler for all input events: unregister released key in the bitset
  */
 private EventHandler keyReleasedEventHandler = new EventHandler() {
  @Override
  public void handle(KeyEvent event) {
   
   // register key up
   keyboardBitSet.set(event.getCode().ordinal(), false);
   
  }
 };
 

 // -------------------------------------------------
 // Evaluate bitset of pressed keys and return the player input.
 // If direction and its opposite direction are pressed simultaneously, then the direction isn't handled.
 // -------------------------------------------------
 
 public boolean isMoveUp() {
  return keyboardBitSet.get( upKey.ordinal()) && !keyboardBitSet.get( downKey.ordinal());
 }
 
 public boolean isMoveDown() {
  return keyboardBitSet.get( downKey.ordinal()) && !keyboardBitSet.get( upKey.ordinal());
 }

 public boolean isMoveLeft() {
  return keyboardBitSet.get( leftKey.ordinal()) && !keyboardBitSet.get( rightKey.ordinal()); 
 }

 public boolean isMoveRight() {
  return keyboardBitSet.get( rightKey.ordinal()) && !keyboardBitSet.get( leftKey.ordinal());
 }

 public boolean isFirePrimaryWeapon() {
  return keyboardBitSet.get( primaryWeaponKey.ordinal());
 }
 
 public boolean isFireSecondaryWeapon() {
  return keyboardBitSet.get( secondaryWeaponKey.ordinal());
 }

 
 // -------------------------------------------------
 // getters & setters for key code modification (more players, user key settings change)
 // -------------------------------------------------
 
}


ps: blogspot seems to modify the code, so please ignore the </keyevent> text and replace keyevent with KeyEvent; I'll put up the code elsewhere later.

Anansi: Sprites - Part 2


Now that we have a sprite base class, we can subclass it and easily add various objects to the playfield.

We can create the class for the background ...

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
 }


}

and similarly easy the cloud class which has an additional opacity attribute, depending on the layer ...
public class Cloud extends SpriteBase {

 public Cloud(Pane layer, Image image, double x, double y, double speed, double opacity) {
  
  super(layer, image, x, y, 0.0, 0.0, speed, 0.0, Double.MAX_VALUE, 0.0);

  getView().setOpacity(opacity);
  
 }

 @Override
 public void checkRemovability() {

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


}

... and we create the player bullet class ...
public class PlayerBullet extends SpriteBase {

 public PlayerBullet(Pane layer, Image image, double x, double y, double dx, double dy) {
  super(layer, image, x, y, 0, dx, dy, 0, 1, 1); // TODO: health/damage
 }

 public PlayerBullet(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() {
  
  // upper bounds exceeded
  if( ( getY() + getHeight()) < 0) {
   setRemovable(true);
  }
  
 }

}

... and the enemy class.
public class Enemy extends SpriteBase {

 public Enemy(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, 1, 1); // TODO: health/damage
 }

 @Override
 public void checkRemovability() {

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

  
 }
}

Anansi: Sprites


Okay, we're eager to finally see stuff happening on the screen. From the prototype we can identify various common denominators. Every sprite has the following attributes:

  • an Image
  • a container, which is an ImageView
  • a layer in which we embed the sprite
  • position x and y
  • rotation r
  • a delta by which x, y and r are modified per frame 
  • the sprite dimensions


And while we're at it we can as well add other attributes which we may need later:

  • health

    When the health is depleted, the sprite is considered dead.
  • damage

    The damage a sprite can inflict on another sprite.

    In order to make things not too complicated we think that every sprite is created using the same material. This means that when sprites collide, then "current health = current health - colliding sprite's damage value"
  • width, height, center

    that information is usually always needed
  • removable

    Determine whether the sprite is removable or not. A sprite is removable when it's outside the screen and won't be visible anymore or when its health is depleted.

Given that information we can also create common methods. The things we need are

  • add a sprite's view to a layer
  • remove the sprite's view from the layer
  • move the sprite internally
  • check health status
  • update the sprite on the playfield layer
  • check the bounds

The methods are simple. Although, what about the bounds checking? How do we know that an object is out of its bounds? A missile may go out of bounds, but can come back. A bullet on the other hand will never return. The one who knows whether it can come back or not is the object itself. So we create an abstract checkRemovability() method in which we set a flag with which the game loop as a sprite manager can decide whether or not the sprite should be disposed of.

So all in all the code is as simple as this:


public abstract class SpriteBase {

 Image image;
 ImageView imageView;

 Pane layer;

 double x;
 double y;
 double r;

 double dx;
 double dy;
 double dr;

 double health;
 double damage;

 boolean removable = false;

 double w;
 double h;

 public SpriteBase(Pane layer, Image image, double x, double y, double r, double dx, double dy, double dr, double health, double damage) {

  this.layer = layer;
  this.image = image;
  this.x = x;
  this.y = y;
  this.r = r;
  this.dx = dx;
  this.dy = dy;
  this.dr = dr;

  this.health = health;
  this.damage = damage;

  this.imageView = new ImageView(image);
  this.imageView.relocate(x, y);
  this.imageView.setRotate(r);

  this.w = image.getWidth();
  this.h = image.getHeight();

  addToLayer();

 }

 public void addToLayer() {
  this.layer.getChildren().add(this.imageView);
 }

 public void removeFromLayer() {
  this.layer.getChildren().remove(this.imageView);
 }

 public void move() {

  x += dx;
  y += dy;
  r += dr;

 }

 public boolean isAlive() {
  return Double.compare(health, 0) > 0;
 }

 public void updateUI() {

  imageView.relocate(x, y);
  imageView.setRotate(r);

 }
 
 public abstract void checkRemovability();

  // -----------------------------------------
  // automatically generated getters & setters
  // -----------------------------------------
  
  ...
}


That seems rather short, but indeed, that's it. And it looks like we can not only create the player's ship, enemy ships and bullets, but also the background and clouds with the same sprite code base. Nice. You may think that a background won't have health and damage, that's right. But you never know what we'll come up with. A cloud doesn't have health and damage either. Only by thinking about how a cloud can have health and damage, we come up with a new gameplay element: Acid clouds! So let's not limit ourselves by unplanned ideas. The base is there, let's use it. Now it's time to add the gameplay elements.

Anansi: Debug Overlay


Before we add the sprites we need should ensure that we have debug information. There'll be lots of sprites and we want to verify that all of them are removed from memory so that we don't get memory leaks.

We keep the code from the prototype and move it to a dedicated method, so that we don't clutter the game loop.

We stick with the label that contains the debug information. During gameplay we have the visual focus on the top of the playfield, so we position the debug information on the bottom of the screen. Moreover we'd like to have it stick out a bit. JavaFX gives us great and easy to use tools to make a semi-transparent background.

To make things short, here's the code. It's similar to the one of the prototype with the exception that the width of the overlay spans over the entire screen and that it's attached to the bottom. You can use JavaFX's bind methods to achieve that.

We create the overlay
private void createDebugOverlay() {
 
 // debug info container
 debugLabel = new Label();
 debugLabel.setTextFill(Color.RED);
 
 // add to layer
 debugLayer.getChildren().add( debugLabel);
 
 // semi-transparent gray background
 debugLayer.setStyle("-fx-background-color:rgba(0,0,0,0.3)");

 // stretch the layer horizontally 
 debugLayer.prefWidthProperty().bind(primaryStage.getScene().widthProperty());
 
 // bind the layer to the bottom of the screen,
 debugLayer.layoutYProperty().bind(primaryStage.getScene().heightProperty().subtract(debugLayer.heightProperty()));
 
}


And a method for calculating the FPS which we invoke in our game loop.
// counter for game loop
int frameCount = 0;
int fpsCurrent = 0;
long prevTime = -1;
...
private void updateFps() {
 
   frameCount++;
   
   long currTime = System.currentTimeMillis();
   
   if( currTime - prevTime >= 1000) {
    
    // get current fps
    fpsCurrent = frameCount;

    // reset counter every second
    prevTime = currTime;
    frameCount = 0;
   }
   
}

We update the debug container with the information we wish to display.
private void updateDebugOverlay() {

   // show debug info
   // ---------------------------
   debugLabel.setText("FPS: " + fpsCurrent);

}

And extend the game loop with the just created methods:
private void createGameLoop() {

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

       // calculate fps and show it
       // ---------------------------
       updateFps();
       
       // show debug information (fps etc)
       // --------------------------------
       updateDebugOverlay();
      }

  };
  
}

Anansi: Resources


Before we can create the sprites we need to load the images for them. The code is straightforward:

Image backgroundImage;
Image cloudImages[];
Image playerShipImage;
Image playerBulletImage;
Image playerMissileImage;
Image enemyImage;
...
private void loadResources() {
 
 // background
 // --------------------------------
 backgroundImage = new Image( getClass().getResource( "assets/maps/canyon.jpg").toExternalForm());
 
 // player ship
 // --------------------------------
 playerShipImage = new Image( getClass().getResource( "assets/vehicles/anansi.png").toExternalForm());
 
 // clouds
 // --------------------------------
 cloudImages = new Image[4];
 cloudImages[0] = new Image( getClass().getResource( "assets/environment/cloud-01.png").toExternalForm());
 cloudImages[1] = new Image( getClass().getResource( "assets/environment/cloud-02.png").toExternalForm());
 cloudImages[2] = new Image( getClass().getResource( "assets/environment/cloud-03.png").toExternalForm());
 cloudImages[3] = new Image( getClass().getResource( "assets/environment/cloud-04.png").toExternalForm());
 
 // bullets
 playerBulletImage = new Image( getClass().getResource( "assets/bullets/bullet_01.png").toExternalForm());
  
 // enemy ship: tormentor
 enemyImage = new Image( getClass().getResource( "assets/vehicles/tormentor.png").toExternalForm());
}

There isn't much to add to this, it's simple. Since everything is very similar one could create an identifier (eg enum) for each image type and put it into a map with the filename as value, iterate the map and load the resources that way. But we'll leave that for later when we create multiple levels and need a resource manager.

As you may already suggest, this method would also load other resources like e. g. sound files.