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.

Anansi: Layers


Our game will have various layers to show some kind of depth. The layers are in this order:

  • scrolling background
  • lower clouds
  • bullets
  • playfield (player, enemies)
  • upper clouds
  • score
  • debug information

The code for this is straightforward:

Pane backgroundLayer;
Pane lowerCloudLayer;
Pane bulletLayer;
Pane playfieldLayer;
Pane upperCloudLayer;
Pane debugLayer;

@Override
public void start(Stage primaryStage) {
 
 this.primaryStage = primaryStage;
 
 try {

  // create root node
  Group root = new Group();
  
  // create layers
  backgroundLayer = new Pane();
  lowerCloudLayer = new Pane();
  bulletLayer = new Pane();
  playfieldLayer = new Pane();
  upperCloudLayer = new Pane();
  debugLayer = new Pane();
  
  // add layers to scene root
  root.getChildren().add( backgroundLayer);
  root.getChildren().add( lowerCloudLayer);
  root.getChildren().add( bulletLayer);
  root.getChildren().add( playfieldLayer);
  root.getChildren().add( upperCloudLayer);
  root.getChildren().add( debugLayer);

  // create scene
  Scene scene = new Scene( root, Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT, Color.BLACK);
  
  // show stage
  primaryStage.setScene(scene);
  primaryStage.show();
  
  ...
  
  // start the game
  startGame();
  
 } catch(Exception e) {
  e.printStackTrace();
 }
}
 
We introduced a special Settings class so that we can change the game's attributes at one place.
public class Settings {

 public static double SCENE_WIDTH = 400;
 public static double SCENE_HEIGHT = 800;
 
}

This way it's easy to vary the scene dimensions depending on your playfield size.

Anansi: The Game Loop - Revisited


We decided to go with the AnimationTimer. It runs smooth at fixed 60fps and does what we want.

It should be possible for a game loop to

  • start
  • stop
  • pause
  • resume

The AnimationTimer doesn't have a pause flag, so we'll add a simple boolean to indicate the state.

Here's what we need:

private AnimationTimer gameLoop;
private boolean gamePaused = false;
...
private void startGame() {
 
      gameLoop.start();
      
}

private void pauseGame() {

 gamePaused = true;
 
 gameLoop.stop();
 
}

private void resumeGame() {

 gamePaused = false;

 gameLoop.start();
 
}

private void stopGame() {
 
 // TODO: remove event handlers (player, etc)
 
}

private void createGameLoop() {
 
  gameLoop = new AnimationTimer() {
   
      @Override
      public void handle(long l) {
       
        // player AI (input)
       
        // sprite AI

        // add sprites (clouds, enemies, bullets, missiles)

        // move sprites internally

        // move sprites in the UI

        // check if sprites can be removed (eg collsion, off-screen)

        // update debug information    

      }

  };
      
} 

That's about it. We'll add other parts like sprite collision once the basic engine is complete.

Anansi: Chapter 2: From Prototype To Game


It's finally weekend, time to continue with the hobby. We've seen that JavaFX gives us nice means to create a 2D vertically scrolling Shoot'em'up. What we do now is to find common denominators, unify the code from the prototype and extend it.

Here's a summary of what we need:

  • game loop
  • resource loading mechanism
  • input (keyboard in our case)
  • layers
  • sprites
  • sprite management (move, update ui, explode, artificial intelligence)
  • utilities (debug information, screenshot)

There would be some things to consider which we'll do later. I don't want to confuse you with an unnecessary complexity. For now. For a full game we'd need e. g.

  • a level manager for multiple levels
  • a resource manager 
  • a sprite manager
  • multi-player support
  • ...  

If we structure the code in regards to these requirements, it will be easy to add them later. For now we want a single player game with 1 Level, however one with diversity. Rewriting the prototype should be done within a few hours. So let's get coding!

Tuesday, January 20, 2015

Anansi: Chapter 1 Conclusion


We now have the answers to the initial questions:

  • Can we implement a 2D vertically scrolling shoot'em'up with JavaFX?

    Yes, that's definitely possible.
  • How easy is it to create a game? I prefer Rapid Application Development.

    I did the prototype within a few hours. I'd say given some JavaFX experience it's a very good option to create something from scratch and get a quick result.
  • Do we need 3rd party libraries or is JavaFX sufficient?

    So far we use no 3rd party libraries. And I'd like to keep it this way. The less dependencies the better. Who knows, maybe we port it Android. I've read that it would be possible to run JavaFX on mobile devices. I haven't done it yet and I have no information about how these ports perform, but it's something I'll most certainly try.
  • How does the game perform with a lot of stuff happening on the screen?

    The prototype performs very well with a lot of sprites being on the screen simultaneously. Of course we have to expect much more stuff happening on the screen as we code along, but from what I've seen so far to me it's a GO to continue.
  • The amount of time to create the prototype.

    Creating the prototype took a few hours. We were very well below 1000 lines of code which is very good. The less code the better. I even think that creating a game like that would be a candidate for a "Creating a game in 24 hours or less" article.


I'd say we're done with the prototype and can get serious with our game. I'm looking forward to see what we'll get ... let's keep on coding! :-)


Sunday, January 18, 2015

Anansi: Screenshots


I'd like to get rid of the border around the screenshots of our game. Until now I did them with windows print screen key. JavaFX has a built-in snapshot command. So let's use that one.

Here's a small utility class that I created using a tutorial on the Oracle website.
package game.utils;

import java.io.File;
import java.io.IOException;

import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.WritableImage;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

import javax.imageio.ImageIO;

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

We can use it by extending our KEY_RELEASED event handler. The utility method demands a stage as parameter, so we need to keep a reference of the primary stage in a global variable.
Stage primaryStage;
 
@Override
public void start(Stage primaryStage) {

    this.primaryStage = primaryStage;
    ...

}
         

The screenshot key will be F12.
...
 @Override
 public void handle(KeyEvent event) {
 
  // register key up
  keyboardBitSet.set(event.getCode().ordinal(), false);
 
  // take screenshot, open save dialog and save it
  if( event.getCode() == KeyCode.F12) {
   
   // pause game
   gameLoop.stop();
   
   // save screenshot
   Utils.screenshot( primaryStage);
  
   // resume game
   gameLoop.start();
  
  }
 
 }
...
And now we got nice PNG files without the windows border whenever we press the screenshot key:

Anansi: Enemies


Adding enemies is very easy with the mechanism we have. It's similar to the code in which we add the clouds.

In the original game the alien ship was called a "Tormentor". I took a screenshot of it and use it in the game. Of course as usual you can pick whatever image you prefer. If you'd like to use a plane, simply replace the one in our code with it.

Speaking of code, we create the image for the enemy and load it.

Image tormentor;
...
// enemy ship: tormentor
tormentor = new Image( getClass().getResource( "assets/vehicles/tormentor.png").toExternalForm());

And then we add the enemy at random intervals in our game loop.
// add enemies
// --------------------------
// add enemies at random intervals
if( rnd.nextInt(100) == 0) {
 
  // create sprite
  ImageView enemy = new ImageView( tormentor);

  // 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
  Sprite sprite = new Sprite( playfieldLayer, enemy, rnd.nextDouble() * (SCENE_WIDTH - enemy.getImage().getWidth()), -enemy.getImage().getHeight(), 0, speed);
  sprites.add( sprite);

}

The game with the enemies looks like this:


You can clearly distinguish the various layers we have.

Now we're eager to see how it all performs with lots of enemies. It all depends on the power of your machine. I reduced the randomness interval from 100 to 10 and got this still running at 60 FPS:


Even reducing it to 1 and spawning an enemy at every frame didn't cause the FPS to drop. Very awesome!



But let's not get ahead of ourselves. There are still a few things to do which may cost performance, one of them being that we'd like to shoot the enemy and see an explosion.

Code-wise I think we can certainly continue. In the intermediary summary section you've seen the current code base. It's not nice to have it all in one game loop. Moreover our sprites belong to different categories and we should make proper objects of them. It's time for refactoring in one of the next lessons.

Anansi: The Story So Far ...


We've achieved quite a lot in these few hours of happy coding. I have to admit that i'm still very impressed with JavaFX. That's a lot of functionality for this relatively little amount of code.



Here's what we got so far:

Main.java
package game;
 
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;


public class Main extends Application {
 
 private double SCENE_WIDTH = 400;
 private double SCENE_HEIGHT = 800;
 
 Random rnd = new Random();
 
 private AnimationTimer gameLoop;

 ImageView backgroundImageView;
 ImageView playerShip;
 Image playerBullet;
 
 double playerShipSpeed = 4.0;
 double playerShipDeltaX = 0.0;
 double playerShipDeltaY = 0.0;
 
 double backgroundScrollSpeed = 0.6;
 
 Pane backgroundLayer;
 Pane lowerCloudLayer;
 Pane bulletLayer;
 Pane playfieldLayer;
 Pane upperCloudLayer;
 Pane debugLayer;
 
 Label debugLabel;
 
 // note: ordinal may be not appropriate, but we don't have a method in keycode to get the int code, so it'll have to do
 BitSet keyboardBitSet = new BitSet();
 
 // counter for game loop
 int frameCount = 0;
 int fpsCurrent = 0;
 long prevTime = -1;
 
 // list of available cloud images
 Image clouds[];
 
 List sprites = new ArrayList();
 
 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
 
 @Override
 public void start(Stage primaryStage) {
  try {

   // create root node
   Group root = new Group();

   // create layers
   backgroundLayer = new Pane();
   lowerCloudLayer = new Pane();
   bulletLayer = new Pane();
   playfieldLayer = new Pane();
   upperCloudLayer = new Pane();
   debugLayer = new Pane();
   
   // add layers to scene root
   root.getChildren().add( backgroundLayer);
   root.getChildren().add( lowerCloudLayer);
   root.getChildren().add( bulletLayer);
   root.getChildren().add( playfieldLayer);
   root.getChildren().add( upperCloudLayer);
   root.getChildren().add( debugLayer);
   
   // create scene
   Scene scene = new Scene( root, SCENE_WIDTH,SCENE_HEIGHT);
   
   // show stage
   primaryStage.setScene(scene);
   primaryStage.show();
   
   // load game assets
   loadGame();

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

   // keyboard control
   addInputControls( scene);
   
   // start the game
   startGameLoop();
   
  } catch(Exception e) {
   e.printStackTrace();
  }
 }
 
 private void addDebugInformation() {
  
  debugLabel = new Label();
  debugLabel.setTextFill(Color.RED);
  debugLayer.getChildren().add( debugLabel);
  
 }

 private void addInputControls( Scene scene) {
  
  // keyboard handler: key pressed
  scene.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler() {
   @Override
   public void handle(KeyEvent event) {
    
    keyboardBitSet.set(event.getCode().ordinal(), true);
    
   }
  });
  
        // keyboard handler: key up
  scene.addEventFilter(KeyEvent.KEY_RELEASED, new EventHandler() {
   @Override
   public void handle(KeyEvent event) {
    
    keyboardBitSet.set(event.getCode().ordinal(), false);
    
   }
  });
  
 }
 
 private void loadGame() {
  
  // background
  // --------------------------------
  backgroundImageView = new ImageView( getClass().getResource( "assets/maps/canyon.jpg").toExternalForm());
  
  // reposition the map. it is scrolling from bottom of the background to top of the background
  backgroundImageView.relocate( 0, -backgroundImageView.getImage().getHeight() + SCENE_HEIGHT);
  
  // add background to layer
  backgroundLayer.getChildren().add( backgroundImageView);
  
  // player ship
  // --------------------------------
  playerShip = new ImageView( getClass().getResource( "assets/vehicles/anansi.png").toExternalForm());
  
  // center horizontally, position at 70% vertically
  playerShip.relocate( (SCENE_WIDTH - playerShip.getImage().getWidth()) / 2.0, SCENE_HEIGHT * 0.7);
  
  // add to playfield layer
  playfieldLayer.getChildren().add( playerShip);
  
  // clouds
  // --------------------------------
  clouds = new Image[4];
  clouds[0] = new Image( getClass().getResource( "assets/environment/cloud-01.png").toExternalForm());
  clouds[1] = new Image( getClass().getResource( "assets/environment/cloud-02.png").toExternalForm());
  clouds[2] = new Image( getClass().getResource( "assets/environment/cloud-03.png").toExternalForm());
  clouds[3] = new Image( getClass().getResource( "assets/environment/cloud-04.png").toExternalForm());
  
  // bullets
  playerBullet = new Image( getClass().getResource( "assets/bullets/bullet_01.png").toExternalForm());
 }
 
 private void startGameLoop() {

  // calculate movement bounds of the player ship
  // allow half of the ship to be outside of the screen 
  double playerShipMinX = 0 - playerShip.getImage().getWidth() / 2.0;
  double playerShipMaxX = SCENE_WIDTH - playerShip.getImage().getWidth() / 2.0;
  double playerShipMinY = 0 - playerShip.getImage().getHeight() / 2.0;
  double playerShipMaxY = SCENE_HEIGHT - playerShip.getImage().getHeight() / 2.0;
  
  // game loop
        gameLoop = new AnimationTimer() {
         
            @Override
            public void handle(long l) {
            
             // get keyboard input
             // ---------------------------
             // note: ordinal may be not appropriate, but we don't have an method in keycode to get the int code, so it'll have to do

             // evaluate keyboard events
             boolean isUpPressed = keyboardBitSet.get(KeyCode.UP.ordinal()); 
             boolean isDownPressed = keyboardBitSet.get(KeyCode.DOWN.ordinal()); 
             boolean isLeftPressed = keyboardBitSet.get(KeyCode.LEFT.ordinal()); 
             boolean isRightPressed = keyboardBitSet.get(KeyCode.RIGHT.ordinal());
             boolean isSpacePressed = keyboardBitSet.get(KeyCode.SPACE.ordinal());
             boolean isControlPressed = keyboardBitSet.get(KeyCode.CONTROL.ordinal());
             
             // vertical direction
             if( isUpPressed && !isDownPressed) {
              playerShipDeltaY = -playerShipSpeed;
             } else if( !isUpPressed && isDownPressed) {
              playerShipDeltaY = playerShipSpeed;
             } else {
              playerShipDeltaY = 0d;
             }
             
             // horizontal direction
             if( isLeftPressed && !isRightPressed) {
              playerShipDeltaX = -playerShipSpeed;
             } else if( !isLeftPressed && isRightPressed) {
              playerShipDeltaX = playerShipSpeed;
             } else {
              playerShipDeltaX = 0d;
             }
             
             // scroll background
             // ---------------------------
             // calculate new position
             double y = backgroundImageView.getLayoutY() + backgroundScrollSpeed;
             
             // 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;
             }

             // move background
             backgroundImageView.setLayoutY( y);

             // move player ship
             // ---------------------------
             double newX = playerShip.getLayoutX() + playerShipDeltaX;
             double newY = playerShip.getLayoutY() + playerShipDeltaY;
             
             // check bounds
             // vertical
             if( Double.compare( newY, playerShipMinY) < 0) {
              newY = playerShipMinY;
             } else if( Double.compare(newY, playerShipMaxY) > 0) {
              newY = playerShipMaxY;
             }

             // horizontal
             if( Double.compare( newX, playerShipMinX) < 0) {
              newX = playerShipMinX;
             } else if( Double.compare(newX, playerShipMaxX) > 0) {
              newX = playerShipMaxX;
             }
             
             playerShip.setLayoutX( newX);
             playerShip.setLayoutY( newY);

             // limit bullet fire
             // ---------------------------
             // charge player cannon: increase a counter by some delta. once it reaches a limit, the cannon is considered charged
             cannonChargeCounter += cannonChargeCounterDelta;
             if( cannonChargeCounter > cannonChargeTime) {
              cannonChargeCounter = cannonChargeTime;
             }
             
             // fire player bullets
             // ---------------------------
             boolean isCannonCharged = cannonChargeCounter >= cannonChargeTime;
             if( isSpacePressed && isCannonCharged) {
              
              // x-position: center bullet on center of the ship
              double bulletX = playerShip.getLayoutX() + playerShip.getImage().getWidth() / 2.0 - playerBullet.getWidth() / 2.0;
              // y-position: let bullet come out on top of the ship
              double bulletY = playerShip.getLayoutY();

              // create sprite
              ImageView imgv = new ImageView( playerBullet);
              Sprite sprite = new Sprite( bulletLayer, imgv, bulletX, bulletY, 0, -cannonBulletSpeed);
              sprites.add( sprite);

              // left/right: vary x-axis position
              for( int i=0; i < cannonBullets / 2.0; i++) {

               // left
                  imgv = new ImageView( playerBullet);
                  sprite = new Sprite( bulletLayer, imgv, bulletX, bulletY, -cannonBulletSpread * i, -cannonBulletSpeed);
                  sprites.add( sprite);

               // right
                  imgv = new ImageView( playerBullet);
                  sprite = new Sprite( bulletLayer, imgv, bulletX, bulletY, cannonBulletSpread * i, -cannonBulletSpeed);
                  sprites.add( sprite);

              }
              
              // player bullet uncharged
              cannonChargeCounter = 0;
             }
             
             // check sprite visibility
             // remove every sprite that's not visible anymore
             // ---------------------------
             Iterator iter = sprites.iterator();
             while( iter.hasNext()) {
              
              Sprite sprite = iter.next();
              
              // check lower screen bounds
              if( sprite.getY() > SCENE_HEIGHT) {
               iter.remove();
               continue;
              }
              
              // check upper screen bounds
              if( (sprite.getY() + sprite.getImageView().getImage().getHeight()) < 0) {
               iter.remove();
               continue;
              }
              
              // check right screen bounds
              if( sprite.getX() > SCENE_WIDTH) {
               iter.remove();
               continue;
              }

              // check left screen bounds
              if( (sprite.getX() + sprite.getImageView().getImage().getWidth()) < 0) {
               iter.remove();
               continue;
              }

             }
             
             // move sprites
             // ---------------------------
             // move sprites internally
             for( Sprite sprite: sprites) {
              sprite.move();
             }
             
             // move sprites on screen
             for( Sprite sprite: sprites) {
              sprite.getImageView().relocate( sprite.getX(), sprite.getY());
             }
             
             // add random clouds
             // ---------------------------
             if( rnd.nextInt(100) == 0) {
              
              // determine random layer
              Pane layer;
              if( rnd.nextInt(2) == 0) {
               layer = lowerCloudLayer;
              } else {
               layer = upperCloudLayer;
              }

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

              // determine random image
              Image image = clouds[ rnd.nextInt( clouds.length)]; 
              
              // create sprite
     ImageView imgv = new ImageView( image);
     
     // 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( layer == upperCloudLayer) {
      imgv.setOpacity(0.5);
      speed +=1.0;
     }
     
     // create a sprite and position it horizontally so that half of it may be outside of the view, vertically so that it enters the view with the next movement
     Sprite sprite = new Sprite( layer, imgv, rnd.nextDouble() * SCENE_WIDTH - imgv.getImage().getWidth() / 2.0, -imgv.getImage().getHeight(), 0, speed);
     sprites.add( sprite);
     
             }
             
             
             // calculate fps
             // ---------------------------
             frameCount++;
             
             long currTime = System.currentTimeMillis();
             
             if( currTime - prevTime >= 1000) {
              
              // get current fps
              fpsCurrent = frameCount;

              // reset counter every second
              prevTime = currTime;
              frameCount = 0;
             }
             
             // show debug info
             // ---------------------------
             debugLabel.setText("FPS: " + fpsCurrent + "\nSprites: " + sprites.size());
            }
 
        };
        
        gameLoop.start();
        
 }
 
 public static void main(String[] args) {
  launch(args);
 }
}


Sprite.java
package game;

import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;

public class Sprite {

 Pane layer;
 ImageView imageView;
 double x;
 double y;
 double dx;
 double dy;
 double rotation;
 
 boolean alive = true;
 
 public Sprite( Pane layer, ImageView imageView, double x, double y, double dx, double dy) {
  
  this.layer = layer;
  this.imageView = imageView;
  this.x = x;
  this.y = y;
  this.dx = dx;
  this.dy = dy;
  
  layer.getChildren().add( imageView);
  imageView.relocate(x, y);
 }
 
 public Pane getLayer() {
  return layer;
 }

 public void setLayer(Pane layer) {
  this.layer = layer;
 }

 public ImageView getImageView() {
  return imageView;
 }
 public void setImageView(ImageView imageView) {
  this.imageView = imageView;
 }
 public double getX() {
  return x;
 }
 public void setX(double x) {
  this.x = x;
 }
 public double getY() {
  return y;
 }
 public void setY(double y) {
  this.y = y;
 }
 public double getDx() {
  return dx;
 }
 public void setDx(double dx) {
  this.dx = dx;
 }
 public double getDy() {
  return dy;
 }
 public void setDy(double dy) {
  this.dy = dy;
 }

 public void move() {
  
  x += dx;
  y += dy;
  
 }
 
}
ps: does anyone know why blogspot changes "Sprite" to "sprite" and adds tags like "</sprite></keyevent></keyevent></sprite></sprite> " to the code parts?