Sunday, January 18, 2015

Anansi: Bullets


Let's add some game to our game. We now have

  • a sprite manager
  • a mechanims to create moving sprites
  • a player ship
  • a keyboard event handler

With that it should be easy to spawn some bullets using the primary weapon key (space bar). Adding bullets is just more of the same.

We let the bullets fly on their own layer. We place it right below the playfield layer.

Pane bulletLayer = new Pane();
...
bulletLayer = new Pane();
...
root.getChildren().add( bulletLayer);
And then create a global image variable and load the image in our loadGame() method.
Image playerBullet;
...
playerBullet = new Image( getClass().getResource( "assets/bullets/bullet_01.png").toExternalForm());
The bullets fly at a given speed. Varying the speed gives us the opportunity to start at slower speeds and increase it when the player picks up some bonus from the playfield. We declare a global variable for the player bullet speed.
double cannonBulletSpeed = 8.0;
And all that's left is to make it fly upon keypress. We add the mechanism for that to the game loop. The speed is given as positive value. But since the bullets flies from bottom to top, we need it to be negative in order to decrease the y position.
// fire player bullets
// ---------------------------
if( isSpacePressed) {
 
 // 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);

}

And there we have it: We can shoot bullets from our ship.

This all happens in our game loop at 60 frames per second, which means a lot of bullets are getting spawned. It makes the shooting look more like a straight laser:


What we need now is some kind of charging delay.

Think in terms of the game loop. Something is happening 60 times a second. So we add a counter and depending on the status of that counter we allow the player to fire a bullet, even though he keeps on pressing the primary weapon fire key on the keyboard. We introduce a few variables for the cannon-charging mechanism.
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 
And we extend our cannon firing code.
// 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);

 // player bullet uncharged
 cannonChargeCounter = 0;
}
This gives us a nice cannon firing delay which looks like this:


I mentioned player bonus earlier. In addition to increasing the cannon firing speed we could also add more bullets. We don't want more bullets in the same firing line, so we add a bullet spread. Of course the spread should vary depending on the player's bonus level. We introduce new variables.
double cannonBullets = 9; // number of bullets which the cannon can fire in 1 shot (center, left, right)
double cannonBulletSpread = 0.6; // dx of left and right bullets
And extend our code so that the bullets spread left/center/right.
// 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);

}
This is how it looks like:



Very nice. Now we have a powerful cannon, we can expect lots of enemy targets. In the next lesson they come ;-)

Anansi: Environment


I still have a little bit of time left for today, so let's add something to the scenery: Clouds.

We add 2 layers:

  • lower layer is beetween the background and the player ship
  • upper layer is above the player ship

The upper layer would cover the player ship, but we want the player ship to be always visible. So we change the opacity of the images in the upper layer.

In my example I use 4 semi-transparent PNG files as cloud layer. Setting the position and opacity to random makes a bit more of diversity. We could also rotate the clouds, but we can do that always later.

Adding the images and layers is the usual process which we learned in the previous lessons.

The layers for now: 
Pane backgroundLayer;
Pane lowerCloudLayer;
Pane playfieldLayer;
Pane upperCloudLayer;
Pane debugLayer;
...
// create layers
backgroundLayer = new Pane();
lowerCloudLayer = 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( playfieldLayer);
root.getChildren().add( upperCloudLayer);
root.getChildren().add( debugLayer);

Load the clouds into an array of images. Create a reference
// list of available cloud images
Image clouds[];
And load the images in our loadGame() method:
// 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()); 

The clouds are supposed to be positioned randomly. For that use the Random class
Random rnd = new Random();
We use randomness to determine

  • if we should show a cloud at all
  • if we should put it on the upper or the lower layer
  • which cloud we should display
  • the movement speed of the clouds

And since each cloud should have their own movement logic, it's time we introduce a class for this. Let's call it "Sprite". Here's the code which we put into the AnimationTimer's handle method:
// 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( Sprite.Type.CLOUD, layer, imgv, rnd.nextDouble() * SCENE_WIDTH - imgv.getImage().getWidth() / 2.0, -imgv.getImage().getHeight(), 0, speed);
 sprites.add( sprite);

}
For now the Sprite class is a very simple class which also holds the layer information. Basically that's the parent of the node. We add the sprite to the layer in the constructor. That may change later as we code along.

The class has a method move(). In it we simply advance the Sprite from position x/y by dx/dy. Clouds are moving downwards, so their dx will be 0 and the dy will be some random positive value.
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;
  
 }
 
}
We need to hold the Sprites in a list so that we can process them all at once for
  • calculating their new position
  • movement in the view
  • removing them from the layer when they are outside of the view

We simply create a list for further references.
List sprites = new ArrayList();
In the AnimationTimer's handle code we iterate through all of the sprites and move them:
// 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());
}

We also need to remove the sprites once they're not visible anymore:
// 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;
 }

}
And we'd like to see the number of sprites on the screen, so we modify our debug information:
debugLabel.setText("FPS: " + fpsCurrent + "\nSprites: " + sprites.size());

Here's a screenshot about what we've achieved so far. Of course it looks better when it's animated.




I have to admit I'm really impressed with what JavaFX offers. Looking forward to adding enemies and heat seeking missiles!

That's it for today ... and keep on coding!

Anansi: Debug Information


One of the most important things during coding is to get some information during runtime.

We like to see the FPS (Frames Per Second) in order to see if our game is performing well.

We proceed like in the pervious lessons: Create a layer for that information and add a node to it.

 
Pane debugLayer;
...
Label debugLabel;
...
debugLayer = new Pane();
...
root.getChildren().add( debugLayer);
...
// debug information
debugLabel = new Label();
debugLabel.setTextFill(Color.RED);
debugLayer.getChildren().add( debugLabel);
...
In the AnimationTimer we calculate the FPS and write that information into the debugLabel. We create some global variables
 
// counter for game loop
int frameCount = 0;
int fpsCurrent = 0;
long prevTime = -1;
and add this code in the AnimationTimer's handle method:
 
// 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);

In our game we can see on the top left that it runs indeed with 60 FPS.

Anansi: Keyboard Controls


The keyboard controls will be the usual for that kind of games:

  • Cursor up = movement up
  • Cursor down = movement down
  • Cursor left = movement left
  • Cursor right = movement right
  • Space = primary weapon
  • Control = secondary weapon

The difficulty we are facing is that JavaFX has great event handlers which fire when you interact with them, i. e. on KeyPressed, KeyReleased, etc. but no means to check if a key is down at a given point in time.

My current solution is simply to register the KeyPressed and KeyReleased events in a BitSet. This way the keyboard code is pretty much generic regarding the event handlers. One problem we are facing is that we don't get the KeyCode as int out of the key event. We'll simply use the ordinal value of the KeyCode instead. We don't use impl_getCode of the KeyCode class, because the method is flagged to be removed.

Let's start coding!

Add controls for the scene:
   // keyboard control
   addInputControls( scene);

Registration for the key events:
 // 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();

Add the keyboard event handler:
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);
   
  }
 });
 
}


We create variables for the player ship movement
 double playerShipSpeed = 4.0;
 double playerShipDeltaX = 0.0;
 double playerShipDeltaY = 0.0;


Calculate ship movement bounds outside of the AnimationTimer, since we only need to do this once:
// 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;

In the AnimationTimer's handle method we evaluate the keyboard input and calculate the movement of the player ship:
// 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;
}
             

Then we calculate the new position of the player ship and move it. We allow the player ship to be only a little bit outside of the playfield.
// 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);

Now we have a playfield with a scrolling background and a player ship which we can control with the keyboard. In case anyone wants to get the whole code of this tutorial, please let me know.

And keep on coding!

Anansi: Player Ship


Now that we have a scrolling background, we can add the player's ship. The code is very similar to the background code.

Create the player ship:

ImageView playerShip;

Create a layer for the ship:

Pane playfieldLayer;

Add the playfield layer to the scene:


playfieldLayer = new Pane();
...   
root.getChildren().add( playfieldLayer);

Load the player ship and add it to the scene. We position it in the horizontally centered and at 70% vertically:

  // 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);


With the added player ship the game looks like this:






As with the background feel free to use any of your resources for the player ship.

Anansi: Scrolling Background


We create a vertically scrolling game. So we need to figure out how to add a scrolling background. 

First thing that comes to mind is to use an ImageView and position it in our game loop. Since we will be loading various data, we create a loadGame() method.

We will have various objects like 
  • background
  • clouds
  • player ship
  • enemy ships
  • bullets 
  • ...
moving around in our game, it's best to use layers for them. The background will be the bottom layer, everything else will be on top of it. 

We scroll the background by putting the background ImageView on the background layer and reposition it in the AnimationTimer.
 
 
package game;
 
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;


public class Main extends Application {
 
 private double SCENE_WIDTH = 400;
 private double SCENE_HEIGHT = 800;
 
 /**
  * Main game loop
  */
 private AnimationTimer gameLoop;

 /**
  * Container for the background image
  */
 ImageView backgroundImageView;
 
 /**
  * Scrolling speed of the background
  */
 double backgroundScrollSpeed = 0.5;
 
 /**
  * Layer for the background
  */
 Pane backgroundLayer;
 
 @Override
 public void start(Stage primaryStage) {
  try {

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

   // create layers
   backgroundLayer = new Pane();
   
   // add layers to scene root
   root.getChildren().add( backgroundLayer);
   
   // create scene
   Scene scene = new Scene( root, SCENE_WIDTH,SCENE_HEIGHT);
   
   // show stage
   primaryStage.setScene(scene);
   primaryStage.show();

   // load game assets
   loadGame();

   // start the game
   startGameLoop();
   
  } catch(Exception e) {
   e.printStackTrace();
  }
 }

 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);
  
 }
 
 private void startGameLoop() {

  // game loop
        gameLoop = new AnimationTimer() {
         
            @Override
            public void handle(long l) {
            
             // 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);

             
            }
 
        };
        
        gameLoop.start();
        
 }
 
 public static void main(String[] args) {
  launch(args);
 }
}
Now we have a smooth scrolling background which looks like this:


I'll upload my assets later. If you simply wish to check out the code, you can replace the file "assets/maps/canyon.jpg" by any of your images.

Anansi: The Game Loop


The game we create is a 2D scrolling sh'm'up. So we need a game loop with gives us events at a given frame rate. There are various solutions to this, but we want to keep it simple for now. So the current choice for the game loop is an AnimationTimer.

The game loop basically looks like this:

  
package game;
 
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;


public class Main extends Application {
 
 private double SCENE_WIDTH = 400;
 private double SCENE_HEIGHT = 800;
 
 private AnimationTimer timer;
 
 
 @Override
 public void start(Stage primaryStage) {
  try {

   // create root node
   Group root = new Group();
   
   // create scene
   Scene scene = new Scene( root, SCENE_WIDTH,SCENE_HEIGHT);
   
   // show stage
   primaryStage.setScene(scene);
   primaryStage.show();

   // start the game
   gameLoop();
   
  } catch(Exception e) {
   e.printStackTrace();
  }
 }

 private void gameLoop() {

  // game loop
        timer = new AnimationTimer() {
            @Override
            public void handle(long l) {
            
             // do something
             
            }
 
        };
        
        timer.start();
        
 }
 
 public static void main(String[] args) {
  launch(args);
 }
}

The AnimationTimer runs at 60 fps. Everything we do inside runs 60 times a second, so we need to be careful about the performance in that loop.