Musings about Java and Swing

Horse Race GUI

I’ve written Java articles about 3 other games; Hangman Swing GUI, Minesweeper Applet, and Sudoku Solver Swing GUI. Each illustrated certain principles that go into creating a Java GUI.

This is an animated horse race GUI. I kept it simple to illustrate what goes into making an animated Java GUI.

Here’s what the game looks like when a race is about to start.

Here’s what the game looks like when the horses are running.

Here’s what the game looks like when the horses have finished the race.

I purposefully kept the game simple. I drew circles for horses. I did not try and move the race track to make the race last longer. Even keeping the game simple, I wrote 10 Java classes.

The Horse Race GUI consists of 3 main JPanels. There’s a winner JPanel at the top, a race JPanel in the middle, and a button / timer JPanel at the bottom. The button / timer JPanel contains 2 JPanels; a button JPanel and a timer JPanel. I used the GridBagLayout as the layout manager for the button / timer JPanel, so I could get the JButtons to center vertically.

The Java application consists of 3 model classes, 4 view classes, and 2 controller classes. I use the model / view / controller (MVC) architecture to separate the classes and keep each class simple.

Here is the main class.

package com.ggl.horse.race;

import javax.swing.SwingUtilities;

import com.ggl.horse.race.model.RaceBuilder;
import com.ggl.horse.race.view.RaceFrame;

public class HorseRace implements Runnable {
	
	public static void main(String[] args) {
		SwingUtilities.invokeLater(new HorseRace());
	}

	@Override
	public void run() {		
		new RaceFrame(RaceBuilder.create400Pixel3HorseRace());
	}

}

This main class, similar to all of my Java GUI main classes, does 3 things.

– Puts the Swing components on the Event Dispatch thread (EDT).
– Creates a new Race instance.
– Creates a new RaceFrame (JFrame) instance.

You should start your Java GUI application with a similar class.

Next, let’s look at the RaceBuilder class.

package com.ggl.horse.race.model;

import java.awt.Color;

public class RaceBuilder {

	public static Race create400Pixel3HorseRace() {
		Horse horse1 = new Horse(Color.RED, "Red Rover");
		Horse horse2 = new Horse(Color.ORANGE, "Golden Girl");
		Horse horse3 = new Horse(Color.GREEN, "Clover Green");

		Race race = new Race(400.0D);
		race.addHorse(horse1);
		race.addHorse(horse2);
		race.addHorse(horse3);

		return race;
	}
	
	public static Race create500Pixel5HorseRace() {
		Horse horse1 = new Horse(Color.RED, "Red Rover");
		Horse horse2 = new Horse(Color.ORANGE, "Golden Girl");
		Horse horse3 = new Horse(Color.GREEN, "Clover Green");
		Horse horse4 = new Horse(Color.GRAY, "Gray Ghost");
		Horse horse5 = new Horse(Color.CYAN, "Cyan I Cried");

		Race race = new Race(500.0D);
		race.addHorse(horse1);
		race.addHorse(horse2);
		race.addHorse(horse3);
		race.addHorse(horse4);
		race.addHorse(horse5);

		return race;
	}
}

The static methods create400Pixel3HorseRace and create500Pixel5HorseRace set all of the fields in the Race and Horse classes to create 3 and 5 instances of Horse and one instance of Race. Other static methods could be written to set up other race conditions.

The RaceBuilder class allows me to put all of the code to construct different kinds of races in one class.

Let’s take a look at the Race class.

package com.ggl.horse.race.model;

import java.awt.Color;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.List;

public class Race {

	/** Distance of race in pixels */
	private double		distance;

	private long		elapsedTime;

	private List<Horse>	horses;

	public Race(double distance) {
		this.distance = distance;
		this.horses = new ArrayList<Horse>();
		this.elapsedTime = 0;
	}

	public void init() {
		this.elapsedTime = 0;
		for (Horse horse : horses) {
			horse.init();
		}
	}

	public void addHorse(Horse horse) {
		this.horses.add(horse);
	}

	public int getHorseCount() {
		return horses.size();
	}

	public double getDistance() {
		return distance;
	}

	public void setElapsedTime(long elapsedTime) {
		if (isWinner() == null) {
			this.elapsedTime = elapsedTime;
		}
	}

	public String getElapsedTime() {
		int centiseconds = (int) (((elapsedTime % 1000L) + 5L) / 10L);
		int seconds = (int) (elapsedTime / 1000L);
		if (seconds < 60) {
			return String.format("%2d.%02d", seconds, centiseconds);
		} else {
			int minutes = seconds / 60;
			seconds -= minutes * 60;
			return String.format("%2d:%02d.%02d", minutes, seconds,
					centiseconds);
		}
	}

	public int getTrackWidth() {
		return (int) Math.round(getDistance()) + 100;
	}

	public int getTrackHeight() {
		return getHorseCount() * Horse.POSITION + Horse.MARGIN;
	}

	public void setHorseVelocity() {
		for (Horse horse : horses) {
			horse.setVelocity();
		}
	}

	public void updateHorsePositions(int milliseconds) {
		for (Horse horse : horses) {
			horse.moveHorse(milliseconds);
		}
	}

	public Horse isWinner() {
		for (Horse horse : horses) {
			if ((distance - Horse.RADIUS) <= horse.getDistance()) {
				return horse;
			}
		}

		return null;
	}

	public boolean allHorsesRunning() {
		for (Horse horse : horses) {
			if ((distance + Horse.RADIUS + 6) > horse.getDistance()) {
				return true;
			}
		}

		return false;
	}

	public void draw(Graphics g) {
		drawLine(g, Horse.POSITION, 6);
		drawLine(g, (int) Math.round(getDistance()) + Horse.RADIUS
				+ Horse.MARGIN, 6);

		for (Horse horse : horses) {
			horse.draw(g);
		}
	}

	private void drawLine(Graphics g, int x, int width) {
		int y = Horse.MARGIN;
		int height = getHorseCount() * Horse.POSITION - y;
		g.setColor(Color.BLACK);
		g.fillRect(x, y, width, height);
	}

}

The Race class contains all of the information that is displayed on the Horse Race GUI. All of the logic for manipulating the information is present in Race class methods.

The draw method draws everything. The draw method is in the model, rather than the view, because it’s lots easier in Java to have objects draw themselves. The draw code may reside in the model, but it’s still executed as a part of the view. We still have an MVC separation of concerns.

Let’s take a look at the last model class, the Horse class.

package com.ggl.horse.race.model;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.util.Random;

public class Horse {
	
	public static final int RADIUS = 15;
	public static final int MARGIN = 15;
	public static final int DIAMETER = RADIUS + RADIUS;
	public static final int POSITION = DIAMETER + MARGIN;
	
	private static Point currentPosition;
	
	static {
		int x = MARGIN + RADIUS;
		int y = MARGIN + RADIUS;
		currentPosition = new Point(x, y);
	}
	
	private static Random random = new Random();
	
	
	/** Distance in pixels */
	private double distance;
	
	/** Velocity in pixels per second */
	private int velocity;
	
	private Color color;

	/** Initial position in pixels */
	private Point initialPosition;
	
	private String name;

	public Horse(Color color, String name) {
		setInitialPosition();
		this.color = color;
		this.name = name;
		init();
	}

	private void setInitialPosition() {
		this.initialPosition = 
				new Point(currentPosition.x, currentPosition.y);
		currentPosition.y += POSITION;
	}
	
	public void init() {
		this.distance = 0.0D;
	}
	
	public void setVelocity() {
		this.velocity = random.nextInt(5) + 6;
	}
	
	public double getDistance() {
		return distance;
	}
	
	public String getName() {
		return name;
	}

	public void moveHorse(int milliseconds) {
		double pixels = 0.001D * velocity * milliseconds;
		this.distance += pixels;
	}
	
	public void draw(Graphics g) {
		g.setColor(color);
		g.fillOval(initialPosition.x + (int) Math.round(distance) - RADIUS,
				initialPosition.y - RADIUS, DIAMETER, DIAMETER);
	}

}

The Horse class is a basic getter / setter class, with a few exceptions.

The initial position of each horse is calculated using static fields. This allows the user of this class (the RaceBuilder class) to define horses without having to define their starting positions.

The setVelocity method changes the velocity of the horse randomly, to make the horse race more interesting. You’ll see this later in the controller code, but the animation is rendered at 40 frames per second. The horse’s positions are updated every frame, and the setVelocity method is called every second to set a new velocity for each horse.

The draw method draws a horse (circle) at its initial position + the distance the horse ran. The initial position is never changed. This draw method is the method that would be changed if you wanted to draw horse images instead of circles.

Finally, we get to the view classes.

The first view class is the RaceFrame (JFrame) class. I use a JFrame in this class. I do not extend a JFrame.

You should only extend a Swing component (or any Java class) when you want to override one of the methods. When you use a JFrame, your methods are in a separate list from the JFrame methods in Eclipse, making them much easier to find.

package com.ggl.horse.race.view;

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.JFrame;
import javax.swing.JPanel;

import com.ggl.horse.race.model.Horse;
import com.ggl.horse.race.model.Race;

public class RaceFrame {

	private JFrame frame;
	
	private Race race;
	
	private RacePanel racePanel;
	
	private StatusPanel statusPanel;
	
	private WinnerPanel winnerPanel;

	public RaceFrame(Race race) {
		this.race = race;
		createPartControl();
	}
	
	protected void createPartControl() {
		winnerPanel = new WinnerPanel();
		racePanel = new RacePanel(race);
		statusPanel = new StatusPanel(this, race);

		frame = new JFrame();
		frame.setTitle("Horse Race");
		frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		frame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent event) {
				exitProcedure();
			}
		});

		frame.setLayout(new FlowLayout());

		JPanel panel = new JPanel();
		panel.setLayout(new BorderLayout());
		panel.add(winnerPanel.getPanel(), BorderLayout.NORTH);
		panel.add(racePanel, BorderLayout.CENTER);
		panel.add(statusPanel.getPanel(), BorderLayout.SOUTH);
		
		frame.add(panel);
		frame.setLocationByPlatform(true);
		frame.pack();
		frame.setVisible(true);
	}

	public void exitProcedure() {
		frame.dispose();
		System.exit(0);
	}

	public JFrame getFrame() {
		return frame;
	}
	
	public void clearWinnerText() {
		winnerPanel.clearText();
	}

	public void declareWinner(Horse horse) {
		winnerPanel.declareWinner(horse);
	}
	
	public void repaintRacePanel() {
		racePanel.repaint();
	}
	
	public void updateTimer() {
		statusPanel.updateTimer(race.getElapsedTime());
	}
	
}

The createPartControl method is boilerplate code. The only code that changes is the panel initialization and the convenience methods at the bottom. By convenience methods, I mean methods that work on the inner JPanels of the GUI. They are replicated in the RaceFrame class so that the controller classes can do everything they need to do through the RaceFrame instance.

Let’s look at the RacePanel class. This class extends JPanel because we want to override the paintComponent method so we can paint (draw) the horses and track on the panel.

package com.ggl.horse.race.view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;

import javax.swing.JPanel;

import com.ggl.horse.race.model.Race;

public class RacePanel extends JPanel {
	
	private static final long	serialVersionUID	= 1040577191811714944L;

	private Race race;

	public RacePanel(Race race) {
		this.race = race;
		int width = race.getTrackWidth();
		int height = race.getTrackHeight();
		this.setPreferredSize(new Dimension(width, height));
	}
	
	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		drawBackground(g);
		race.draw(g);
	}

	private void drawBackground(Graphics g) {
		g.setColor(Color.WHITE);
		g.fillRect(0, 0, getWidth(), getHeight());
	}

}

The RacePanel class is short. All of the values and draw methods are defined in the model, so all this class does is draw the race objects on the JPanel canvas.

We set the preferred size in the constructor. The packed size would be zero width and zero height, since there are no child Swing components. Whenever you use a JPanel as a canvas, you have to set the preferred size. It’s an easy thing to forget to do.

You should set a serialVersionUID for any class that implements Serializable. Since JPanel implements Serializable, my extended class needs a serialVersionUID. A serialVersionUID defines the particular serialization. If you add or remove fields from the Serializable class, you should change the serialVersionUID.

Next, let’s look at the WinnerPanel class. This is the class that displays the winner message at the top of the GUI.

package com.ggl.horse.race.view;

import java.awt.FlowLayout;

import javax.swing.JLabel;
import javax.swing.JPanel;

import com.ggl.horse.race.model.Horse;

public class WinnerPanel {

	private JLabel winnerLabel;
	
	private JPanel panel;

	public WinnerPanel() {
		createPartControl();
	}
	
	private void createPartControl() {
		panel = new JPanel();
		panel.setLayout(new FlowLayout());
		
		winnerLabel = new JLabel(" ");
		winnerLabel.setFont(winnerLabel.getFont().deriveFont(18.0F));
		
		panel.add(winnerLabel);
	}
	
	public void declareWinner(Horse horse) {
		StringBuilder builder = new StringBuilder();
		builder.append(horse.getName());
		builder.append(" is the winner!");
		winnerLabel.setText(builder.toString());
	}
	
	public void clearText() {
		winnerLabel.setText(" ");
	}

	public JPanel getPanel() {
		return panel;
	}

}

The WinnerPanel class is pretty simple. We define a JPanel and define a JLabel to go inside the JPanel. The JLabel will not size correctly unless you pass a non-empty String to the constructor. I passed a single blank character.

The declareWinner method displays the winner message in the JLabel. The clearText method sets the JLabel back to a single blank character.

Finally, let’s look at the last of the view classes, the StatusPanel. The StatusPanel is the JPanel at the bottom of the GUI that holds the buttons and the race timer.

package com.ggl.horse.race.view;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;

import com.ggl.horse.race.controller.RaceRunnable;
import com.ggl.horse.race.model.Race;

public class StatusPanel {
	
	private static final Insets	defaultInsets	= new Insets(10, 10, 10, 10);
	
	private JButton setButton;
	private JButton startButton;
	
	private JLabel timerDisplayLabel;

	private JPanel panel;
	
	private Race race;
	
	private RaceFrame raceFrame;
	
	private RaceRunnable raceRunnable;

	public StatusPanel(RaceFrame raceFrame, Race race) {
		this.raceFrame = raceFrame;
		this.race = race;
		createPartControl();
	}
	
	private void createPartControl() {
		panel = new JPanel();
		panel.setLayout(new GridBagLayout());
		
		JPanel buttonPanel = new JPanel();
		buttonPanel.setAlignmentY(JPanel.CENTER_ALIGNMENT);
		buttonPanel.setLayout(new FlowLayout());
		
		setButton = new JButton("Line horses up");
		setButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent event) {
				setRace();
			}
		});
		buttonPanel.add(setButton);
		
		startButton = new JButton("Start race");
		startButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent event) {
				if (raceRunnable != null) {
					setRace();
				}
				raceRunnable = new RaceRunnable(raceFrame, race);
				new Thread(raceRunnable).start();
			}	
		});
		buttonPanel.add(startButton);
		
		setButtonSizes(setButton, startButton);
		
		addComponent(panel, buttonPanel, 0, 0, 1, 1,
				defaultInsets, GridBagConstraints.CENTER,
				GridBagConstraints.CENTER);
		
		JPanel timerPanel = new JPanel();
		timerPanel.setBorder(BorderFactory.createLineBorder(Color.BLUE, 6));
		timerPanel.setLayout(new FlowLayout());
		timerPanel.setPreferredSize(new Dimension(160, 64));
		
		timerDisplayLabel = new JLabel(race.getElapsedTime());
		Font font = timerDisplayLabel.getFont();
		Font labelFont = font.deriveFont(32.0F);
		timerDisplayLabel.setFont(labelFont);
		timerDisplayLabel.setForeground(Color.BLUE);
		
		timerPanel.add(timerDisplayLabel);
			
		addComponent(panel, timerPanel, 1, 0, 1, 1,
				defaultInsets, GridBagConstraints.CENTER,
				GridBagConstraints.CENTER);
	}
	
	private void setButtonSizes(JButton ... buttons) {
		Dimension preferredSize = new Dimension();
		for (JButton button : buttons) {
			Dimension d = button.getPreferredSize();
			preferredSize = setLarger(preferredSize, d);
		}
		for (JButton button : buttons) {
			button.setPreferredSize(preferredSize);
		}
	}
	
	private Dimension setLarger(Dimension a, Dimension b) {
		Dimension d = new Dimension();
		d.height = Math.max(a.height, b.height);
		d.width = Math.max(a.width, b.width);
		return d;
	}
	
	private void addComponent(Container container, Component component,
			int gridx, int gridy, int gridwidth, int gridheight, Insets insets,
			int anchor, int fill) {
		GridBagConstraints gbc = new GridBagConstraints(gridx, gridy,
				gridwidth, gridheight, 1.0D, 1.0D, anchor, fill, insets, 0, 0);
		container.add(component, gbc);
	}
	
	private void setRace() {
		if (raceRunnable != null) {
			raceRunnable.stopRace();
			raceRunnable = null;
		}
		
		race.init();
		raceFrame.clearWinnerText();
		raceFrame.repaintRacePanel();
		raceFrame.updateTimer();
	}
	
	public void updateTimer(String time) {
		timerDisplayLabel.setText(time);
	}

	public JPanel getPanel() {
		return panel;
	}
		
}

I had to use the GridBagLayout to get the button JPanel to center. I create a new GridBagConstraints for each Swing component because I don’t like to rely on defaults. I also tend to type GridBagConstants when I need GridBagConstraints.

The setButtonSizes method makes all of the buttons passed the same size as the largest button. Making all the buttons the same size makes your GUI look more professional.

The action listeners for the set button and the start button are coded in the class as anonymous classes. I did this because both actionPerformed methods were short. The setRace method executes quickly, so I stopped the GUI while everything initialized. The actual race is run in the RaceRunnable runnable, which is executed in a separate thread from the EDT. The reason for this is to keep the GUI responsive while a long running RaceRunnable runs.

Running the race in a separate thread allows the user to stop a race before it ends.

Up to this point, I’ve written pretty standard Java Swing GUI code. The difference between a static GUI and an animated GUI is one or more threads that run the animation. In the Horse Race GUI, I have one thread to run the race and one thread to draw the race frame. I draw the race frame in a separate runnable because I have to execute Swing component commands on the EDT.

Let’s look at the RaceRunnable class.

package com.ggl.horse.race.controller;

import javax.swing.SwingUtilities;

import com.ggl.horse.race.model.Race;
import com.ggl.horse.race.view.RaceFrame;

public class RaceRunnable implements Runnable {
	
	private static final int FRAMES_PER_SECOND = 40;
	
	private volatile boolean raceRunning;
	
	private long frameStart;
	private long frameTime;
	private long raceStart;
	
	private Race race;
	
	private RaceFrame raceFrame;

	public RaceRunnable(RaceFrame raceFrame, Race race) {
		this.raceFrame = raceFrame;
		this.race = race;
		this.raceRunning = true;
	}

	@Override
	public void run() {
		raceStart = System.currentTimeMillis();
		frameTime = 1000L / FRAMES_PER_SECOND;
		int frameCount = FRAMES_PER_SECOND;
		DrawRunnable drawRunnable = new DrawRunnable(raceFrame, race);
		
		while (raceRunning && race.allHorsesRunning()) {
			frameStart = System.currentTimeMillis();
			
			frameCount++;
			if (frameCount >= FRAMES_PER_SECOND) {
				race.setHorseVelocity();
				frameCount = 0;
			}
				
			race.updateHorsePositions((int) frameTime);
			race.setElapsedTime(System.currentTimeMillis() - raceStart);
			SwingUtilities.invokeLater(drawRunnable);
			
			long frameElapsedTime = System.currentTimeMillis() - frameStart;
			// We have to sleep for a positive period
			long frameSleepTime = Math.max(5L, frameTime - frameElapsedTime);
			
			try {
				Thread.sleep(frameSleepTime);
			} catch (InterruptedException e) {
			}
		}
		
	}
	
	public synchronized void stopRace() {
		raceRunning = false;
	}

}

The basis for any Java two dimensional animation is a timing loop. In the RaceRunnable class, you can see the while loop in the run method.

This timing loop is doing two things at the same time; moving the horses and incrementing the race timer. First, we get the current frame time. Next, we set the horse velocity once per second for all the horses. Next, we update the horse’s positions. Next, we set the race elapsed time. Finally, we draw the race frame.

We determine how long to sleep based on the frame rate. Since the frame rate is 40 frames per second, we have 25 milliseconds to get everything accomplished. If we go over 25 milliseconds, we sleep for 5 milliseconds. We have to sleep for a period of time to keep the CPU from pinging at 100%. If we keep going over 25 milliseconds, we will drop frames. If we stay under 25 milliseconds, we sleep to keep the frame rate at 40 frames per second.

The stopRace method allows us to stop the thread before the race is over.

Finally, let’s look at the DrawRunnable class.

package com.ggl.horse.race.controller;

import com.ggl.horse.race.model.Horse;
import com.ggl.horse.race.model.Race;
import com.ggl.horse.race.view.RaceFrame;

public class DrawRunnable implements Runnable {
	
	private boolean raceRunning;
	
	private Race race;
	
	private RaceFrame raceFrame;

	public DrawRunnable(RaceFrame raceFrame, Race race) {
		this.raceFrame = raceFrame;
		this.race = race;
		this.raceRunning = true;
	}

	@Override
	public void run() {
		declareWinner();
		raceFrame.repaintRacePanel();
		raceFrame.updateTimer();
	}
	
	private void declareWinner() {
		if (raceRunning) {
			Horse winner = race.isWinner();
			if (winner != null) {
				raceFrame.declareWinner(winner);
				raceRunning = false;
			}
		}
	}		

}

The DrawRunnable draws the animation frame. Each animation frame is completely redrawn. It’s been my experience that trying to redraw part of the JPanel (canvas) is a lot of trouble for no gain. You can see on your own computer, but my Windows XP computer updates and draws a frame in about 5 to 7 milliseconds. That means the while loop is sleeping most of the time. Which is nice for the other running applications on your Windows computer.

I hope that this article shows you how to put a Java Swing animation GUI together. Your timing loop can do many things at the same time, as long as each iteration of the loop (or frame) is the smallest increment of time for your methods.

Post a Comment

Your email is kept private. Required fields are marked *