Sunday, July 26, 2015

Particle Systems

Reading the Particle Systems chapter in the excellent book The Nature of Code by Daniel Shiffman I had to implement a system to toy around. It's in fact very easy to come up with something like this:



Here's a summary of what's necessary to create a particle system in JavaFX.

Shiffman's particle classes consist of properties for

  • acceleration vector
  • velocity vector
  • location vector

The acceleration is accumulated depending on the various forces in the system, then it is applied to the velocity and the velocity is applied to the location. Here's a short snippet:
 Vector2D location;
 Vector2D velocity;
 Vector2D acceleration;
 ...

  /**
   * Accumulate forces. Here we could also consider mass.
   */ 
 public void applyForce(Vector2D force) {

  acceleration.add(force);
      
 }

 /**
  * Standard movement method: calculate valocity depending on 
  * accumulated acceleration force, then calculate the location.
  * Reset acceleration so that it can be recalculated in the 
  * next animation step.
  */
 public void move() {
  
  // set velocity depending on acceleration
  velocity.add(acceleration);
     
  // limit velocity to max speed
  velocity.limit(maxSpeed);
     
  // change location depending on velocity
  location.add(velocity);

  // angle: towards velocity (ie target)
  angle = velocity.angle();
     
  // clear acceleration
  acceleration.multiply(0);
  
 }
 
 /**
  * Update node position
  */
 public void display() {
  
  // location
  relocate(location.x - centerX, location.y - centerY);
  
  // rotation
  setRotate(Math.toDegrees( angle));

 } 
Too easy to be true? Well, it is. The Vector2D class is a custom implementation. You could in theory use Point2D of JavaFX, but that class always returns a new Point2D class whereas in the custom implementation you can modify the x and y coordinates instead of spawning a new object which of course would cost performance, especially with thousands of particles on the screen.

Now we need an AnimationTimer in which we invoke these methods. Here's a snippet of how it could look like:
  List<Particle> allParticles = new ArrayList<>();
    
  ...
    
  Vector2D forceGravity = new Vector2D( 0, 0.1);
    
  AnimationTimer animationLoop = new AnimationTimer() {
   
   @Override
   public void handle(long now) {

    // add a new particle each frame
    addParticle();

    // apply force: gravity
    allParticles.forEach(sprite -> {
     sprite.applyForce(forceGravity);
    });

    // move sprite: apply acceleration, calculate velocity and location
    allParticles.stream().parallel().forEach(Sprite::move);

    // change particle node location on screen
    allParticles.stream().parallel().forEach(Sprite::display);
    
    // life span of particle
    allParticles.stream().parallel().forEach(Sprite::decreaseLifeSpan);

    // remove all particles that aren't visible anymore
    removeDeadParticles();

   }
  };

  animationLoop.start();
In this version it is assumed that the particles are all of type Node (e. g. a Circle, Rectangle, ImageView, etc). That was actually the first version I created.

However, when I implemented this, I noticed that the limit on my system is around 2.000 particles per frame. That number seemed a little bit low. So I toyed around and adapted the display mechanism in order to paint the particles on a Canvas instead of a node. In other words: The way you usually do this kind of stuff.

It was just a few lines of code changes. First the images and their colors are precalculated. For a particle lifespan of 256 this of course results in 256 precalculated images. Then the image is picked depending on the particle's lifespan and drawn on the canvas:
 // draw all particles on canvas
 // -----------------------------------------
 graphicsContext.setFill(Color.BLACK);
 graphicsContext.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());

 double particleSizeHalf = Settings.get().getParticleWidth() / 2;
 allParticles.stream().forEach(particle -> {

  Image img = images[particle.getLifeSpan()];
  graphicsContext.drawImage(img, particle.getLocation().x - particleSizeHalf, particle.getLocation().y - particleSizeHalf);

 });
Those few lines of code changes gave it a boost of a factor of 10 - 15. By using this technique, keeping the particle code as it is and invoking the canvas painting instead of the display method the system currently runs at 28.000 particles per frame with 60 fps in full-hd resolution. It's actually a lot of fun to toy around.

Here's a demonstration video:



In the video you can see different forces like attractor, repeller and gravity working together. You can change parameters via user interface. New repellers and attractors can be added via context menu.

Interested ones can get the code from this gist.

Have fun & keep on coding!

Tuesday, July 7, 2015

Sprite dynamics using Vector calculation


I recently stumbled upon the excellent book The Nature of Code by Daniel Shiffman. It's one of the best books I've read so far when it comes starting with learing about motion and physics in games. Not only do you get a very good written lecture, but Daniel also made a video series to supplement the book. And it can be read on the web for free. That book was an instant buy.

The code is written in Processing, but that can be easily translated to JavaFX.

I leave the details to the book and only describe here how the code is translated to JavaFX. One thing to notice is that the motion and physics are based on vector calculations. The JavaFX Point2D class provides us the means to work with, hower in the end it won't suffice, it does for now. The main reason is that the Point2D class can't be modified, you'll always get new instances of it when you add, subtract, multiply, etc. That's not very performing. As a replacement you could write your own vector class or use the PVector class from Processing, since it's written in Java (please note the license of Processing).

The code example is from chapter 2.10 Everything Attracts Everything. What we need is a sprite, i. e. a Mover class with attributes like

  • location
  • velocity
  • acceleration
  • mass

For sake of simplicity the Mover is a Circle. From the book we learn that the movement consists of 3 steps:

  • apply force(s) and get the acceleration
  • apply acceleration to velocity
  • apply velocity to location

This is basically the required code, what it does mathematically is very well explained in the book:

Point2D location;
Point2D velocity;
Point2D acceleration;
...

public void applyForce(Point2D force) {

  Point2D f = new Point2D( force.getX(), force.getY());
  f = f.multiply(1/mass);
  
  acceleration = acceleration.add(f);
}

public void move() {

  // set velocity depending on acceleration
  velocity = velocity.add(acceleration);

  // limit velocity to max speed
  double mag = velocity.magnitude();
  if( mag > Settings.MOVER_MAX_SPEED) {
   velocity = velocity.normalize();
   velocity = velocity.multiply(mag);
  }

  // change location depending on velocity
  location = location.add(velocity);

  // clear acceleration
  acceleration = new Point2D(0,0);
}

public Point2D attract(Mover m) {

  // force direction
  Point2D force = location.subtract(m.location);
  double distance = force.magnitude();
  
  // constrain movement
  distance = constrain(distance, Settings.ATTRACTION_DISTANCE_MIN, Settings.ATTRACTION_DISTANCE_MAX);
  
  force = force.normalize();

  // force magnitude
  double strength = (Settings.GRAVITATIONAL_CONSTANT * mass * m.mass) / (distance * distance);
  force = force.multiply(strength);

  return force;
}

public void display() {
  setCenterX( location.getX());
  setCenterY( location.getY());
}

And then we create a game loop in which we apply the forces, move the sprites and update them in the UI:

gameLoop = new AnimationTimer() {

 @Override
 public void handle(long now) {

  // force: attraction
  for (Mover m1 : allMovers) {
   for (Mover m2 : allMovers) {

    if (m1 == m2)
     continue;

    // calculate attraction
    Point2D force = m1.attract(m2);

    // apply attraction
    m2.applyForce(force);

   };
  };

  // move
  allMovers.forEach(Mover::move);

  // update in fx scene
  allMovers.forEach(Mover::display);

 }
};

gameLoop.start();


That's it, that's the core, nothing much to it. You can as easily apply other forces like wind and gravity, as described in the book.

Just in case your pc isn't powerful enough to handle this many nodes, or just for the sake of quick performance improvement, you may want to use a parallel stream() in order to apply the force like this:

    allMovers.stream().parallel().forEach(m1 -> {
     allMovers.stream().parallel().forEach(m2 -> {

      if (m1 != m2) {

       // calculate attraction
       Point3D force = m1.attract(m2);
 
       // apply attraction
       m2.applyForce(force);

      }
     });
    });


Here's a video about how it looks like. Random movers are placed on the screen, everything attracts everything:


Interested ones can get the full JavaFX source from this gist.

Sunday, May 17, 2015

A* Algorithm (or A Star Algorithm) in JavaFX

In a previous blog we created a simple tower defense engine. It's all working nice, but we want the enemies to enter at a given start point and move to an end point. It would all be easy if the path is a given and hence can't be changed.

But as the enemy moves we want to create towers in their way and hence we could block their path. When that happens the enemies will have to find a new path.

One of the commonly used path finding algorithms is the A* Algorithm (aka A Star Algorithm). It's very good described on Wikipedia. The implementation in JavaFX is straightforward, you simply have to follow the pseudo code on Wikipedia using standard Java means.

I'll post the code here, including the pseudo code and how it got translated. Here's what we'll achieve:



What we need first are cells that we can access and in which we can store the variables

  • g = distance from start to current cell
  • h = distance from current cell to goal
  • f = g + h

We use a simple Euclidian distance calculation. You can choose whatever you prefer and whatever your game needs, e. g. distance calculations considering hills and slopes.

The algorithm is implemented as a common algorithm. So you have to map the cells of your grid to the grid algorithm and vice versa when the path is found. To facilitate the mapping, you can store your actual grid cell in the A* algorithm's cell.

And then there's the attribute which tells us if a cell is traversable (i. e. a path candidate) or not.

So the code for a cell could look like this.

 
package application.astar;

/**
 * A virtual cell which defines if it is traversable. The cell is used to store the f,g,h values of the A* algorithm.
 * 
 * @param <T> You can use {@link #obj} to store information about e. g. an external object.
 */
public class AStarCell<T> implements Cloneable {
 
 int col;
 int row;
 boolean isTraversable;
 
 /**
  * A pointer to an object of your choice. Unused in the A* algorithm.
  * Usually you convert your grid to the (virtual) A* grid, then find the path.
  * Afterwards you'd want to find out which of your cells are on the path.
  */
 T obj;
 
 double g;
 double f;
 double h;
 
 AStarCell<T> cameFrom;
 
 public AStarCell( int col, int row, boolean isPath, T obj) {
  this.col=col;
  this.row=row;
  this.isTraversable = isPath;
  this.obj = obj;
 }
 
 public T getObject() {
  return obj;
 }
 
 public double getF() {
  return f;
 }
 
 public double getG() {
  return g;
 }

 public double getH() {
  return h;
 }

 /**
  * Cloning only used in order to show the steps of the A* algorithm.
  */
 public AStarCell<T> clone() {
  
  AStarCell<T> clonedCell = new AStarCell<T>( col, row, isTraversable, obj);
  clonedCell.f =f;
  clonedCell.g = g;
  clonedCell.h = h;
  
  if( cameFrom != null) {
   clonedCell.cameFrom = cameFrom.clone();
  }
  
  return clonedCell;
  
 }
}

Then we need to put the cells into a grid. The grid also knows about the neighbors of the cells, so we implement the algorithm to find the neighbors in there. It's just a matter of checking 8 coordinates.

 
package application.astar;

/**
 * Virtual grid for the A* algorithm. Used to determine the neighbors of a cell.
 */
public class AStarGrid<T> {
 
 AStarCell<T>[][] gridCells;
 int cols;
 int rows;
 
 public AStarGrid( int cols, int rows) {
  this.cols = cols;
  this.rows = rows;
  gridCells = new AStarCell[rows][cols];
 }
 
 public void setCell( T cell, int col, int row, boolean path) {
  gridCells[row][col] =  new AStarCell<T>(col,row, path, cell);
 }
 
 public AStarCell<T> getCell( int col, int row) {
  return gridCells[row][col];
 }
 
 /**
  * Get neighboring cells relative to the given cell. By default they are top/right/bottom/left. 
  * If allowDiagonals is enabled, then also top-left, top-right, bottom-left, bottom-right cells are in the results.
  * @param cell
  * @param allowDiagonals
  * @return
  */
 public AStarCell<T>[] getNeighbors(AStarCell<T> cell, boolean allowDiagonals) {
  
  AStarCell<T>[] neighbors = new AStarCell[ allowDiagonals ? 8 : 4];

  int currentColumn = cell.col;
  int currentRow = cell.row;

  int neighborColumn;
  int neighborRow;
  
  // top
  neighborColumn = currentColumn;
  neighborRow = currentRow - 1;
  
  if (neighborRow >= 0) {
   if( gridCells[neighborRow][neighborColumn].isTraversable) {
    neighbors[0] = gridCells[neighborRow][neighborColumn];
   }
  }

  // bottom
  neighborColumn = currentColumn;
  neighborRow = currentRow + 1;
  
  if (neighborRow < rows) {
   if( gridCells[neighborRow][neighborColumn].isTraversable) {
    neighbors[1] = gridCells[neighborRow][neighborColumn];
   }
  }

  // left
  neighborColumn = currentColumn - 1;
  neighborRow = currentRow;
  
  if ( neighborColumn >= 0) {
   if( gridCells[neighborRow][neighborColumn].isTraversable) {
    neighbors[2] = gridCells[neighborRow][neighborColumn];
   }
  }

  // right
  neighborColumn = currentColumn + 1;
  neighborRow = currentRow;
  
  if ( neighborColumn < cols) {
   if( gridCells[neighborRow][neighborColumn].isTraversable) {
    neighbors[3] = gridCells[neighborRow][neighborColumn];
   }
  }

  if (allowDiagonals) {
   
   // top/left
   neighborColumn = currentColumn - 1;
   neighborRow = currentRow - 1;
   
   if (neighborRow >= 0 && neighborColumn >= 0) {
    if( gridCells[neighborRow][neighborColumn].isTraversable) {
     neighbors[4] = gridCells[neighborRow][neighborColumn];
    }
   }

   // bottom/right
   neighborColumn = currentColumn + 1;
   neighborRow = currentRow + 1;
   
   if (neighborRow < rows && neighborColumn < cols) {
    if( gridCells[neighborRow][neighborColumn].isTraversable) {
     neighbors[5] = gridCells[neighborRow][neighborColumn];
    }
   }

   // top/right
   neighborColumn = currentColumn + 1;
   neighborRow = currentRow - 1;
   
   if (neighborRow >= 0 && neighborColumn < cols) {
    if( gridCells[neighborRow][neighborColumn].isTraversable) {
     neighbors[6] = gridCells[neighborRow][neighborColumn];
    }
   }

   // bottom/left
   neighborColumn = currentColumn - 1;
   neighborRow = currentRow + 1;
   
   if (neighborRow < rows && neighborColumn >= 0) {
    if( gridCells[neighborRow][neighborColumn].isTraversable) {
     neighbors[7] = gridCells[neighborRow][neighborColumn];
    }
   }

  }

  
  return neighbors;
 }

}

Now we got the elements to implement the algorithm. It's really easy to do. Here's the code:
 
package application.astar;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Set;

/**
 * This is a 1:1 translation of the algorithm on wikipedia: http://en.wikipedia.org/wiki/A*_search_algorithm
 * 
  
   function A*(start,goal)
      closedset := the empty set    // The set of nodes already evaluated.
      openset := {start}    // The set of tentative nodes to be evaluated, initially containing the start node
      came_from := the empty map    // The map of navigated nodes.
   
      g_score[start] := 0    // Cost from start along best known path.
      // Estimated total cost from start to goal through y.
      f_score[start] := g_score[start] + heuristic_cost_estimate(start, goal)
   
      while openset is not empty
          current := the node in openset having the lowest f_score[] value
          if current = goal
              return reconstruct_path(came_from, goal)
   
          remove current from openset
          add current to closedset
          for each neighbor in neighbor_nodes(current)
              if neighbor in closedset
                  continue
              tentative_g_score := g_score[current] + dist_between(current,neighbor)
   
              if neighbor not in openset or tentative_g_score < g_score[neighbor] 
                  came_from[neighbor] := current
                  g_score[neighbor] := tentative_g_score
                  f_score[neighbor] := g_score[neighbor] + heuristic_cost_estimate(neighbor, goal)
                  if neighbor not in openset
                      add neighbor to openset
   
      return failure
   
   function reconstruct_path(came_from,current)
       total_path := [current]
       while current in came_from:
           current := came_from[current]
           total_path.append(current)
       return total_path
       
 */

@SuppressWarnings("rawtypes")
public class AStarAlgorithm {
 
 /**
  * Get the cell with the minimum f value.
  */
 public class CellComparator implements Comparator<AStarCell>
 {
     @Override
     public int compare(AStarCell a, AStarCell b)
     {
      return Double.compare(a.f, b.f);
     }
 }

 /**
  * Find a path from start to goal using the A* algorithm
  */
 @SuppressWarnings("unchecked")
 public List<AStarCell> getPath( AStarGrid grid, AStarCell start, AStarCell goal, boolean allowDiagonals) {

  AStarCell current = null;
  boolean containsNeighbor;

  int cellCount = grid.rows * grid.cols;
  
  // closedset := the empty set    // The set of nodes already evaluated.
  Set<AStarCell> closedSet = new HashSet<>( cellCount);
  
  // openset := {start}    // The set of tentative nodes to be evaluated, initially containing the start node
     PriorityQueue<AStarCell> openSet = new PriorityQueue<AStarCell>( cellCount, new CellComparator());
  openSet.add( start);
  
  // g_score[start] := 0    // Cost from start along best known path.
  start.g = 0d;
  
     // Estimated total cost from start to goal through y.
     // f_score[start] := g_score[start] + heuristic_cost_estimate(start, goal)
  start.f = start.g + heuristicCostEstimate(start, goal);
  
  
     // while openset is not empty
  while( !openSet.isEmpty()) {

   // current := the node in openset having the lowest f_score[] value
   // note: we have a priorityqueue => for performance reasons we also remove the item instead of removing it later (as suggested in the algorithm)
   // remove current from openset
   current = openSet.poll();
   
         // if current = goal
         //        return reconstruct_path(came_from, goal)
   if( current == goal) {
    return reconstructPath( goal);
   }
   
   // remove current from openset
   // already done in openSet.poll(), see above
    
   // add current to closedset
   closedSet.add( current);
   
   // for each neighbor in neighbor_nodes(current)
   for( AStarCell neighbor: grid.getNeighbors( current, allowDiagonals)) {
    
    if( neighbor == null) {
     continue;
    }
    
             // if neighbor in closedset
                //   continue
    if( closedSet.contains( neighbor)) {
     continue;
    }
    
    // tentative_g_score := g_score[current] + dist_between(current,neighbor)
    double tentativeScoreG = current.g + distBetween( current, neighbor);
    
    // if neighbor not in openset or tentative_g_score < g_score[neighbor]
    if( !(containsNeighbor=openSet.contains( neighbor)) || Double.compare(tentativeScoreG, neighbor.g) < 0) {
     
     // came_from[neighbor] := current
     neighbor.cameFrom = current;
    
     // g_score[neighbor] := tentative_g_score
     neighbor.g = tentativeScoreG;
     
     // f_score[neighbor] := g_score[neighbor] + heuristic_cost_estimate(neighbor, goal)
     neighbor.h = heuristicCostEstimate(neighbor, goal);
     neighbor.f = neighbor.g + neighbor.h;
     
                 // if neighbor not in openset
                    //   add neighbor to openset
     if( !containsNeighbor) {
      openSet.add( neighbor);
     }
    }
   }
   
  }
  
  // nothing found
  return new ArrayList<>();
 }
 
 /**
  * Create final path of the A* algorithm.
  * The path is from goal to start.
  */
 // function reconstruct_path(came_from,current)
 private List<AStarCell> reconstructPath( AStarCell current) {
  
  List<AStarCell> totalPath = new ArrayList<>(200); // arbitrary value, we'll most likely have more than 10 which is default for java
  
  // total_path := [current]
  totalPath.add( current);
    
     // while current in came_from:
        // current := came_from[current]
  while( (current = current.cameFrom) != null) {

      // total_path.append(current)
   totalPath.add( current);
         
  }
         
     // return total_path
  return totalPath;
 }
 
 /**
  * Distance between a given cell and its neighbor.
  * Used in the algorithm as distance calculation between the current cell and a neighbor. 
  * In our case we use the same distance which we use from the current cell to the goal.
  */
 private double distBetween(AStarCell current, AStarCell neighbor) {
  return heuristicCostEstimate( current, neighbor);
 }
 
 /**
  * Distance between two cells. We use the euclidian distance here. 
  * Used in the algorithm as distance calculation between a cell and the goal. 
  */
 private double heuristicCostEstimate(AStarCell from, AStarCell to) {
  
  return Math.sqrt((from.col-to.col)*(from.col-to.col) + (from.row - to.row)*(from.row-to.row));
  
 }
 
}

Since that was very easy, I got some spare time left to implement a GUI around it. Among other featuers the GUI allows you to

  • click/drag primary mouse button to create obstacles 
  • click/drag secondary mouse button to remove obstacles 

Path finding is started once you release the mouse button In order to help you understand the algorithm better I added the values which we store in the cells.

  • g = top/left value 
  • h = top/right value 
  • f = g + h = center value 

 The colorization of the cells is as follows:

  • path = blue 
  • obstacle = black 
  • cells of closed set = green 
  • cells of open set = red 

You can use the step slider in order to show the calculations step by step.

A screenshot:



And here's a video of how it looks like in action:



Interested ones can get the AStarFX code on github. If you'd like to have other grid dimensions, simply modify the file Settings.java.

It will be nice to see this algorithm work in our tower defense game.

Have fun and keep on coding :-)

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

Friday, May 15, 2015

Simple Game Engine: Pong in JavaFX

Someone asked on StackOverflow about how to make the keyboard controls work in his game of Pong. That reminded me that I've never created that classic game. So I thought I'd give it a shot with our simple game engine.

Pong is a simple game in which two players try to keep a ball in the game by moving their paddles. In this blog we just create the base in which:

  • a ball moves around the screen
  • the ball bounces off the paddles
  • a player can control a paddle
  • another paddle is controlled by AI

The game loop consists of

  • processing player input
  • add the balls
  • move the paddles and the balls
  • check ball for paddle collision
  • update ui
  • remove sprites, e. g. if ball is outside scene
  • update score

So all in all we have
AnimationTimer gameLoop = new AnimationTimer() {

   @Override
   public void handle(long now) {
    
    // player input
    players.forEach(sprite -> sprite.processInput());

             // add random enemies
             spawnBalls();
             
    // movement
    players.forEach(sprite -> sprite.move());
    balls.forEach(sprite -> sprite.move());

    // check collisions
    checkCollisions();
    
    // update sprites in scene
    players.forEach(sprite -> sprite.updateUI());
    balls.forEach(sprite -> sprite.updateUI());
    
             // check if sprite can be removed
    balls.forEach(sprite -> sprite.checkRemovability());
             
             // remove removables from list, layer, etc
             removeSprites( balls);
             
             // update score, health, etc
             updateScore();
   }
   
  };
We keep the game in retro style, i. e. simple. There's really no need to use image files when we create the images ourselves. Here's a technique that may interest some of you. We simply create a node and then put a snapshot of it into an image:
 private void loadGame() {
  
  WritableImage wi;
  
  // create paddle image
  // -------------------------
  double w = Settings.PADDLE_WIDTH;
  double h = Settings.PADDLE_HEIGHT;
  
  Rectangle rect = new Rectangle( w, h);
  
  wi = new WritableImage( (int) w, (int) h);
  rect.snapshot(null, wi);
  
  playerImage = wi;
  enemyImage = wi;
  
  // create ball image
  // -------------------------
  double r = Settings.BALL_RADIUS;
  
  Circle circle = new Circle( r);
  
  wi = new WritableImage( (int) r * 2, (int) r * 2);
  circle.snapshot(null, wi);
  
  ballImage = wi;
 
 }
This blog is only supposed to be a start for your own game, so we keep it simple. The ball is merely bouncing off in horizontal direction. In the end one should consider the ball's position on the paddle and the paddle movement speed and calculate some bouncing angle.
public class Ball extends SpriteBase {

        ...
 public void bounceOff( SpriteBase sprite) {
  
  // TODO: consider angle
  dx = -dx;
 }
 
 @Override
 public void checkRemovability() {

  if( Double.compare( getX(), 0) < 0 || Double.compare( getX(), Settings.SCENE_WIDTH) > 0 || Double.compare( getY(), 0) < 0 || Double.compare( getY(), Settings.SCENE_HEIGHT) > 0) {
   setRemovable(true);
  }

  
 }
}
Similarly we keep the enemy simple by moving the paddle vertically. For the game one would have to consider the incoming ball's angle, determine the impact position and calculate the vertical movement of the paddle.
public class Enemy extends Player {

 private enum Direction {
  UP,
  DOWN;
 }
 
 Direction direction = Direction.UP;
        
        ... 

 @Override
 public void processInput() {
  
  if( direction == Direction.UP) {
   dy = -speed;
  } else {
   dy = speed;
  }
  
 }
 
 @Override
 protected void checkBounds() {

  super.checkBounds();
  
  if( y == playerShipMinY) {
   direction = Direction.DOWN;
  } else if( y == playerShipMaxY) {
   direction = Direction.UP;
  }

 }
}
The rest of the code is similar to what we've learned so far. We use a base class for the sprites, an input class, evaluate input in the player class, show and update score, etc.

Here's a screenshot of what the game looks like:



And here's the full source if you'd like to try it out for yourself and enhance it:

Game.java
package game.pong;

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

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class Game extends Application {

 Random rnd = new Random();
 
 Pane playfieldLayer;
 AnchorPane scoreLayer;
 
 Image playerImage;
 Image enemyImage;
 Image ballImage;
 
 List<Player> players = new ArrayList<>();
 List<Ball> balls = new ArrayList<>();
 
 Text playerScoreText = new Text();
 Text enemyScoreText = new Text();
 
 Map<Player,Text> scoreDisplay = new HashMap<>();
 
 Scene scene;
 
 @Override
 public void start(Stage primaryStage) {
  
  Group root = new Group();
  
  // create layers
  playfieldLayer = new Pane();
  playfieldLayer.setPrefSize(Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT);
  
  scoreLayer = new AnchorPane();
  scoreLayer.setPrefSize(Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT);
  
  root.getChildren().add( playfieldLayer);
  root.getChildren().add( scoreLayer);
  
  scene = new Scene( root, Settings.SCENE_WIDTH, Settings.SCENE_HEIGHT);
  
  primaryStage.setScene( scene);
  primaryStage.show();
  
  loadGame();
  
  createScoreLayer();
  createPlayers();
  
  AnimationTimer gameLoop = new AnimationTimer() {

   @Override
   public void handle(long now) {
    
    // player input
    players.forEach(sprite -> sprite.processInput());

             // add random enemies
             spawnBalls();
             
    // movement
    players.forEach(sprite -> sprite.move());
    balls.forEach(sprite -> sprite.move());

    // check collisions
    checkCollisions();
    
    // update sprites in scene
    players.forEach(sprite -> sprite.updateUI());
    balls.forEach(sprite -> sprite.updateUI());
    
             // check if sprite can be removed
    balls.forEach(sprite -> sprite.checkRemovability());
             
             // remove removables from list, layer, etc
             removeSprites( balls);
             
             // update score, health, etc
             updateScore();
   }
   
  };
  gameLoop.start();
  
 }
 
 private void loadGame() {
  
  WritableImage wi;
  
  // create paddle image
  // -------------------------
  double w = Settings.PADDLE_WIDTH;
  double h = Settings.PADDLE_HEIGHT;
  
  Rectangle rect = new Rectangle( w, h);
  
  wi = new WritableImage( (int) w, (int) h);
  rect.snapshot(null, wi);
  
  playerImage = wi;
  enemyImage = wi;
  
  // create ball image
  // -------------------------
  double r = Settings.BALL_RADIUS;
  
  Circle circle = new Circle( r);
  
  wi = new WritableImage( (int) r * 2, (int) r * 2);
  circle.snapshot(null, wi);
  
  ballImage = wi;
 
 }

 private void createScoreLayer() {
  
  playerScoreText.setFont( Font.font( null, FontWeight.BOLD, 32));
  enemyScoreText.setFont( Font.font( null, FontWeight.BOLD, 32));

  AnchorPane.setTopAnchor(playerScoreText, 0.0);
  AnchorPane.setLeftAnchor(playerScoreText, 10.0);

  AnchorPane.setTopAnchor(enemyScoreText, 0.0);
  AnchorPane.setRightAnchor(enemyScoreText, 10.0);

  scoreLayer.getChildren().add( playerScoreText);
  scoreLayer.getChildren().add( enemyScoreText);
  
 }
 
 private void createPlayers() {

  // create player instances
  Player player = createPlayer();
  Player enemy = createEnemy();
  
  // register player
  players.add( player);
  players.add( enemy);
  
  // assign score display
  scoreDisplay.put(player, playerScoreText);
  scoreDisplay.put(enemy, enemyScoreText);
   
 }
 
 private Player createPlayer() {

  // player input
  Input input = new Input( scene);
  
  // register input listeners
  input.addListeners(); // TODO: remove listeners on game over

  Image image = playerImage;
  
  // offset x position, center vertically
  double x = Settings.PADDLE_OFFSET_X;
  double y = (Settings.SCENE_HEIGHT - image.getHeight()) * 0.5;
  
  // create player
  Player player = new Player(playfieldLayer, image, x, y, 0, 0, 0, 0, 1, 0, Settings.PADDLE_SPEED, input);

  return player;
  
 }
 
 private Player createEnemy() {
  
  Image image = enemyImage;
  
  // offset x position, center vertically
  double x = Settings.SCENE_WIDTH - Settings.PADDLE_OFFSET_X - image.getWidth();
  double y = (Settings.SCENE_HEIGHT - image.getHeight()) * 0.5;
  
  // create player
  Enemy player = new Enemy(playfieldLayer, image, x, y, 0, 0, 0, 0, 1, 0, Settings.PADDLE_SPEED);
  
  return player;
  
 }
 
 private void spawnBalls() {
  if( balls.size() == 0) {
   createBall();
  }
 }
 private void createBall() {
  
  Image image = ballImage;
  
  // offset x position, center vertically
  double x = (Settings.SCENE_WIDTH - image.getWidth()) / 2;
  double y = (Settings.SCENE_HEIGHT - image.getHeight()) / 2;
  
  // create ball
  Ball ball = new Ball( playfieldLayer, image, x, y, 0, -Settings.BALL_SPEED, 0, 0, 1, 1);
  
  // register ball
  balls.add(ball);
   
 }

 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( Player player: players) {
   for( Ball ball: balls) {
    if( player.collidesWith(ball)) {
     
     // bounce ball
     ball.bounceOff(player);
     
     // add score
     // TODO: proper score handling: score only to the player if the ball leaves screen afterwards
     player.addScore( 1); 
     
    }
   }
  }
 }
 
 private void updateScore() {
  
  for( Player player: players) {
   scoreDisplay.get( player).setText( "" + (int) player.getScore());    
  }

 }
 
 public static void main(String[] args) {
  launch(args);
 }
  
}
Ball.java
package game.pong;

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

public class Ball extends SpriteBase {

 public Ball(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);
 }

 public void bounceOff( SpriteBase sprite) {
  
  // TODO: ensure the ball doesn't get stuck inside a player
  dx = -dx;
 }
 
 @Override
 public void checkRemovability() {

  if( Double.compare( getX(), 0) < 0 || Double.compare( getX(), Settings.SCENE_WIDTH) > 0 || Double.compare( getY(), 0) < 0 || Double.compare( getY(), Settings.SCENE_HEIGHT) > 0) {
   setRemovable(true);
  }

  
 }
}
Enemy.java
package game.pong;

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

public class Enemy extends Player {

 private enum Direction {
  UP,
  DOWN;
 }
 
 Direction direction = Direction.UP;
 
 public Enemy(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, speed, null);
 }

 @Override
 public void processInput() {
  
  if( direction == Direction.UP) {
   dy = -speed;
  } else {
   dy = speed;
  }
  
 }
 
 @Override
 protected void checkBounds() {

  super.checkBounds();
  
  if( y == playerShipMinY) {
   direction = Direction.DOWN;
  } else if( y == playerShipMaxY) {
   direction = Direction.UP;
  }

 }
}
Player.java
package game.pong;

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

public class Player extends SpriteBase {

 double playerShipMinX;
 double playerShipMaxX;
 double playerShipMinY;
 double playerShipMaxY;

 Input input;
 
 double speed;
 
 public Player(Pane layer, Image image, double x, double y, double r, double dx, double dy, double dr, double health, double damage, double speed, Input input) {

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

  this.speed = speed;
  this.input = input;
  
  init();
 }

 
 private void init() {
  
  // calculate movement bounds of the player ship
  playerShipMinX = x; // limit to vertical movement 
  playerShipMaxX = x; // limit to vertical movement 
  playerShipMinY = 0;
  playerShipMaxY = Settings.SCENE_HEIGHT -image.getHeight();
  
 }

 public void processInput() {
  
  // ------------------------------------
  // movement
  // ------------------------------------
  
     // vertical direction
     if( input.isMoveUp()) {
      dy = -speed;
     } else if( input.isMoveDown()) {
      dy = speed;
     } else {
      dy = 0d;
     }
     
     // horizontal direction
     if( input.isMoveLeft()) {
      dx = -speed;
     } else if( input.isMoveRight()) {
      dx = speed;
     } else {
      dx = 0d;
     }

 }
 
 @Override
 public void move() {
  
  super.move();
  
  // ensure the ship can't move outside of the screen
  checkBounds();
  
     
 }
 
 protected void checkBounds() {

     // vertical
     if( Double.compare( y, playerShipMinY) < 0) {
      y = playerShipMinY;
     } else if( Double.compare(y, playerShipMaxY) > 0) {
      y = playerShipMaxY;
     }

     // horizontal
     if( Double.compare( x, playerShipMinX) < 0) {
      x = playerShipMinX;
     } else if( Double.compare(x, playerShipMaxX) > 0) {
      x = playerShipMaxX;
     }

 }


 @Override
 public void checkRemovability() {
 }
 
}
Settings.java
package game.pong;


public class Settings {

 public static double SCENE_WIDTH = 600;
 public static double SCENE_HEIGHT = 400;

 public static double PADDLE_WIDTH = 20;
 public static double PADDLE_HEIGHT = 100;

 public static double BALL_RADIUS = 10;

 public static double PADDLE_OFFSET_X = 50;
 
 public static double PADDLE_SPEED = 4.0;
 public static double BALL_SPEED = 4.0;
 
}
Input.java
package game.pong;

import java.util.BitSet;

import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

public class Input {

    /**
     * Bitset which registers if any {@link KeyCode} keeps being pressed or if it is released.
     */
    private BitSet keyboardBitSet = new BitSet();

    // -------------------------------------------------
    // default key codes
    // will vary when you let the user customize the key codes or when you add support for a 2nd player
    // -------------------------------------------------

    private KeyCode upKey = KeyCode.UP;
    private KeyCode downKey = KeyCode.DOWN;
    private KeyCode leftKey = KeyCode.LEFT;
    private KeyCode rightKey = KeyCode.RIGHT;
    private KeyCode primaryWeaponKey = KeyCode.SPACE;
    private KeyCode secondaryWeaponKey = KeyCode.CONTROL;

    Scene scene;

    public Input( Scene scene) {
        this.scene = scene;
    }

    public void addListeners() {

        scene.addEventFilter(KeyEvent.KEY_PRESSED, keyPressedEventHandler);
        scene.addEventFilter(KeyEvent.KEY_RELEASED, keyReleasedEventHandler);

    }

    public void removeListeners() {

        scene.removeEventFilter(KeyEvent.KEY_PRESSED, keyPressedEventHandler);
        scene.removeEventFilter(KeyEvent.KEY_RELEASED, keyReleasedEventHandler);

    }

    /**
     * "Key Pressed" handler for all input events: register pressed key in the bitset
     */
    private EventHandler<KeyEvent> keyPressedEventHandler = new EventHandler<KeyEvent>() {
        @Override
        public void handle(KeyEvent event) {

            // register key down
            keyboardBitSet.set(event.getCode().ordinal(), true);

        }
    };

    /**
     * "Key Released" handler for all input events: unregister released key in the bitset
     */
    private EventHandler<KeyEvent> keyReleasedEventHandler = new EventHandler<KeyEvent>() {
        @Override
        public void handle(KeyEvent event) {

            // register key up
            keyboardBitSet.set(event.getCode().ordinal(), false);

        }
    };


    // -------------------------------------------------
    // Evaluate bitset of pressed keys and return the player input.
    // If direction and its opposite direction are pressed simultaneously, then the direction isn't handled.
    // -------------------------------------------------

    public boolean isMoveUp() {
        return keyboardBitSet.get( upKey.ordinal()) && !keyboardBitSet.get( downKey.ordinal());
    }

    public boolean isMoveDown() {
        return keyboardBitSet.get( downKey.ordinal()) && !keyboardBitSet.get( upKey.ordinal());
    }

    public boolean isMoveLeft() {
        return keyboardBitSet.get( leftKey.ordinal()) && !keyboardBitSet.get( rightKey.ordinal());  
    }

    public boolean isMoveRight() {
        return keyboardBitSet.get( rightKey.ordinal()) && !keyboardBitSet.get( leftKey.ordinal());
    }

    public boolean isFirePrimaryWeapon() {
        return keyboardBitSet.get( primaryWeaponKey.ordinal());
    }

    public boolean isFireSecondaryWeapon() {
        return keyboardBitSet.get( secondaryWeaponKey.ordinal());
    }

}
SpriteBase.java
package game.pong;

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;
 
 double score = 0;
 
 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 void addScore( double value) {
  this.score += value;
 }
 
 public double getScore() {
  return score;
 }
 public abstract void checkRemovability();

}

Simple Game Engine: Top-Down Scrolling Game in JavaFX


Someone asked on StackOverflow How do I move a sprite across a screen? I thought I'd help and create a minimalistic engine from our Anansi engine. It demonstates the necessary areas that you need to cover when you create a game.

This blog post is a recap of what we've learned so far.

We need a main class in which we load the game, create the scene and which contains the game loop for

  • input check
  • move player sprites
  • create and move AI sprites
  • check sprite collision
  • update sprites in the UI
  • update score


public class Game extends Application {

    Random rnd = new Random();

    Pane playfieldLayer;
    Pane scoreLayer;

    Image playerImage;
    Image enemyImage;

    List<Player> players = new ArrayList<>();
    List<Enemy> enemies = new ArrayList<>();

    Text collisionText = new Text();
    boolean collision = false;

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

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

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

        loadGame();

        createScoreLayer();
        createPlayers();

        AnimationTimer gameLoop = new AnimationTimer() {

            @Override
            public void handle(long now) {

                // player input
                players.forEach(sprite -> sprite.processInput());

                // add random enemies
                spawnEnemies( true);

                // movement
                players.forEach(sprite -> sprite.move());
                enemies.forEach(sprite -> sprite.move());

                // check collisions
                checkCollisions();

                // update sprites in scene
                players.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() {


        collisionText.setFont( Font.font( null, FontWeight.BOLD, 64));
        collisionText.setStroke(Color.BLACK);
        collisionText.setFill(Color.RED);

        scoreLayer.getChildren().add( collisionText);

        // TODO: quick-hack to ensure the text is centered; usually you don't have that; instead you have a health bar on top
        collisionText.setText("Collision");
        double x = (Settings.SCENE_WIDTH - collisionText.getBoundsInLocal().getWidth()) / 2;
        double y = (Settings.SCENE_HEIGHT - collisionText.getBoundsInLocal().getHeight()) / 2;
        collisionText.relocate(x, y);
        collisionText.setText("");

        collisionText.setBoundsType(TextBoundsType.VISUAL);


    }
    private void createPlayers() {

        // player input
        Input input = new Input( scene);

        // register input listeners
        input.addListeners(); // TODO: remove listeners on game over

        Image image = playerImage;

        // center horizontally, position at 70% vertically
        double x = (Settings.SCENE_WIDTH - image.getWidth()) / 2.0;
        double y = Settings.SCENE_HEIGHT * 0.7;

        // create player
        Player player = new Player(playfieldLayer, image, x, y, 0, 0, 0, 0, Settings.PLAYER_HEALTH, 0, Settings.PLAYER_SPEED, input);

        // register player
        players.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() {

        collision = false;

        for( Player player: players) {
            for( Enemy enemy: enemies) {
                if( player.collidesWith(enemy)) {
                    collision = true;
                }
            }
        }
    }

    private void updateScore() {
        if( collision) {
            collisionText.setText("Collision");
        } else {
            collisionText.setText("");
        }
    }

    public static void main(String[] args) {
        launch(args);
    }

}




Then we need a base class for sprites. The class unifies the common mechanisms that every movable object on the screen has. The sprites have attributes like

  • layout (usually an image)
  • position and rotation
  • health
  • damage they can cause

We provide methods for accessing and modifying these attributes. Specific changes are overridden in the Subclasses like Player and Enemy.

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

}





Every sprite has some kind of AI. The player sprite's AI is the user, we evaluate the user's input.
public class Player extends SpriteBase {

    double playerShipMinX;
    double playerShipMaxX;
    double playerShipMinY;
    double playerShipMaxY;

    Input input;

    double speed;

    public Player(Pane layer, Image image, double x, double y, double r, double dx, double dy, double dr, double health, double damage, double speed, Input input) {

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

        this.speed = speed;
        this.input = input;

        init();
    }


    private void init() {

        // calculate movement bounds of the player ship
        // allow half of the ship to be outside of the screen 
        playerShipMinX = 0 - image.getWidth() / 2.0;
        playerShipMaxX = Settings.SCENE_WIDTH - image.getWidth() / 2.0;
        playerShipMinY = 0 - image.getHeight() / 2.0;
        playerShipMaxY = Settings.SCENE_HEIGHT -image.getHeight() / 2.0;

    }

    public void processInput() {

        // ------------------------------------
        // movement
        // ------------------------------------

        // vertical direction
        if( input.isMoveUp()) {
            dy = -speed;
        } else if( input.isMoveDown()) {
            dy = speed;
        } else {
            dy = 0d;
        }

        // horizontal direction
        if( input.isMoveLeft()) {
            dx = -speed;
        } else if( input.isMoveRight()) {
            dx = speed;
        } else {
            dx = 0d;
        }

    }

    @Override
    public void move() {

        super.move();

        // ensure the ship can't move outside of the screen
        checkBounds();


    }

    private void checkBounds() {

        // vertical
        if( Double.compare( y, playerShipMinY) < 0) {
            y = playerShipMinY;
        } else if( Double.compare(y, playerShipMaxY) > 0) {
            y = playerShipMaxY;
        }

        // horizontal
        if( Double.compare( x, playerShipMinX) < 0) {
            x = playerShipMinX;
        } else if( Double.compare(x, playerShipMaxX) > 0) {
            x = playerShipMaxX;
        }

    }

}




The enemy sprites in our simple engine just move from one direction to the other.
public class Enemy extends SpriteBase {

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

    @Override
    public void checkRemovability() {

        if( Double.compare( getY(), Settings.SCENE_HEIGHT) > 0) {
            setRemovable(true);
        }


    }
}



Then we need an input mechanism. We create a dedicated class for that so that we can easily create various instances in case we'd like to create a multiplayer game.
public class Input {

    /**
     * Bitset which registers if any {@link KeyCode} keeps being pressed or if it is released.
     */
    private BitSet keyboardBitSet = new BitSet();

    // -------------------------------------------------
    // default key codes
    // will vary when you let the user customize the key codes or when you add support for a 2nd player
    // -------------------------------------------------

    private KeyCode upKey = KeyCode.UP;
    private KeyCode downKey = KeyCode.DOWN;
    private KeyCode leftKey = KeyCode.LEFT;
    private KeyCode rightKey = KeyCode.RIGHT;
    private KeyCode primaryWeaponKey = KeyCode.SPACE;
    private KeyCode secondaryWeaponKey = KeyCode.CONTROL;

    Scene scene;

    public Input( Scene scene) {
        this.scene = scene;
    }

    public void addListeners() {

        scene.addEventFilter(KeyEvent.KEY_PRESSED, keyPressedEventHandler);
        scene.addEventFilter(KeyEvent.KEY_RELEASED, keyReleasedEventHandler);

    }

    public void removeListeners() {

        scene.removeEventFilter(KeyEvent.KEY_PRESSED, keyPressedEventHandler);
        scene.removeEventFilter(KeyEvent.KEY_RELEASED, keyReleasedEventHandler);

    }

    /**
     * "Key Pressed" handler for all input events: register pressed key in the bitset
     */
    private EventHandler<KeyEvent> keyPressedEventHandler = new EventHandler<KeyEvent>() {
        @Override
        public void handle(KeyEvent event) {

            // register key down
            keyboardBitSet.set(event.getCode().ordinal(), true);

        }
    };

    /**
     * "Key Released" handler for all input events: unregister released key in the bitset
     */
    private EventHandler<KeyEvent> keyReleasedEventHandler = new EventHandler<KeyEvent>() {
        @Override
        public void handle(KeyEvent event) {

            // register key up
            keyboardBitSet.set(event.getCode().ordinal(), false);

        }
    };


    // -------------------------------------------------
    // Evaluate bitset of pressed keys and return the player input.
    // If direction and its opposite direction are pressed simultaneously, then the direction isn't handled.
    // -------------------------------------------------

    public boolean isMoveUp() {
        return keyboardBitSet.get( upKey.ordinal()) && !keyboardBitSet.get( downKey.ordinal());
    }

    public boolean isMoveDown() {
        return keyboardBitSet.get( downKey.ordinal()) && !keyboardBitSet.get( upKey.ordinal());
    }

    public boolean isMoveLeft() {
        return keyboardBitSet.get( leftKey.ordinal()) && !keyboardBitSet.get( rightKey.ordinal());  
    }

    public boolean isMoveRight() {
        return keyboardBitSet.get( rightKey.ordinal()) && !keyboardBitSet.get( leftKey.ordinal());
    }

    public boolean isFirePrimaryWeapon() {
        return keyboardBitSet.get( primaryWeaponKey.ordinal());
    }

    public boolean isFireSecondaryWeapon() {
        return keyboardBitSet.get( secondaryWeaponKey.ordinal());
    }

}



And then we need some special settings in which we store the attributes of our game.
public class Settings {

    public static double SCENE_WIDTH = 400;
    public static double SCENE_HEIGHT = 800;

    public static double PLAYER_SPEED = 4.0;
    public static double PLAYER_HEALTH = 100.0;

    public static int ENEMY_SPAWN_RANDOMNESS = 100;

}


You can use any image for the sprites. I took mine from Wikipedia:

player.png



enemy.png



We can now put all the classes and images together into a single Java package and start the main class Game.java.

The gameplay is simple: The Zombie sprites are scrolling down from top to bottom, you have to evade them by moving the Smiley sprite via the cursor keys. A "Collision" text appears when the player sprite collides with the enemy sprites. The collision detection is a simple intersection check of the rectangular area of the sprites. For the final game one could consider a per-pixel collision detection.

Here's a screenshot of how it looks like:


Have fun! I hope this helps some of you in starting to create your own game.