Saturday, May 16, 2015

Simple Game Engine: Tower Defense in JavaFX

Someone asked on StackOverflow about how to move enemies on the screen in a tower defense game. I thought that it should be easy to do this with our game engine. And it was.

There's a lot of code we can reuse and 2 things that we basically needed to change:

  • the input is now via mouse
  • the towers should rotate towards their enemies and fire

The code in this blog is an extended version of the one I posted on StackOverflow. You can use whatever images you like. This is just a prototype, so I took a brief look at some sites and I found nice ones at lostgarden. These awesome images make our game look very nice:




Since we are just extending our previously created engine a bit, I'll only post the main things here and the full code at the end of this blog.

The easy part: Instead of the keyboard input we use a mouse listener with which we place an instance of our tower:
  // add event handler to create towers
  playfieldLayer.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
   createTower(e.getX(), e.getY());
  });
and we create the tower as we would do any other player
 private void createTower( double x, double y) {
  
  Image image = playerImage;
  
  // center image at position
  x -= image.getWidth() / 2;
  y -= image.getHeight() / 2;
  
  // create player
  Tower player = new Tower(playfieldLayer, image, x, y, 180, 0, 0, 0, Settings.PLAYER_SHIP_HEALTH, 0, Settings.PLAYER_SHIP_SPEED);
  
  // register player
  towers.add( player);
  
 }

There's nothing special to it. In your final game you'll probably want to place the towers on a pre-defined grid.

The more interesting part is to find the target for each tower and rotate towards it. The finding mechanism was already covered in the Anansi blog. A target can be found when it's within search range. Once it's within search range, we want to rotate the tower towards the target. And once the tower is rotated towards the target and the tower's guns can hit the target because it is within firing range, we want to fire at the target. Usually we do an animation (we already learned how to do that in a previous blog post) when firing, but I simply change the color to red in order to indicate some action of the guns. I'll leave the bullet part out for sake of simplicity.

So coming back to our code, we need to find a target that is within search range:
 
 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);   

   // check if enemy is within range
   if( Double.compare( distanceTotal, targetRange) > 0) {
    continue;
   }
   
   if (closestTarget == null) {
    
    closestTarget = target;
    closestDistance = distanceTotal;
    
   } else if (Double.compare(distanceTotal, closestDistance) < 0) {

    closestTarget = target;
    closestDistance = distanceTotal;
    
   }
  }

  setTarget(closestTarget);

 }

The movement of the towers is reduced to rotation. We don't want to instantly rotate to the target, instead we'd like to rotate smoothly to it, with some kind of speed limitation. The problem when coding this kind of things is that the angle isn't a full 360 degree circle, instead it's a range from -180 to 180. So we have to consider this. The common problem is that e. g. if the tower faces top/right and the enemy moves down, then once the angle becomes negative (i. e. -180 degrees), then the tower would normally rotate the long way, i. e. clockwise towards the target. So we have to consider this, we want the short way, not the long way. A post in LostInActionScript helped solve the problem.

 public void move() {
  
  SpriteBase follower = this;
  
  // reset within firing range
  withinFiringRange = false;
  
  // rotate towards target
  if( target != null)
  {

   // calculate rotation angle; follower must rotate towards the target
   // we need to consider the angle ranges from -180..180 by transforming the coordinates to a range of 0..360 and checking the values
   // the calculation is done in degrees
   
   double xDist = target.getCenterX() - follower.getCenterX();
   double yDist = target.getCenterY() - follower.getCenterY();
   
   double angleToTarget = Math.atan2(yDist, xDist) - Math.PI / 2; // -Math.PI / 2 because our sprite faces downwards
   
   double targetAngle = Math.toDegrees( angleToTarget);
   double currentAngle = follower.r;
   
   // check current angle range
   if( Math.abs(currentAngle) > 360) {
    if( currentAngle < 0) {
     currentAngle = currentAngle % 360 + 360;
    } else {
     currentAngle = currentAngle % 360;
    }
   }
   
   // calculate angle difference between follower and target
   double diff = targetAngle - currentAngle;
   
   // normalize the target angle
   if( Math.abs( diff) < 180) {
    // values within range => ok
   } else {
    if( diff > 0) {
     targetAngle -= 360;
    } else {
     targetAngle += 360;
    }
   }
   
   // get the angle between follower and target
   diff = targetAngle - currentAngle;
   
   // add the differnce angle to the current angle while considering easing when rotation comes closer to the target point
         currentAngle = currentAngle + diff / roatationEasing;  
   
         // apply rotation
   follower.r = currentAngle;
      
   // determine if the player ship is within firing range; currently if the player ship is within 10 degrees (-10..+10)
   withinFiringRange = Math.abs( this.targetAngle-this.currentAngle) < 20;

  } 
  
  super.move();
     
 }


Well, that's basically it. If you'd like to see it in action, here's a video:



This is looking very nice. However, for a proper tower defense game the enemies need to follow a given path. It'll be interesting to find out how to implement that. A frequently used algorithm for that is the A* algorithm. Well, but first we'll have to find out how the A* algorithm works. So this is to-be-continued in another blog post.

And here's the source code for the interested ones.

Game.java
 
package game.towerdefense;

import game.towerdefense.sprites.Enemy;
import game.towerdefense.sprites.SpriteBase;
import game.towerdefense.sprites.Tower;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextBoundsType;
import javafx.scene.transform.Scale;
import javafx.stage.Stage;

public class Game extends Application {

 Random rnd = new Random();
 
 Pane backgroundLayer;
 Pane playfieldLayer;
 Pane scoreLayer;
 
 Image backgroundImage;
 Image playerImage;
 Image enemyImage;
 
 List<Tower> towers = new ArrayList<>();;
 List<Enemy> enemies = new ArrayList<>();;
 
 Text scoreText = new Text();
 int score = 0;
 
 Scene scene;
 
 @Override
 public void start(Stage primaryStage) {
  
  Group root = new Group();
  
  // create layers
  backgroundLayer = new Pane();
  playfieldLayer = new Pane();
  scoreLayer = new Pane();
  
  root.getChildren().add( backgroundLayer);
  root.getChildren().add( playfieldLayer);
  root.getChildren().add( scoreLayer);
  
  // ensure the playfield size so that we can click on it
  playfieldLayer.setPrefSize( Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT);
  
  // add event handler to create towers
  playfieldLayer.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
   createTower(e.getX(), e.getY());
  });
  
  scene = new Scene( root, Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT);
  
  primaryStage.setScene( scene);
  
  // fullscreen
  primaryStage.setFullScreen( Settings.STAGE_FULLSCREEN);
  primaryStage.setFullScreenExitHint("");
  
  // scale by factor of 2 (in settings we have half-hd) to get proper dimensions in fullscreen (full-hd)
  if( primaryStage.isFullScreen()) {
   Scale scale = new Scale( Settings.STAGE_FULLSCREEN_SCALE, Settings.STAGE_FULLSCREEN_SCALE);
   scale.setPivotX(0);
   scale.setPivotY(0);
   scene.getRoot().getTransforms().setAll(scale);
  }
  
  primaryStage.show();
  loadGame();
  
  createBackgroundLayer();
  createPlayfieldLayer();
  createScoreLayer();
  createTowers();
  
  AnimationTimer gameLoop = new AnimationTimer() {

   @Override
   public void handle(long now) {
    
             // add random enemies
             spawnEnemies( true);

    // check if target is still valid
    towers.forEach( tower -> tower.checkTarget());

             // tower movement: find target
          for( Tower tower: towers) {
           tower.findTarget( enemies);
          }
          
    // movement
    towers.forEach(sprite -> sprite.move());
    enemies.forEach(sprite -> sprite.move());
    
    // check collisions
    checkCollisions();
    
    // update sprites in scene
    towers.forEach(sprite -> sprite.updateUI());
    enemies.forEach(sprite -> sprite.updateUI());
    
             // check if sprite can be removed
    enemies.forEach(sprite -> sprite.checkRemovability());
             
             // remove removables from list, layer, etc
             removeSprites( enemies);

             // update score, health, etc
             updateScore();
   }
   
  };
  gameLoop.start();
  
 }
 
 private void loadGame() {
  backgroundImage = new Image( getClass().getResource("images/background.png").toExternalForm());
  playerImage = new Image( getClass().getResource("images/tower.png").toExternalForm());
  enemyImage = new Image( getClass().getResource("images/ship.png").toExternalForm());
 }

 private void createBackgroundLayer() {
  ImageView background = new ImageView( backgroundImage);
  backgroundLayer.getChildren().add( background);
 }


 private void createPlayfieldLayer() {

  // shadow effect to show depth
  // setting it on the entire group/layer preserves the shadow angle even if the node son the layer are rotated
  DropShadow dropShadow = new DropShadow();
  dropShadow.setRadius(5.0);
  dropShadow.setOffsetX(10.0);
  dropShadow.setOffsetY(10.0);
  
  playfieldLayer.setEffect(dropShadow);
  
 }
 
 private void createScoreLayer() {
  

  scoreText.setFont( Font.font( null, FontWeight.BOLD, 48));
  scoreText.setStroke(Color.BLACK);
  scoreText.setFill(Color.RED);
  
  scoreLayer.getChildren().add( scoreText);

  scoreText.setText( String.valueOf( score));
  
  double x = (Settings.SCENE_WIDTH - scoreText.getBoundsInLocal().getWidth()) / 2;
  double y = 0;
  scoreText.relocate(x, y);
  
  scoreText.setBoundsType(TextBoundsType.VISUAL);


 }
 private void createTowers() {
  
  // position initial towers
  List<Point2D> towerPositionList = new ArrayList<>();
//  towerPositionList.add(new Point2D( 100, 200));
//  towerPositionList.add(new Point2D( 100, 400));
//  towerPositionList.add(new Point2D( 1160, 200));
//  towerPositionList.add(new Point2D( 1160, 600));
  
  for( Point2D pos: towerPositionList) {

   createTower( pos.getX(), pos.getY());

  }
   
 }
 
 private void createTower( double x, double y) {
  
  Image image = playerImage;
  
  // center image at position
  x -= image.getWidth() / 2;
  y -= image.getHeight() / 2;
  
  // create player
  Tower player = new Tower(playfieldLayer, image, x, y, 180, 0, 0, 0, Settings.PLAYER_SHIP_HEALTH, 0, Settings.PLAYER_SHIP_SPEED);
  
  // register player
  towers.add( player);
  
 }
 
 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 + 2.0;
  
  // x position range: enemy is always fully inside the trench, 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 trenchMinX; // left pixel pos of trench
  double trenchMaxX; // right pixel pos of trench
  
  // 2 waves: 0 = left, 1 = right
  if( rnd.nextInt(2) == 0) {
   trenchMinX = 220; // left pixel pos
   trenchMaxX = 530; // right pixel pos
  } else {
   trenchMinX = 760; // left pixel pos
   trenchMaxX = 1050; // right pixel pos
  }
  
  
  double x = trenchMinX + rnd.nextDouble() * (trenchMaxX - trenchMinX - 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
  enemies.add( enemy);
     
 }

 private void removeSprites(  List<? extends SpriteBase> spriteList) {
  Iterator<? extends SpriteBase> iter = spriteList.iterator();
     while( iter.hasNext()) {
      SpriteBase sprite = iter.next();
      
      if( sprite.isRemovable()) {
       
       // remove from layer
       sprite.removeFromLayer();
       
       // remove from list
       iter.remove();
      }
     }
 }
 
 private void checkCollisions() {
  
  for( Tower tower: towers) {
   for( Enemy enemy: enemies) {
    if( tower.hitsTarget( enemy)) {
     
     enemy.getDamagedBy( tower);
     
     // TODO: explosion
     if( !enemy.isAlive()) {
      
      enemy.setRemovable(true);
      
      // increase score
      score++;
      
     }
    }
   }
  }
 }
 
 private void updateScore() {
  scoreText.setText( String.valueOf( score));
 }
 
 public static void main(String[] args) {
  launch(args);
 }
  
}


Settings.java
 
package game.towerdefense;


public class Settings {
 
 // fullscreen or windowed mode
 public static boolean STAGE_FULLSCREEN = false;
 
 // scale by factor of 2 (in settings we have half-hd) to get proper dimensions in fullscreen (full-hd)
 public static double STAGE_FULLSCREEN_SCALE = 2;

 public static double SCENE_WIDTH = 1280;
 public static double SCENE_HEIGHT = 720;

 public static double TOWER_DAMAGE = 1;
 
 // distance within which a tower can lock on to an enemy
 public static double TOWER_RANGE = 400; 
 
 public static double PLAYER_SHIP_SPEED = 4.0;
 public static double PLAYER_SHIP_HEALTH = 100.0;

 public static int ENEMY_HEALTH = 200;
 public static int ENEMY_SPAWN_RANDOMNESS = 30;
 
}


HealthBar.java
 
package game.towerdefense.ui;

import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;

public class HealthBar extends Pane {
 
 Rectangle outerHealthRect;
 Rectangle innerHealthRect;
 
 public HealthBar() {
  
  double height = 10;
  
  double outerWidth = 60;
  double innerWidth = 40;
  
  double x=0.0;
  double y=0.0;
  
  outerHealthRect = new Rectangle( x, y, outerWidth, height);
  outerHealthRect.setStroke(Color.BLACK);
  outerHealthRect.setStrokeWidth(2);
  outerHealthRect.setStrokeType( StrokeType.OUTSIDE);
  outerHealthRect.setFill(Color.RED);

  innerHealthRect = new Rectangle( x, y, innerWidth, height);
  innerHealthRect.setStrokeType( StrokeType.OUTSIDE);
  innerHealthRect.setFill(Color.LIMEGREEN);

  getChildren().addAll( outerHealthRect, innerHealthRect);

 }
 
 public void setValue( double value) {
  innerHealthRect.setWidth( outerHealthRect.getWidth() * value);
 }
 
}


Tower.java
 
package game.towerdefense.sprites;

import game.towerdefense.Settings;

import java.util.List;

import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.Image;
import javafx.scene.layout.Pane;

public class Tower extends SpriteBase {

 SpriteBase target; // TODO: use weakreference

 double turnRate = 0.6;
 
 double speed;
 
 double targetRange = Settings.TOWER_RANGE; // distance within which a tower can lock on to an enemy
 
 ColorAdjust colorAdjust;
 
 double rotationLimitDeg=0;
 double rotationLimitRad =  Math.toDegrees( this.rotationLimitDeg);
 double roatationEasing = 10; // the higher the value, the slower the rotation
 double targetAngle = 0;
 double currentAngle = 0;
 
 boolean withinFiringRange = false;
 
 public Tower(Pane layer, Image image, double x, double y, double r, double dx, double dy, double dr, double health, double damage, double speed) {

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

  this.speed = speed;

  this.currentAngle = Math.toRadians(r);
  
  this.setDamage(Settings.TOWER_DAMAGE);
  
  init();
 }

 
 private void init() {
  
  // red colorization
  colorAdjust = new ColorAdjust();
  colorAdjust.setContrast(0.0);
  colorAdjust.setHue(0.8);
  
 }

 @Override
 public void move() {
  
  SpriteBase follower = this;
  
  // reset within firing range
  withinFiringRange = false;
  
  // rotate towards target
  if( target != null)
  {

   // calculate rotation angle; follower must rotate towards the target
   // we need to consider the angle ranges from -180..180 by transforming the coordinates to a range of 0..360 and checking the values
   // the calculation is done in degrees
   
   double xDist = target.getCenterX() - follower.getCenterX();
   double yDist = target.getCenterY() - follower.getCenterY();
   
   double angleToTarget = Math.atan2(yDist, xDist) - Math.PI / 2; // -Math.PI / 2 because our sprite faces downwards
   
   double targetAngle = Math.toDegrees( angleToTarget);
   double currentAngle = follower.r;
   
   // check current angle range
   if( Math.abs(currentAngle) > 360) {
    if( currentAngle < 0) {
     currentAngle = currentAngle % 360 + 360;
    } else {
     currentAngle = currentAngle % 360;
    }
   }
   
   // calculate angle difference between follower and target
   double diff = targetAngle - currentAngle;
   
   // normalize the target angle
   if( Math.abs( diff) < 180) {
    // values within range => ok
   } else {
    if( diff > 0) {
     targetAngle -= 360;
    } else {
     targetAngle += 360;
    }
   }
   
   // get the angle between follower and target
   diff = targetAngle - currentAngle;
   
   // add the differnce angle to the current angle while considering easing when rotation comes closer to the target point
         currentAngle = currentAngle + diff / roatationEasing;  
   
         // apply rotation
   follower.r = currentAngle;
      
   // determine if the player ship is within firing range; currently if the player ship is within 10 degrees (-10..+10)
   withinFiringRange = Math.abs( this.targetAngle-this.currentAngle) < 20;

  } 
  
  super.move();
     
 }

 public void checkTarget() {
  
  if( target == null) {
   return;
  }
  
  
  if( !target.isAlive() || target.isRemovable()) {
   setTarget( null);
   return;
  }
  
  //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( Double.compare( distanceTotal, targetRange) > 0) {
   setTarget( null);
  }
  
 }

 public void findTarget( List<? extends SpriteBase> 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);   

   // check if enemy is within range
   if( Double.compare( distanceTotal, targetRange) > 0) {
    continue;
   }
   
   if (closestTarget == null) {
    
    closestTarget = target;
    closestDistance = distanceTotal;
    
   } else if (Double.compare(distanceTotal, closestDistance) < 0) {

    closestTarget = target;
    closestDistance = distanceTotal;
    
   }
  }

  setTarget(closestTarget);

 }
 
 public SpriteBase getTarget() {
  return target;
 }
 
 public void setTarget(SpriteBase target) {
  this.target = target;
 }

 

 @Override
 public void checkRemovability() {
  
  if( Double.compare( health, 0) < 0) {
   setTarget(null);
   setRemovable(true);
  }
  
 }
 
 public boolean hitsTarget( SpriteBase enemy) {
  
  return target == enemy && withinFiringRange;
  
 }
 
 public void updateUI() {
  
  // change effect (color/shadow) depending on whether we're firing or not
  if( withinFiringRange) {
   imageView.setEffect(colorAdjust);
  } else {
   imageView.setEffect(null);
  }
  
  super.updateUI();

 }
}



Enemy.java
 
package game.towerdefense.sprites;

import game.towerdefense.Settings;
import game.towerdefense.ui.HealthBar;
import javafx.scene.image.Image;
import javafx.scene.layout.Pane;

public class Enemy extends SpriteBase {

 HealthBar healthBar;
 
 double healthMax;

 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, health, damage);
  
  healthMax = Settings.ENEMY_HEALTH;
  
  setHealth(healthMax);
  
  init();
 }
 
 private void init() {
 }
 
 @Override
 public void checkRemovability() {

  if( Double.compare( getY(), Settings.SCENE_HEIGHT) > 0) {
   setRemovable(true);
  }
  
 }
 
 public void addToLayer() {
  
  super.addToLayer();
  
  // create health bar; has to be created here because addToLayer is called in super constructor
  // and it wouldn't exist yet if we'd create it as class member
  healthBar = new HealthBar();
   
  this.layer.getChildren().add(this.healthBar);
  
 }

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

 /**
  * Health as a value from 0 to 1.
  * @return
  */
 public double getRelativeHealth() {
  return getHealth() / healthMax;
 }

 
 public void updateUI() {

  super.updateUI();
  
  // update health bar
  healthBar.setValue( getRelativeHealth());
  
  // locate healthbar above enemy, centered horizontally
  healthBar.relocate(x + (imageView.getBoundsInLocal().getWidth() - healthBar.getBoundsInLocal().getWidth()) / 2, y - healthBar.getBoundsInLocal().getHeight() - 4);  
 }
}

SpriteBase.java
 
package game.towerdefense.sprites;

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

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;

 boolean canMove = true;
 
 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(); // imageView.getBoundsInParent().getWidth();
  this.h = image.getHeight(); // imageView.getBoundsInParent().getHeight();

  addToLayer();

 }

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

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

 public Pane getLayer() {
  return layer;
 }

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

 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 getR() {
  return r;
 }

 public void setR(double r) {
  this.r = r;
 }

 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 double getDr() {
  return dr;
 }

 public void setDr(double dr) {
  this.dr = dr;
 }

 public double getHealth() {
  return health;
 }

 public double getDamage() {
  return damage;
 }

 public void setDamage(double damage) {
  this.damage = damage;
 }

 public void setHealth(double health) {
  this.health = health;
 }

 public boolean isRemovable() {
  return removable;
 }

 public void setRemovable(boolean removable) {
  this.removable = removable;
 }

 public void move() {

  if( !canMove)
   return;
  
  x += dx;
  y += dy;
  r += dr;

 }

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

 public ImageView getView() {
  return imageView;
 }

 public void updateUI() {

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

 }

 public double getWidth() {
  return w;
 }

 public double getHeight() {
  return h;
 }

 public double getCenterX() {
  return x + w * 0.5;
 }

 public double getCenterY() {
  return y + h * 0.5;
 }

 // TODO: per-pixel-collision
 public boolean collidesWith( SpriteBase otherSprite) {
  
  return ( otherSprite.x + otherSprite.w >= x && otherSprite.y + otherSprite.h >= y && otherSprite.x <= x + w && otherSprite.y <= y + h);
  
 }
 
 /**
  * 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);
 }
 
 /**
  * Set flag that the sprite can't move anymore.
  */
 public void stopMovement() {
  this.canMove = false;
 }

 public abstract void checkRemovability();

}
And you may want the sprites for testing. Here they are, but you can use whatever you prefer. Just put them into the ./game/towerdefense/images folder of your project.

tower.png


ship.png


background.png

2 comments:

  1. Hi RolandC ,

    I have issues with testing the above software ...It's a javaFX application but how many packages are they ? There are 6 different java Classes , are they all in the same package ? And is there another package for the images ?

    Greetings

    ReplyDelete
  2. For simplicity I uploaded a more recent code to github. You can get the project from here:

    https://github.com/Roland09/TowerDefenseTest

    Just create a new JavaFX project and copy/paste the source in there.

    I don't have time currently to continue development. I'd like to combine the AStar code and the Particle code with the Tower Defense Code. I'd be happy to see the results if anyone got the time to do it.

    Have fun & keep on coding :-)

    ReplyDelete