Tower Defense in JavaFX (2)
In the last part we’ve created a simple editor that let’s us place turrets. Now we’ll add a spawnpoint where the enemies originate, and define an attack target for them. First I’ll add some more information to the map via an Object Layer. That’s standar TMX, so we can do it in the TileMap Editor:
In order to calculate the attack path for our enemies, we’ll use the A* Algorithm, which is part of the tilengine module:
So let’s get the spawnpoint and target and store them for our algorithm:
ArrayList objectGroups = tileMap.getObjectGroups(); for (ObjectGroup objectGroup : objectGroups) { for (final TObject tObject : objectGroup.getObjectLIst()) { if (tObject.getName().equals("spawnpoint")) { spawnpointX = tObject.getX() / turrets.getTilewidth(); spawnpointY = tObject.getY() / turrets.getTileheight(); } if (tObject.getName().equals("target")) { targetX = tObject.getX() / turrets.getTilewidth(); targetY = tObject.getY() / turrets.getTileheight(); } } }
With these values we can initialize the A* algorithm that calculates the shortest path for our enemies:
AStar.AStarTile start = new AStar.AStarTile((int) spawnpointX, (int) spawnpointY); AStar.AStarTile end = new AStar.AStarTile((int) targetX, (int) targetY); attackPath = AStar.getPath(tileMap, platformLayer, start, end);
In order to see the result , we’ll add a debug layer to the GameCanvas:
private class AStarLayer extends Layer { public AStarLayer() { } Color pathColor = Color.rgb(255, 100, 100, .2); @Override public void draw(GraphicsContext graphicsContext, double x, double y, double width, double height) { AStar.PathNode start = attackPath; if (start != null) { graphicsContext.setFill(pathColor); graphicsContext.fillRect(start.getX() * tileMap.getTilewidth(), start.getY() * tileMap.getTileheight(), tileMap.getTilewidth(), tileMap.getTileheight()); while (start.getParent() != null) { start = start.getParent(); graphicsContext.fillRect(start.getX() * tileMap.getTilewidth(), start.getY() * tileMap.getTileheight(), tileMap.getTilewidth(), tileMap.getTileheight()); } } } }
The result looks like this:
You see the shortest path in red color. Since the algorithm doesn’t “see” the structures of the background image, it calculates the path accordingly, and the enemies would simply ignore the structures of the ship (the background is supposed to be a part of a spaceship). To fix this, we’ll add some invisible Tiles later. For larger games it’s better to use an invisible collision layer though, which gives you better performance and more ways to implement things like locked passages. For us the transparent-tile-approach is better, because we don’t need an extra layer, and it’s easier if the user can edit the layout.
Now we need to send the enemies down this path. In order to animate the Sprite I combined the animation phases into a single image:
Now we can use the Tiled editor to create a TileSet from it:
I also used Tiled to add two additional properties to the spawnpoint:
The first one defines how many enemies I want to spawn of each type, the second one defines the pause between their spawning. I doubt that they’ll stand the test of time unchanged, but for now let’s work with them. Inside the code for reading the ObjectGroups we can access the Properties:
if (tObject.getName().equals("spawnpoint")) { Properties properties = tObject.getProperties(); evaluationInterval = Long.parseLong(properties.getProperty("delay")); spawnpointX = tObject.getX() / turrets.getTilewidth(); spawnpointY = tObject.getY() / turrets.getTileheight(); }
For now we only have one type of monster, so we can ignore that and only use the delay. First we’ll create a SpriteAnimation from our TileSet:
final TileSet enemy1 = tileMap.getTileSet("enemy1"); final TileSetAnimation tileSetAnimation = new TileSetAnimation(enemy1, new int[]{0, 1, 2, 3, 4, 5}, 10f);
In order to Spawn a monster we’ll define a Behavior. That’s simply a timed method call. The API will probably be changed a bit here in order to support Lambda expressions:
Behavior monsterSpawnBehavior = new Behavior() { int enemyCount = 0; @Override public boolean perform(GameCanvas canvas, long nanos) { new Sprite(canvas, tileSetAnimation, "enemy" + (enemyCount++), ((int)spawnpointTileX) * tileMap.getTilewidth(), ((int)spawnpointTileY) * tileMap.getTileheight(), 46, 46, Lookup.EMPTY); return false; } }; monsterSpawnBehavior.setEvaluationInterval(evaluationInterval); canvas.addBehaviour(monsterSpawnBehavior);
So now every soandso nanoseconds a new Enemy will be added to the playfield. We’ll probably create an EnemySprite class later to encapsulate the Behavior. But for now let’s stick with this Sprite and add Behaviour to it:
sprite.addBehaviour(new SpriteBehavior() { AStar.PathNode start = attackPath; @Override public boolean perform(Sprite sprite) { double x = sprite.getX(); double y = sprite.getY(); double pathX = start.getX() * tileMap.getTilewidth(); double pathY = start.getY() * tileMap.getTileheight(); if (Math.abs(pathX- x) 1) { sprite.setVelocityX(.5); } else if (pathX- x < -1) { sprite.setVelocityX(-.5); } else { sprite.setVelocityX(0); } if (pathY - y > 1) { sprite.setVelocityY(.5); } else if (pathY - y < -1) { sprite.setVelocityY(-.5); } else { sprite.setVelocityY(0); } return true; } });
And here’s the result:
That’s it for now. As you can see, it’s pretty simple to add AI to the sprites via Behaviors and AStar comes pretty handy. In the next part we’ll take care that our enemies point in the right direction, and add some Behavior to the turrets.