Part 1 - Building the GUI
Part 2 - Key Listener and Scheduler
Continuing with the Sword Sword Revolution game, today I decided to tackle animation. Specifically, I needed the ability to randomly generate the arrow images, and then scroll them along the bottom of the screen, right to left. This proved to be much more difficult than I had originally imagined, and I owe a huge thanks to the folks over at StackOverflow, specifically Hovercraft Full of Eels and Aurelien Ribon.
When it comes to animating in Java, there are a few options. Many people try to create their own Layout Manager or use the null layout option and just set the location of a component and then repaint the container. I even tried the second one, but learn from my mistake. Null layout is NOT a good idea and both of these options just make things more difficult. Other options include using an external library such as SlidingLayout or The Universal Tween Engine, or the route that I decided on: overwriting the existing paintComponent function.
The first thing I needed to do was create a new class that extended JPanel. The reason for this is that in order to customize the paintComponent function, you have to have a custom class. This class is very specific to my exact needs, but can easily be modified for other projects.
First, I needed to manage the necessary imports and instantiate the necessary instance variables.
import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.LinkedList; import java.util.Random; import javax.imageio.ImageIO; import javax.swing.JPanel; import javax.swing.Timer; public class AnimatedPanel extends JPanel { private static final long serialVersionUID = -7564181037729291253L; private static final int PREF_W = 600; private static final int PREF_H = 100; private static final int TIMER_DELAY = 15; private LinkedListimages = new LinkedList (); private BufferedImage rightarrow, leftarrow, uparrow, downarrow; private LinkedList xLocs = new LinkedList (); private int moveSpeed = 3; private long totalTime = 0, nextNewTime = 0; private static Random r; }
Next, I created the constructor. Here I needed to instantiate a few variables, load my image files, and then schedule a timer thread to add new arrow images, and move the arrow images currently on the screen.
public AnimatedPanel() { r = new Random(); try { rightarrow = ImageIO.read(new File("src/images/arrowright.gif")); leftarrow = ImageIO.read(new File("src/images/arrowleft.gif")); uparrow = ImageIO.read(new File("src/images/arrowup.gif")); downarrow = ImageIO.read(new File("src/images/arrowdown.gif")); } catch (IOException e) { System.exit(0); } new Timer(TIMER_DELAY, new ActionListener(){ public void actionPerformed(java.awt.event.ActionEvent e) { int index = 0; totalTime += TIMER_DELAY; if (totalTime >= nextNewTime) addArrow(); for(int imgX : xLocs) { xLocs.set(index, imgX - moveSpeed); index++; } repaint(); }; }).start(); }
The Timer used here is the javax.swing.Timer, NOT the java.util.Timer class. The TIMER_DELAY parameter is passed in milliseconds, and then the ActionListener which calls its actionPerformed function whenever the delay is reached. This Timer runs indefinitely until you manually cancel it, which we don't particularly care about for the purposes of this project.
The function I wrote to add an arrow image to the screen uses a random number generator to decide which of the four arrow directions should be added. It also sets the initial x location to be 600 which is just off the edge of the screen.
private void addArrow() { int arrow = r.nextInt(4); nextNewTime = totalTime + r.nextInt(1000) + 300 + (300 / moveSpeed); switch(arrow){ case 0: images.add(rightarrow); xLocs.add(600); break; case 1: images.add(leftarrow); xLocs.add(600); break; case 2: images.add(downarrow); xLocs.add(600); break; case 3: images.add(uparrow); xLocs.add(600); break; } }
Finally, I need to override the existing paintComponent function with my own, custom version. Essentially, I just need to determine the proper location for each of the images on the screen, and then display them.
@Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; g.setColor(Color.LIGHT_GRAY); g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); int index = 0; for(BufferedImage image : images) { if (image != null) g.drawImage(image, xLocs.get(index), 10, 65, 65, this); index++; } }
And with that, we have animated arrows scrolling across the bottom of the screen.