-2

I am trying to develop a Tower Defense Game using javafx and I am having trouble as to how to make it so that the enemies move around the screen. Which classes and methods should I be using in order to approach this problem?

El Fufu
  • 66
  • 1
  • 9
  • Take a look at one of the JavaFX game tutorials: http://smooth-java.blogspot.com/2013/12/tutorial-how-to-develop-simple-javafx.html – Buddy May 05 '15 at 05:30
  • Show us what you have already tried and what exactly are you not able to perform. – ItachiUchiha May 05 '15 at 05:34
  • Welcome to StackOverflow.. First [have a quick tour](http://stackoverflow.com/tour) and then [read how to ask a good question](http://stackoverflow.com/help/how-to-ask). Good luck. – Uluk Biy May 05 '15 at 05:34
  • Basically, what I have been able to do is to setup a background image. Also, I've been able to setup my enemies over that image, but they are static, they are not moving and that is my main issue, I don't know how to make the enemies move. I used the following classes on my code: stackpane, image and imageview – El Fufu May 05 '15 at 05:38
  • @ElFufu, see http://stackoverflow.com/a/29963104 as a hint. – Uluk Biy May 05 '15 at 05:43
  • Example code about [how to move player and enemies](http://stackoverflow.com/questions/29057870/in-javafx-how-do-i-move-a-sprite-across-the-screen/29058909#29058909). – Roland May 05 '15 at 05:49

1 Answers1

1

A tower defense game is too much to be covered on SO. I had a little bit of spare time and modified the engine I created in this thread.

Here's the main class with the game loop where the game is loaded, input is checked, sprites are moved, collision is checked, score is updated etc. In opposite to the other engine here you don't need keyboard input. Instead use a mouse click to position a tower. I added 4 initial towers.

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.image.Image;
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.stage.Stage;

public class Game extends Application {

    Random rnd = new Random();

    Pane playfieldLayer;
    Pane scoreLayer;

    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
        playfieldLayer = new Pane();
        scoreLayer = new Pane();

        root.getChildren().add( playfieldLayer);
        root.getChildren().add( scoreLayer);

        playfieldLayer.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
            createTower(e.getX(), e.getY());
        });

        scene = new Scene( root, Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT);

        primaryStage.setScene( scene);
        primaryStage.show();

        loadGame();

        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() {
        playerImage = new Image( getClass().getResource("player.png").toExternalForm());
        enemyImage = new Image( getClass().getResource("enemy.png").toExternalForm());
    }

    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( 800, 200));
        towerPositionList.add(new Point2D( 800, 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, 0, 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 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
        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);
    }

}

Then you need a base class for your sprites. You can use it for enemies and towers.

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

}

The towers are subclasses of the sprite base class. Here you need a little bit of math because you want the towers to rotate towards the enemies and let the towers fire when the enemy is within range.

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 = 300; // distance within tower can lock to enemy

    ColorAdjust colorAdjust;

    double rotationLimitDeg=0.0;
    double rotationLimitRad =  Math.toDegrees( this.rotationLimitDeg);
    double roatationEasing = 10;
    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.setDamage(Settings.TOWER_DAMAGE);

        init();
    }


    private void init() {

        // red colorization (simulate "angry")
        colorAdjust = new ColorAdjust();
        colorAdjust.setContrast(0.0);
        colorAdjust.setHue(-0.2);

    }

    @Override
    public void move() {

        SpriteBase follower = this;

        // reset within firing range
        withinFiringRange = false;

        // rotate towards target
        if( target != null)
        {
            // parts of code used from shane mccartney (http://lostinactionscript.com/page/3/)
            double xDist = target.getCenterX() - follower.getCenterX();
            double yDist = target.getCenterY() - follower.getCenterY();

            this.targetAngle = Math.atan2(yDist, xDist) - Math.PI / 2;

            this.currentAngle = Math.abs(this.currentAngle) > Math.PI * 2 ? (this.currentAngle < 0 ? (this.currentAngle % Math.PI * 2 + Math.PI * 2) : (this.currentAngle % Math.PI * 2)) : (this.currentAngle);
            this.targetAngle = this.targetAngle + (Math.abs(this.targetAngle - this.currentAngle) < Math.PI ? (0) : (this.targetAngle - this.currentAngle > 0 ? ((-Math.PI) * 2) : (Math.PI * 2)));
            this.currentAngle = this.currentAngle + (this.targetAngle - this.currentAngle) / roatationEasing;  // give easing when rotation comes closer to the target point

            // check if the rotation limit has to be kept
            if( (this.targetAngle-this.currentAngle) > this.rotationLimitRad) {
                this.currentAngle+=this.rotationLimitRad;
            } else if( (this.targetAngle-this.currentAngle) < -this.rotationLimitRad) {
                this.currentAngle-=this.rotationLimitRad;
            }

            follower.r = Math.toDegrees(currentAngle);

            // determine if the player ship is within firing range; currently if the player ship is within 10 degrees (-10..+10)
            withinFiringRange = Math.abs( Math.toDegrees( 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() {

        if( withinFiringRange) {
            imageView.setEffect(colorAdjust);
        } else {
            imageView.setEffect(null);
        }

        super.updateUI();

    }
}

The enemy class is easier. It needs only movement. However, in your final version the enemies should consider obstacles during movement. In this example I add a health bar above the enemy to show the health.

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

    }

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

The health bar

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

}

And then you need some global settings like this

public class Settings {

    public static double SCENE_WIDTH = 1024;
    public static double SCENE_HEIGHT = 768;

    public static double TOWER_DAMAGE = 1;

    public static double PLAYER_SHIP_SPEED = 4.0;
    public static double PLAYER_SHIP_HEALTH = 100.0;

    public static int ENEMY_HEALTH = 100;
    public static int ENEMY_SPAWN_RANDOMNESS = 50;

}

These are the images:

player.png

enter image description here

enemy.png

enter image description here

So summarized the gameplay is for now:

  • click on the screen to place a tower ( ie a smiley)
  • when an enemy is in range, the smiley becomes angry (i just change the color to red), in your final version the tower would be firing
  • as long as the tower is firing at the enemy, the health is reduced. i did it by changing the health bar depending on the health
  • when the health is depleted, the enemy is killed and the score is updated; you'd have to add an explosion

So all in all it's not so easy to create a tower defense game. Hope it helps as a start.

Heres' a screenshot:

enter image description here

Community
  • 1
  • 1
Roland
  • 18,114
  • 12
  • 62
  • 93
  • Thanks for the post, this is helping me get the basics of what I want. I have a question though, in your sprite class you have a "r" and "dr" value what are those supposed to mean? I'm assuming it is the radial difference, but I just want to be sure. Thank you again for your help. – El Fufu May 05 '15 at 23:44
  • Or does it have something to do with the rotation of the images? – El Fufu May 05 '15 at 23:50
  • r is absolute rotation, dr is the delta of the rotation, i. e. rotation per frame. Same applies to x and y vs dx and dy. When you move a node from 0/0 to 100/100, you can't move it in 1 step there. Instead you have to calculate a delta by which the current x/y position is increased in every animation step. Same goes for the roatation r and dr. – Roland May 06 '15 at 03:55
  • @El Fufu: I changed the enemy health indicator to a health bar. – Roland May 06 '15 at 04:24