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?