Sunday, January 18, 2015

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!

No comments:

Post a Comment